From 705aea398b1d4ce4660b5318a1515931412eef0b Mon Sep 17 00:00:00 2001 From: baurine <2008.hbl@gmail.com> Date: Fri, 19 Feb 2021 21:56:44 +0800 Subject: [PATCH 01/51] add gitignore --- .gitignore | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.gitignore b/.gitignore index 662b23b6b6..8aced31d93 100644 --- a/.gitignore +++ b/.gitignore @@ -33,3 +33,7 @@ bin/ docker/secret/ *__failpoint_binding__.go *__failpoint_stash__ + +# NPM packages +node_modules/ +build/ From 13db3d0d9b0218f827139e79da3b5363b96bd49f Mon Sep 17 00:00:00 2001 From: baurine <2008.hbl@gmail.com> Date: Fri, 19 Feb 2021 22:04:12 +0800 Subject: [PATCH 02/51] add cluster-ui back --- cluster-ui/.gitignore | 23 + cluster-ui/CHANGELOG.md | 41 + cluster-ui/README.md | 29 + cluster-ui/config-overrides.js | 36 + cluster-ui/package.json | 53 + cluster-ui/public/favicon.ico | Bin 0 -> 3150 bytes cluster-ui/public/index.html | 98 + cluster-ui/public/logo192.png | Bin 0 -> 5347 bytes cluster-ui/public/logo512.png | Bin 0 -> 9664 bytes cluster-ui/public/manifest.json | 25 + cluster-ui/public/robots.txt | 3 + cluster-ui/src/App.less | 1 + cluster-ui/src/App.tsx | 41 + cluster-ui/src/apis/api.ts | 67 + cluster-ui/src/apis/index.ts | 3 + cluster-ui/src/apis/request.ts | 51 + cluster-ui/src/components/Root.tsx | 5 + cluster-ui/src/components/index.ts | 1 + cluster-ui/src/hooks/index.ts | 5 + cluster-ui/src/hooks/useComps.ts | 21 + cluster-ui/src/hooks/useGlobalDir.ts | 20 + cluster-ui/src/hooks/useGlobalLoginOptions.ts | 14 + cluster-ui/src/hooks/useMachines.ts | 21 + cluster-ui/src/hooks/useQueryParams.ts | 17 + cluster-ui/src/index.css | 13 + cluster-ui/src/index.tsx | 12 + .../src/pages/Clusters/ClusterDetail.tsx | 275 + .../src/pages/Clusters/ClusterScaleOut.tsx | 10 + .../src/pages/Clusters/DashboardPortal.tsx | 186 + cluster-ui/src/pages/Clusters/index.tsx | 66 + .../src/pages/Deployment/CompsManager.tsx | 285 + .../src/pages/Deployment/DeploymentTable.tsx | 193 + .../src/pages/Deployment/EditCompForm.tsx | 191 + .../src/pages/Deployment/GlobalDirForm.tsx | 62 + .../src/pages/Deployment/TopoPreview.tsx | 149 + cluster-ui/src/pages/Deployment/index.tsx | 6 + cluster-ui/src/pages/Home/index.tsx | 69 + .../pages/Machines/GlobalLoginOptionsForm.tsx | 50 + cluster-ui/src/pages/Machines/MachineForm.tsx | 259 + .../src/pages/Machines/MachinesTable.tsx | 86 + cluster-ui/src/pages/Machines/index.tsx | 144 + cluster-ui/src/pages/Setting/index.tsx | 44 + .../src/pages/Status/OperationStatus.tsx | 80 + cluster-ui/src/pages/Status/index.tsx | 61 + cluster-ui/src/react-app-env.d.ts | 1 + cluster-ui/src/types/comps.ts | 393 + cluster-ui/src/types/index.ts | 3 + cluster-ui/src/types/machine.ts | 62 + cluster-ui/src/types/misc.ts | 34 + cluster-ui/tsconfig.json | 27 + cluster-ui/tsconfig.paths.json | 12 + cluster-ui/yarn.lock | 11688 ++++++++++++++++ 52 files changed, 15036 insertions(+) create mode 100644 cluster-ui/.gitignore create mode 100644 cluster-ui/CHANGELOG.md create mode 100644 cluster-ui/README.md create mode 100644 cluster-ui/config-overrides.js create mode 100644 cluster-ui/package.json create mode 100644 cluster-ui/public/favicon.ico create mode 100644 cluster-ui/public/index.html create mode 100644 cluster-ui/public/logo192.png create mode 100644 cluster-ui/public/logo512.png create mode 100644 cluster-ui/public/manifest.json create mode 100644 cluster-ui/public/robots.txt create mode 100644 cluster-ui/src/App.less create mode 100644 cluster-ui/src/App.tsx create mode 100644 cluster-ui/src/apis/api.ts create mode 100644 cluster-ui/src/apis/index.ts create mode 100644 cluster-ui/src/apis/request.ts create mode 100644 cluster-ui/src/components/Root.tsx create mode 100644 cluster-ui/src/components/index.ts create mode 100644 cluster-ui/src/hooks/index.ts create mode 100644 cluster-ui/src/hooks/useComps.ts create mode 100644 cluster-ui/src/hooks/useGlobalDir.ts create mode 100644 cluster-ui/src/hooks/useGlobalLoginOptions.ts create mode 100644 cluster-ui/src/hooks/useMachines.ts create mode 100644 cluster-ui/src/hooks/useQueryParams.ts create mode 100644 cluster-ui/src/index.css create mode 100644 cluster-ui/src/index.tsx create mode 100644 cluster-ui/src/pages/Clusters/ClusterDetail.tsx create mode 100644 cluster-ui/src/pages/Clusters/ClusterScaleOut.tsx create mode 100644 cluster-ui/src/pages/Clusters/DashboardPortal.tsx create mode 100644 cluster-ui/src/pages/Clusters/index.tsx create mode 100644 cluster-ui/src/pages/Deployment/CompsManager.tsx create mode 100644 cluster-ui/src/pages/Deployment/DeploymentTable.tsx create mode 100644 cluster-ui/src/pages/Deployment/EditCompForm.tsx create mode 100644 cluster-ui/src/pages/Deployment/GlobalDirForm.tsx create mode 100644 cluster-ui/src/pages/Deployment/TopoPreview.tsx create mode 100644 cluster-ui/src/pages/Deployment/index.tsx create mode 100644 cluster-ui/src/pages/Home/index.tsx create mode 100644 cluster-ui/src/pages/Machines/GlobalLoginOptionsForm.tsx create mode 100644 cluster-ui/src/pages/Machines/MachineForm.tsx create mode 100644 cluster-ui/src/pages/Machines/MachinesTable.tsx create mode 100644 cluster-ui/src/pages/Machines/index.tsx create mode 100644 cluster-ui/src/pages/Setting/index.tsx create mode 100644 cluster-ui/src/pages/Status/OperationStatus.tsx create mode 100644 cluster-ui/src/pages/Status/index.tsx create mode 100644 cluster-ui/src/react-app-env.d.ts create mode 100644 cluster-ui/src/types/comps.ts create mode 100644 cluster-ui/src/types/index.ts create mode 100644 cluster-ui/src/types/machine.ts create mode 100644 cluster-ui/src/types/misc.ts create mode 100644 cluster-ui/tsconfig.json create mode 100644 cluster-ui/tsconfig.paths.json create mode 100644 cluster-ui/yarn.lock diff --git a/cluster-ui/.gitignore b/cluster-ui/.gitignore new file mode 100644 index 0000000000..4d29575de8 --- /dev/null +++ b/cluster-ui/.gitignore @@ -0,0 +1,23 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# production +/build + +# misc +.DS_Store +.env.local +.env.development.local +.env.test.local +.env.production.local + +npm-debug.log* +yarn-debug.log* +yarn-error.log* diff --git a/cluster-ui/CHANGELOG.md b/cluster-ui/CHANGELOG.md new file mode 100644 index 0000000000..aec80bae7f --- /dev/null +++ b/cluster-ui/CHANGELOG.md @@ -0,0 +1,41 @@ +# TiUP Cluster UI Changelog + +## 2020.11.13 + +- Add audit for deploy/destroy/start/stop/scale_in/scale_out operations + +## 2020.11.04 + +- Support config arch for machine + +## 2020.11.03 + +- Use default "root" as the machine login user name if leave it empty +- Set default labels values if leave them empty +- Add confirmation prompt when starting to deploy + +## 2020.10.26 + +- Enable manually edit the topo yaml configuration +- Support config the numa_node option for TiDB/TiKV/PD/TiFlash + +## 2020.10.21 + +- Skip check TiKV location labels when deploying or scaling out to enable deploy multiple TiKV instances in a same host + +## 2020.10.16 + +- Add data management and db users management features by embeding TiDB dashboard +- Add full TiDB dashboard features entry + +## 2020.10.15 + +- Support to modify mirror address + +## 2020.10.14 + +- Support to modify cluster configuration by embeding TiDB dashboard + +## 2020.09.16 + +- Enable select TiDB v4.0.6 to deploy, support type any TiDB version manually diff --git a/cluster-ui/README.md b/cluster-ui/README.md new file mode 100644 index 0000000000..f1dd478f83 --- /dev/null +++ b/cluster-ui/README.md @@ -0,0 +1,29 @@ +# Cluster Web UI + +This is the web ui for tiup-cluster command. + +## How to Run + +### Release Mode + +```shell +$ cd tiup +$ make embed_cluster_ui +$ UI=1 make +$ bin/tiup-cluster --ui +``` + +Then access `http://127.0.0.1:8080` in the browser. + +### Develop Mode + +```shell +$ cd tiup +$ make +$ bin/tiup-cluster --ui +# a new tab +$ cd cluster-ui +$ yarn && yarn start +``` + +It will auto open `http://127.0.0.1:3000/tiup` in the browser. diff --git a/cluster-ui/config-overrides.js b/cluster-ui/config-overrides.js new file mode 100644 index 0000000000..97b1433cdf --- /dev/null +++ b/cluster-ui/config-overrides.js @@ -0,0 +1,36 @@ +const { override, addLessLoader } = require('customize-cra') +const { alias, configPaths } = require('react-app-rewire-alias') + +const addAlias = () => (config) => { + alias({ + ...configPaths('tsconfig.paths.json'), + })(config) + return config +} + +const configEslint = () => (config) => { + const eslintRule = config.module.rules.filter( + (r) => + r.use && r.use.some((u) => u.options && u.options.useEslintrc !== void 0) + )[0] + const options = eslintRule.use[0].options + // options.ignore = true + // options.ignorePattern = 'lib/client/api/*.ts' + + // To close "The href attribute is required for an anchor to be keyboard accessible" warning + options.baseConfig.rules = { + 'jsx-a11y/anchor-is-valid': 'off', + } + return config +} + +module.exports = override( + addLessLoader({ + lessOptions: { + javascriptEnabled: true, + modifyVars: { '@primary-color': '#3351ff' }, + }, + }), + addAlias(), + configEslint() +) diff --git a/cluster-ui/package.json b/cluster-ui/package.json new file mode 100644 index 0000000000..e1f2a22f86 --- /dev/null +++ b/cluster-ui/package.json @@ -0,0 +1,53 @@ +{ + "name": "tiup-ui", + "version": "0.1.0", + "private": true, + "homepage": "/tiup", + "dependencies": { + "@ant-design/icons": "^4.2.2", + "@testing-library/jest-dom": "^4.2.4", + "@testing-library/react": "^9.3.2", + "@testing-library/user-event": "^7.1.2", + "@types/jest": "^24.0.0", + "@types/node": "^12.0.0", + "@types/react": "^16.9.0", + "@types/react-dom": "^16.9.0", + "@types/uniqid": "^5.2.0", + "ahooks": "^2.5.0", + "antd": "^4.5.3", + "history": "^5.0.0", + "react": "^16.13.1", + "react-dom": "^16.13.1", + "react-router-dom": "^6.0.0-beta.0", + "react-scripts": "3.4.2", + "typescript": "4.0.2", + "uniqid": "^5.2.0", + "yaml": "^1.10.0" + }, + "scripts": { + "start": "react-app-rewired start", + "build": "react-app-rewired build", + "test": "react-app-rewired test" + }, + "eslintConfig": { + "extends": "react-app" + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + }, + "devDependencies": { + "customize-cra": "^1.0.0", + "less-loader": "^6.2.0", + "react-app-rewire-alias": "^0.1.6", + "react-app-rewired": "^2.1.6" + } +} diff --git a/cluster-ui/public/favicon.ico b/cluster-ui/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..bcd5dfd67cd0361b78123e95c2dd96031f27f743 GIT binary patch literal 3150 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 MDeX2GIKR{Qf5okjU;qFB literal 0 HcmV?d00001 diff --git a/cluster-ui/public/index.html b/cluster-ui/public/index.html new file mode 100644 index 0000000000..d695588d73 --- /dev/null +++ b/cluster-ui/public/index.html @@ -0,0 +1,98 @@ + + + + + + + + TiUP + + + + + +
+
+
+ + diff --git a/cluster-ui/public/logo192.png b/cluster-ui/public/logo192.png new file mode 100644 index 0000000000000000000000000000000000000000..fc44b0a3796c0e0a64c3d858ca038bd4570465d9 GIT binary patch literal 5347 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 b{|{}U^kRn2wo}j7|0ATu<;8xA7zX}7|B6mN literal 0 HcmV?d00001 diff --git a/cluster-ui/public/manifest.json b/cluster-ui/public/manifest.json new file mode 100644 index 0000000000..080d6c77ac --- /dev/null +++ b/cluster-ui/public/manifest.json @@ -0,0 +1,25 @@ +{ + "short_name": "React App", + "name": "Create React App Sample", + "icons": [ + { + "src": "favicon.ico", + "sizes": "64x64 32x32 24x24 16x16", + "type": "image/x-icon" + }, + { + "src": "logo192.png", + "type": "image/png", + "sizes": "192x192" + }, + { + "src": "logo512.png", + "type": "image/png", + "sizes": "512x512" + } + ], + "start_url": ".", + "display": "standalone", + "theme_color": "#000000", + "background_color": "#ffffff" +} diff --git a/cluster-ui/public/robots.txt b/cluster-ui/public/robots.txt new file mode 100644 index 0000000000..e9e57dc4d4 --- /dev/null +++ b/cluster-ui/public/robots.txt @@ -0,0 +1,3 @@ +# https://www.robotstxt.org/robotstxt.html +User-agent: * +Disallow: diff --git a/cluster-ui/src/App.less b/cluster-ui/src/App.less new file mode 100644 index 0000000000..ce63c10da7 --- /dev/null +++ b/cluster-ui/src/App.less @@ -0,0 +1 @@ +@import '~antd/dist/antd.less'; diff --git a/cluster-ui/src/App.tsx b/cluster-ui/src/App.tsx new file mode 100644 index 0000000000..44f2d952e0 --- /dev/null +++ b/cluster-ui/src/App.tsx @@ -0,0 +1,41 @@ +import React from 'react' +import { HashRouter as Router, Routes, Route, Navigate } from 'react-router-dom' + +import StatusPage from '_pages/Status' +import HomePage from '_pages/Home' +import MachinesPage from '_pages/Machines' +import DeploymentPage from '_pages/Deployment' +import ClustersPage from '_pages/Clusters' +import ClusterDetailPage from '_pages/Clusters/ClusterDetail' +import ClusterScaleOutPage from '_pages/Clusters/ClusterScaleOut' +import DashboardPortalPage from '_pages/Clusters/DashboardPortal' +import SettingPage from '_pages/Setting' + +import './App.less' + +function App() { + return ( + + + } /> + + }> + } /> + }> + } /> + } + /> + } /> + + } /> + } /> + } /> + + + + ) +} + +export default App diff --git a/cluster-ui/src/apis/api.ts b/cluster-ui/src/apis/api.ts new file mode 100644 index 0000000000..380dc418e2 --- /dev/null +++ b/cluster-ui/src/apis/api.ts @@ -0,0 +1,67 @@ +import request from './request' + +const API_URL = + process.env.NODE_ENV === 'production' ? '/api' : 'http://127.0.0.1:8080/api' + +function fullUrl(path: string): string { + return `${API_URL}/${path}` +} + +//////////////////// + +export function deployCluster(deployment: any) { + return request(fullUrl('deploy'), 'POST', deployment) +} + +export function getStatus() { + return request(fullUrl('status')) +} + +export function getClusterList() { + return request(fullUrl('clusters')) +} + +export function deleteCluster(clusterName: string) { + return request(fullUrl(`clusters/${clusterName}`), 'DELETE') +} + +export function getClusterTopo(clusterName: string) { + return request(fullUrl(`clusters/${clusterName}`)) +} + +export function startCluster(clusterName: string) { + return request(fullUrl(`clusters/${clusterName}/start`), 'POST') +} + +export function stopCluster(clusterName: string) { + return request(fullUrl(`clusters/${clusterName}/stop`), 'POST') +} + +export function scaleInCluster( + clusterName: string, + scaleInOpts: { nodes: string[]; force: boolean } +) { + return request( + fullUrl(`clusters/${clusterName}/scale_in`), + 'POST', + scaleInOpts + ) +} + +export function scaleOutCluster(clusterName: string, scaleOutOpts: any) { + return request( + fullUrl(`clusters/${clusterName}/scale_out`), + 'POST', + scaleOutOpts + ) +} + +export function getMirrorAddress() { + return request(fullUrl(`mirror`)) +} + +export function setMirrorAddress(newAddress: string) { + return request(fullUrl(`mirror`), 'POST', { + mirror_address: newAddress, + }) +} diff --git a/cluster-ui/src/apis/index.ts b/cluster-ui/src/apis/index.ts new file mode 100644 index 0000000000..2268b2b95c --- /dev/null +++ b/cluster-ui/src/apis/index.ts @@ -0,0 +1,3 @@ +export * from './request' +export { default as request } from './request' +export * from './api' diff --git a/cluster-ui/src/apis/request.ts b/cluster-ui/src/apis/request.ts new file mode 100644 index 0000000000..313a4a7b0c --- /dev/null +++ b/cluster-ui/src/apis/request.ts @@ -0,0 +1,51 @@ +export type ResError = Error & { + response?: any +} + +export default function request( + url: string, + method?: 'GET' | 'POST' | 'PUT' | 'DELETE', + body?: object, + options?: RequestInit +) { + const opts: RequestInit = { + ...options, + method: method || 'GET', + headers: { + ...options?.headers, + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + } + if (body) { + opts.body = JSON.stringify(body) + } + return doFetch(url, opts) +} + +function doFetch(url: string, options: RequestInit) { + return fetch(url, options) + .then(parseResponse) + .then((data) => ({ data, err: undefined })) + .catch((err) => ({ data: undefined, err })) +} + +function parseResponse(response: Response) { + if (response.status === 204) { + return {} as any + } else if (response.status >= 200 && response.status < 300) { + return response.json() + } else { + let errMsg = response.statusText + return response + .json() + .then((resData: any) => { + errMsg = resData.msg || resData.message || response.statusText + }) + .finally(() => { + const error: ResError = new Error(errMsg) + error.response = response + throw error + }) + } +} diff --git a/cluster-ui/src/components/Root.tsx b/cluster-ui/src/components/Root.tsx new file mode 100644 index 0000000000..dd63dabde5 --- /dev/null +++ b/cluster-ui/src/components/Root.tsx @@ -0,0 +1,5 @@ +import React, { FC } from 'react' + +const Root: FC = ({ children }) =>
{children}
+ +export default Root diff --git a/cluster-ui/src/components/index.ts b/cluster-ui/src/components/index.ts new file mode 100644 index 0000000000..098334a73c --- /dev/null +++ b/cluster-ui/src/components/index.ts @@ -0,0 +1 @@ +export { default as Root } from './Root' diff --git a/cluster-ui/src/hooks/index.ts b/cluster-ui/src/hooks/index.ts new file mode 100644 index 0000000000..5e54f24080 --- /dev/null +++ b/cluster-ui/src/hooks/index.ts @@ -0,0 +1,5 @@ +export * from './useMachines' +export * from './useComps' +export * from './useGlobalLoginOptions' +export * from './useGlobalDir' +export * from './useQueryParams' diff --git a/cluster-ui/src/hooks/useComps.ts b/cluster-ui/src/hooks/useComps.ts new file mode 100644 index 0000000000..bb0fba34c0 --- /dev/null +++ b/cluster-ui/src/hooks/useComps.ts @@ -0,0 +1,21 @@ +import { useLocalStorageState } from 'ahooks' +import { useMemo } from 'react' + +import { BaseComp, CompMap } from '_types' + +export function useComps() { + const [compObjs, setCompObjs] = useLocalStorageState( + 'components', + {} + ) + + const comps = useMemo(() => { + let _comps: CompMap = {} + Object.keys(compObjs).forEach((k) => { + _comps[k] = BaseComp.deSerial(compObjs[k]) + }) + return _comps + }, [compObjs]) + + return { comps, setCompObjs } +} diff --git a/cluster-ui/src/hooks/useGlobalDir.ts b/cluster-ui/src/hooks/useGlobalDir.ts new file mode 100644 index 0000000000..b283085ffd --- /dev/null +++ b/cluster-ui/src/hooks/useGlobalDir.ts @@ -0,0 +1,20 @@ +import { useLocalStorageState } from 'ahooks' + +import { GlobalDir, IGlobalDir } from '_types' +import { useMemo } from 'react' + +export function useGlobalDir() { + const [globalDirObj, setGlobalDirObj] = useLocalStorageState( + 'global_dir', + {} + ) + + const globalDir = useMemo(() => { + return GlobalDir.deSerial(globalDirObj) + }, [globalDirObj]) + + return { + globalDir, + setGlobalDirObj, + } +} diff --git a/cluster-ui/src/hooks/useGlobalLoginOptions.ts b/cluster-ui/src/hooks/useGlobalLoginOptions.ts new file mode 100644 index 0000000000..751792ff12 --- /dev/null +++ b/cluster-ui/src/hooks/useGlobalLoginOptions.ts @@ -0,0 +1,14 @@ +import { useLocalStorageState } from 'ahooks' + +import { IGlobalLoginOptions } from '_types' + +export function useGlobalLoginOptions() { + const [globalLoginOptions, setGlobalLoginOptions] = useLocalStorageState< + IGlobalLoginOptions + >('global_login_options', {}) + + return { + globalLoginOptions, + setGlobalLoginOptions, + } +} diff --git a/cluster-ui/src/hooks/useMachines.ts b/cluster-ui/src/hooks/useMachines.ts new file mode 100644 index 0000000000..fa40f6c6f4 --- /dev/null +++ b/cluster-ui/src/hooks/useMachines.ts @@ -0,0 +1,21 @@ +import { useLocalStorageState } from 'ahooks' +import { useMemo } from 'react' + +import { MachineMap, Machine } from '_types' + +export function useMachines() { + const [machineObjs, setMachineObjs] = useLocalStorageState( + 'machines', + {} + ) + + const machines = useMemo(() => { + let _machines: MachineMap = {} + Object.keys(machineObjs).forEach((k) => { + _machines[k] = Machine.deSerial(machineObjs[k]) + }) + return _machines + }, [machineObjs]) + + return { machines, setMachineObjs } +} diff --git a/cluster-ui/src/hooks/useQueryParams.ts b/cluster-ui/src/hooks/useQueryParams.ts new file mode 100644 index 0000000000..81a32b9015 --- /dev/null +++ b/cluster-ui/src/hooks/useQueryParams.ts @@ -0,0 +1,17 @@ +import { useMemo } from 'react' +import { useLocation } from 'react-router' + +export function useQueryParams() { + const { search } = useLocation() + + const params = useMemo(() => { + const searchParams = new URLSearchParams(search) + let _params: { [k: string]: any } = {} + for (const [k, v] of searchParams) { + _params[k] = v + } + return _params + }, [search]) + + return params +} diff --git a/cluster-ui/src/index.css b/cluster-ui/src/index.css new file mode 100644 index 0000000000..ec2585e8c0 --- /dev/null +++ b/cluster-ui/src/index.css @@ -0,0 +1,13 @@ +body { + margin: 0; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', + 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', + sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +code { + font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', + monospace; +} diff --git a/cluster-ui/src/index.tsx b/cluster-ui/src/index.tsx new file mode 100644 index 0000000000..9cc87711a7 --- /dev/null +++ b/cluster-ui/src/index.tsx @@ -0,0 +1,12 @@ +import React from 'react' +import ReactDOM from 'react-dom' + +import App from './App' +import './index.css' + +ReactDOM.render( + + + , + document.getElementById('root') +) diff --git a/cluster-ui/src/pages/Clusters/ClusterDetail.tsx b/cluster-ui/src/pages/Clusters/ClusterDetail.tsx new file mode 100644 index 0000000000..a4b3cffd47 --- /dev/null +++ b/cluster-ui/src/pages/Clusters/ClusterDetail.tsx @@ -0,0 +1,275 @@ +import React, { useMemo, useState, useCallback, useEffect, useRef } from 'react' +import { useParams, useNavigate } from 'react-router-dom' +import { Space, Button, Modal, Table, Popconfirm, Divider } from 'antd' +import { ExclamationCircleOutlined } from '@ant-design/icons' +import { useSessionStorageState, useLocalStorageState } from 'ahooks' + +import { + deleteCluster, + getClusterTopo, + startCluster, + stopCluster, + scaleInCluster, +} from '_apis' +import { Root } from '_components' +import { useComps } from '_hooks' +import { ICluster, IClusterInstInfo } from '_types' + +function ClusterDetailPage() { + const refIframe = useRef(null) + + const navigate = useNavigate() + const [clustersList] = useSessionStorageState('clusters', []) + const { clusterName } = useParams() + const cluster = useMemo( + () => clustersList.find((el) => el.name === clusterName), + [clustersList, clusterName] + ) + + const [clusterInstInfos, setClusterInstInfos] = useSessionStorageState< + IClusterInstInfo[] + >(`${clusterName}_cluster_topo`, []) + + const dashboardPD = useMemo(() => { + return clusterInstInfos.find( + (el) => + el.role === 'pd' && + el.status.indexOf('UI') !== -1 && + el.status.indexOf('Up') !== -1 + ) + }, [clusterInstInfos]) + + const [curScaleOutNodes] = useLocalStorageState<{ + cluster_name: string + scale_out_nodes: any[] + }>('cur_scale_out_nodes', { cluster_name: '', scale_out_nodes: [] }) + const { comps, setCompObjs } = useComps() + + const [loadingTopo, setLoadingTopo] = useState(false) + + const handleScaleInCluster = useCallback( + (node: IClusterInstInfo) => { + const lowerStatus = node.status.toLowerCase() + const force = + lowerStatus.indexOf('down') !== -1 || + lowerStatus.indexOf('inactive') !== -1 + + scaleInCluster(clusterName, { + nodes: [node.id], + force, + }) + navigate('/status') + }, + [navigate, clusterName] + ) + + const columns = useMemo(() => { + const _columns = [ + 'ID', + 'Role', + 'Host', + 'Ports', + 'OS_Arch', + 'Status', + 'Data_Dir', + 'Deploy_Dir', + ].map((title) => ({ + title, + key: title.toLowerCase(), + dataIndex: title.toLowerCase(), + fixed: title === 'ID', + })) + _columns.push({ + title: '操作', + key: 'action', + width: 100, + fixed: 'right', + render: (text: any, rec: IClusterInstInfo) => { + if (rec.status.toLowerCase().indexOf('offline') !== -1) { + return null + } + return ( + handleScaleInCluster(rec)} + okText="下线" + cancelText="取消" + > + 缩容 + + ) + }, + } as any) + return _columns + }, [handleScaleInCluster]) + + useEffect(() => { + setLoadingTopo(true) + getClusterTopo(clusterName).then(({ data, err }) => { + setLoadingTopo(false) + if (data !== undefined) { + setClusterInstInfos(data) + updateLocalTopo(data) + } + }) + // eslint-disable-next-line + }, []) + + const [dashboardToken] = useSessionStorageState( + `${clusterName}_dashboard_token`, + '' + ) + + function updateLocalTopo(clusters: IClusterInstInfo[]) { + if ( + curScaleOutNodes.cluster_name !== clusterName || + curScaleOutNodes.scale_out_nodes.length === 0 + ) { + return + } + let newComps = { ...comps } + for (const n of curScaleOutNodes.scale_out_nodes) { + const exist = clusters.find((el) => el.id === n.node) + if (exist && newComps[n.id]) { + newComps[n.id].for_scale_out = false + } + } + setCompObjs(newComps) + } + + function destroyCluster() { + deleteCluster(clusterName) + navigate('/status') + } + + function handleDestroyCluster() { + Modal.confirm({ + title: `销毁 ${cluster?.name} 集群`, + icon: , + content: '你确定要销毁这个集群吗?所有数据都会清除,操作不可回滚!', + okText: '销毁', + cancelText: '取消', + okButtonProps: { danger: true }, + onOk: () => destroyCluster(), + }) + } + + function handleStartCluster() { + startCluster(clusterName) + navigate('/status') + } + + function handleStopCluster() { + stopCluster(clusterName) + navigate('/status') + } + + function handleScaleOutCluster() { + navigate(`/clusters/${clusterName}/scaleout`) + } + + function handleOpenDashboard(targetFeature: string) { + if (dashboardPD === undefined) { + Modal.error({ + title: '没有找到相应的 PD 节点', + content: + '请检查相应的 PD 节点是否工作正常,如果集群未启动,请先启动集群。', + }) + return + } + if (targetFeature === 'full') { + refIframe.current?.contentWindow?.postMessage( + { + token: dashboardToken, + lang: 'zh', + hideNav: false, + redirectPath: '/diagnose', + }, + '*' + ) + window.open(`http://${dashboardPD.id}/dashboard/`, '_blank') + return + } + + navigate( + `/clusters/${clusterName}/dashboard?pd=${dashboardPD.id}&tidb_version=${cluster?.version}&target=${targetFeature}` + ) + } + + return ( + +
+ + + + + + + + + + + + + + + +
+ + {cluster && ( +
+ {/*

Name: {cluster.name}

+

User: {cluster.user}

*/} +

Version: {cluster.version}

+ {/*

Path: {cluster.path}

+

PrivateKey: {cluster.private_key}

*/} +
+ )} + + + +