feat: 初始化项目并完成基础功能开发
- 完成项目初始化
- 实现用户注册、登录功能
- 完成用户管理与权限管理模块
- 开发后端 Tracker 服务器项目管理接口
- 实现日志管理接口
Change-Id: Ia4bde1c9ff600352a7ff0caca0cc50b02cad1af7
diff --git a/react-ui/src/pages/Monitor/Cache/List.tsx b/react-ui/src/pages/Monitor/Cache/List.tsx
new file mode 100644
index 0000000..af6147a
--- /dev/null
+++ b/react-ui/src/pages/Monitor/Cache/List.tsx
@@ -0,0 +1,240 @@
+import React, { useEffect, useState, useRef } from 'react';
+import { clearCacheAll, clearCacheKey, clearCacheName, getCacheValue, listCacheKey, listCacheName } from '@/services/monitor/cachelist';
+import { Button, Card, Col, Form, FormInstance, Input, message, Row, Table } from 'antd';
+import styles from './index.less';
+import { FormattedMessage } from '@umijs/max';
+import { ReloadOutlined } from '@ant-design/icons';
+import { ProForm } from '@ant-design/pro-components';
+
+const { TextArea } = Input;
+
+
+/* *
+ *
+ * @author whiteshader@163.com
+ * @datetime 2022/06/27
+ *
+ * */
+
+
+
+const CacheList: React.FC = () => {
+ const [cacheNames, setCacheNames] = useState<any>([]);
+ const [currentCacheName, setCurrentCacheName] = useState<any>([]);
+ const [cacheKeys, setCacheKeys] = useState<any>([]);
+ const [form] = Form.useForm();
+
+ const getCacheNames = () => {
+ listCacheName().then((res) => {
+ if (res.code === 200) {
+ setCacheNames(res.data);
+ }
+ });
+ }
+
+ useEffect(() => {
+ getCacheNames();
+ }, []);
+
+ const getCacheKeys = (cacheName: string) => {
+ listCacheKey(cacheName).then(res => {
+ if (res.code === 200) {
+ let index = 0;
+ const keysData = res.data.map((item: any) => {
+ return {
+ index: index++,
+ cacheKey: item
+ }
+ })
+ setCacheKeys(keysData);
+ }
+ });
+ };
+
+ const onClearAll = async () => {
+ clearCacheAll().then(res => {
+ if(res.code === 200) {
+ message.success("清理全部缓存成功");
+ }
+ });
+ };
+
+ const onClearAllFailed = (errorInfo: any) => {
+ message.error('Failed:', errorInfo);
+ };
+
+ const refreshCacheNames = () => {
+ getCacheNames();
+ message.success("刷新缓存列表成功");
+ };
+
+ const refreshCacheKeys = () => {
+ getCacheKeys(currentCacheName);
+ message.success("刷新键名列表成功");
+ };
+
+ const columns = [
+ {
+ title: '缓存名称',
+ dataIndex: 'cacheName',
+ key: 'cacheName',
+ render: (_: any, record: any) => {
+ return record.cacheName.replace(":", "");
+ }
+ },
+ {
+ title: '备注',
+ dataIndex: 'remark',
+ key: 'remark',
+ },
+ {
+ title: <FormattedMessage id="pages.searchTable.titleOption" defaultMessage="操作" />,
+ dataIndex: 'option',
+ width: '40px',
+ valueType: 'option',
+ render: (_: any, record: API.Monitor.CacheContent) => [
+ <Button
+ type="link"
+ size="small"
+ key="remove"
+ danger
+ onClick={() => {
+ clearCacheName(record.cacheName).then(res => {
+ if(res.code === 200) {
+ message.success("清理缓存名称[" + record.cacheName + "]成功");
+ getCacheKeys(currentCacheName);
+ }
+ });
+ }}
+ >
+ <FormattedMessage id="pages.searchTable.delete" defaultMessage="删除" />
+ </Button>,
+ ]
+ }
+ ];
+
+ const cacheKeysColumns = [
+ {
+ title: '序号',
+ dataIndex: 'index',
+ key: 'index'
+ },
+ {
+ title: '缓存键名',
+ dataIndex: 'cacheKey',
+ key: 'cacheKey',
+ render: (_: any, record: any) => {
+ return record.cacheKey.replace(currentCacheName, "");
+ }
+ },
+ {
+ title: <FormattedMessage id="pages.searchTable.titleOption" defaultMessage="操作" />,
+ dataIndex: 'option',
+ width: '40px',
+ valueType: 'option',
+ render: (_: any, record: API.Monitor.CacheContent) => [
+ <Button
+ type="link"
+ size="small"
+ key="remove"
+ danger
+ onClick={() => {
+ console.log(record)
+ clearCacheKey(record.cacheKey).then(res => {
+ if(res.code === 200) {
+ message.success("清理缓存键名[" + record.cacheKey + "]成功");
+ getCacheKeys(currentCacheName);
+ }
+ });
+ }}
+ >
+ <FormattedMessage id="pages.searchTable.delete" defaultMessage="删除" />
+ </Button>,
+ ]
+ }
+ ];
+
+ return (
+ <div>
+ <Row gutter={[24, 24]}>
+ <Col span={8}>
+ <Card title="缓存列表" extra={<Button icon={<ReloadOutlined />} onClick={()=>{ refreshCacheNames()}} type="link" />} className={styles.card}>
+ <Table
+ rowKey="cacheName"
+ dataSource={cacheNames}
+ columns={columns}
+ onRow={(record: API.Monitor.CacheContent) => {
+ return {
+ onClick: () => {
+ setCurrentCacheName(record.cacheName);
+ getCacheKeys(record.cacheName);
+ },
+ };
+ }}
+ />
+ </Card>
+ </Col>
+ <Col span={8}>
+ <Card title="键名列表" extra={<Button icon={<ReloadOutlined />} onClick={()=>{ refreshCacheKeys()}} type="link" />} className={styles.card}>
+ <Table
+ rowKey="index"
+ dataSource={cacheKeys}
+ columns={cacheKeysColumns}
+ onRow={(record: any) => {
+ return {
+ onClick: () => {
+ getCacheValue(currentCacheName, record.cacheKey).then(res => {
+ if (res.code === 200) {
+ form.resetFields();
+ form.setFieldsValue({
+ cacheName: res.data.cacheName,
+ cacheKey: res.data.cacheKey,
+ cacheValue: res.data.cacheValue,
+ remark: res.data.remark,
+ });
+ }
+ });
+ },
+ };
+ }}
+ />
+ </Card>
+ </Col>
+ <Col span={8}>
+ <Card title="缓存内容" extra={<Button icon={<ReloadOutlined />} onClick={()=>{ onClearAll()}} type="link" >清理全部</Button>} className={styles.card}>
+ <ProForm
+ name="basic"
+ form={form}
+ labelCol={{ span: 8 }}
+ wrapperCol={{ span: 16 }}
+ onFinish={onClearAll}
+ onFinishFailed={onClearAllFailed}
+ autoComplete="off"
+ >
+ <Form.Item
+ label="缓存名称"
+ name="cacheName"
+ >
+ <Input />
+ </Form.Item>
+ <Form.Item
+ label="缓存键名"
+ name="cacheKey"
+ >
+ <Input />
+ </Form.Item>
+ <Form.Item
+ label="缓存内容"
+ name="cacheValue"
+ >
+ <TextArea autoSize={{ minRows: 2 }} />
+ </Form.Item>
+ </ProForm>
+ </Card>
+ </Col>
+ </Row>
+ </div>
+ );
+};
+
+export default CacheList;
diff --git a/react-ui/src/pages/Monitor/Cache/List/index.less b/react-ui/src/pages/Monitor/Cache/List/index.less
new file mode 100644
index 0000000..0c5aace
--- /dev/null
+++ b/react-ui/src/pages/Monitor/Cache/List/index.less
@@ -0,0 +1,10 @@
+
+
+
+/* *
+ *
+ * @author whiteshader@163.com
+ * @datetime 2021/09/16
+ *
+ * */
+
diff --git a/react-ui/src/pages/Monitor/Cache/index.less b/react-ui/src/pages/Monitor/Cache/index.less
new file mode 100644
index 0000000..7112c50
--- /dev/null
+++ b/react-ui/src/pages/Monitor/Cache/index.less
@@ -0,0 +1,33 @@
+
+/* *
+ *
+ * @author whiteshader@163.com
+ * @datetime 2021/09/16
+ *
+ * */
+
+
+.card {
+ margin-bottom: 12px;
+}
+
+
+.miniChart {
+ position: relative;
+ width: 100%;
+ .chartContent {
+ position: absolute;
+ bottom: -28px;
+ width: 100%;
+ > div {
+ margin: 0 -5px;
+ overflow: hidden;
+ }
+ }
+ .chartLoading {
+ position: absolute;
+ top: 16px;
+ left: 50%;
+ margin-left: -7px;
+ }
+}
diff --git a/react-ui/src/pages/Monitor/Cache/index.tsx b/react-ui/src/pages/Monitor/Cache/index.tsx
new file mode 100644
index 0000000..4c247ee
--- /dev/null
+++ b/react-ui/src/pages/Monitor/Cache/index.tsx
@@ -0,0 +1,201 @@
+import React, { useEffect, useState } from 'react';
+import { Card, Col, Row, Table } from 'antd';
+import { DataItem } from '@antv/g2plot/esm/interface/config';
+import { Gauge, Pie } from '@ant-design/plots';
+import styles from './index.less';
+import { getCacheInfo } from '@/services/monitor/cache';
+
+
+/* *
+ *
+ * @author whiteshader@163.com
+ * @datetime 2021/09/16
+ *
+ * */
+
+
+const columns = [
+ {
+ title: 'col1',
+ dataIndex: 'col1',
+ key: 'col1',
+ },
+ {
+ title: 'col2',
+ dataIndex: 'col2',
+ key: 'col2',
+ },
+ {
+ title: 'col3',
+ dataIndex: 'col3',
+ key: 'col3',
+ },
+ {
+ title: 'col4',
+ dataIndex: 'col4',
+ key: 'col4',
+ },
+ {
+ title: 'col5',
+ dataIndex: 'col5',
+ key: 'col5',
+ },
+ {
+ title: 'col6',
+ dataIndex: 'col6',
+ key: 'col6',
+ },
+ {
+ title: 'col7',
+ dataIndex: 'col7',
+ key: 'col7',
+ },
+ {
+ title: 'col8',
+ dataIndex: 'col8',
+ key: 'col8',
+ },
+];
+
+const usageFormatter = (val: string): string => {
+ switch (val) {
+ case '10':
+ return '100%';
+ case '8':
+ return '80%';
+ case '6':
+ return '60%';
+ case '4':
+ return '40%';
+ case '2':
+ return '20%';
+ case '0':
+ return '0%';
+ default:
+ return '';
+ }
+};
+
+const CacheInfo: React.FC = () => {
+ const [baseInfoData, setBaseInfoData] = useState<any>([]);
+ const [memUsage, setMemUsage] = useState<Number>(0);
+ const [memUsageTitle, setMemUsageTitle] = useState<any>([]);
+ const [cmdInfoData, setCmdInfoData] = useState<DataItem[]>([]);
+ useEffect(() => {
+ getCacheInfo().then((res) => {
+ if (res.code === 200) {
+ const baseinfo = [];
+ baseinfo.push({
+ col1: 'Redis版本',
+ col2: res.data.info.redis_version,
+ col3: '运行模式',
+ col4: res.data.info.redis_mode === 'standalone' ? '单机' : '集群',
+ col5: '端口',
+ col6: res.data.info.tcp_port,
+ col7: '客户端数',
+ col8: res.data.info.connected_clients,
+ });
+ baseinfo.push({
+ col1: '运行时间(天)',
+ col2: res.data.info.uptime_in_days,
+ col3: '使用内存',
+ col4: res.data.info.used_memory_human,
+ col5: '使用CPU',
+ col6: `${res.data.info.used_cpu_user_children}%`,
+ col7: '内存配置',
+ col8: res.data.info.maxmemory_human,
+ });
+ baseinfo.push({
+ col1: 'AOF是否开启',
+ col2: res.data.info.aof_enabled === '0' ? '否' : '是',
+ col3: 'RDB是否成功',
+ col4: res.data.info.rdb_last_bgsave_status,
+ col5: 'Key数量',
+ col6: res.data.dbSize,
+ col7: '网络入口/出口',
+ col8: `${res.data.info.instantaneous_input_kbps}/${res.data.info.instantaneous_output_kbps}kps`,
+ });
+ setBaseInfoData(baseinfo);
+
+ const data: VisitDataType[] = res.data.commandStats.map((item) => {
+ return {
+ x: item.name,
+ y: Number(item.value),
+ };
+ });
+
+ setCmdInfoData(data);
+ setMemUsageTitle(res.data.info.used_memory_human);
+ setMemUsage(res.data.info.used_memory / res.data.info.total_system_memory);
+ }
+ });
+ }, []);
+
+ return (
+ <div>
+ <Row gutter={[24, 24]}>
+ <Col span={24}>
+ <Card title="基本信息" className={styles.card}>
+ <Table
+ rowKey="col1"
+ pagination={false}
+ showHeader={false}
+ dataSource={baseInfoData}
+ columns={columns}
+ />
+ </Card>
+ </Col>
+ </Row>
+ <Row gutter={[24, 24]}>
+ <Col span={12}>
+ <Card title="命令统计" className={styles.card}>
+ <Pie
+ height={320}
+ radius={0.8}
+ innerRadius={0.5}
+ angleField="y"
+ colorField="x"
+ data={cmdInfoData as any}
+ legend={false}
+ label={{
+ position: 'spider',
+ text: (item: { x: number; y: number }) => {
+ return `${item.x}: ${item.y}`;
+ },
+ }}
+ />
+ </Card>
+ </Col>
+ <Col span={12}>
+ <Card title="内存信息" className={styles.card}>
+ <Gauge
+ title={memUsageTitle}
+ height={320}
+ percent={memUsage}
+ formatter={usageFormatter}
+ padding={-16}
+ style={{
+ textContent: () => { return memUsage.toFixed(2).toString() + '%'},
+ }}
+ data={
+ {
+ target: memUsage,
+ total: 100,
+ name: 'score',
+ thresholds: [20, 40, 60, 80, 100],
+ } as any
+ }
+ meta={{
+ color: {
+ range: ['#C3F71F', '#B5E61D', '#FFC90E', '#FF7F27', '#FF2518'],
+ },
+ }}
+ />
+ </Card>
+ </Col>
+ </Row>
+ </div>
+ );
+};
+
+export default CacheInfo;