mirror of
https://github.com/didi/KnowStreaming.git
synced 2026-01-05 21:12:13 +08:00
V3.2
This commit is contained in:
@@ -111,5 +111,5 @@ export default () => {
|
||||
});
|
||||
}, [routeParams.clusterId]);
|
||||
|
||||
return <CardBar scene="broker" healthData={healthData} cardColumns={cardData} loading={loading}></CardBar>;
|
||||
return <CardBar scene="brokers" healthData={healthData} cardColumns={cardData} loading={loading}></CardBar>;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import CardBar, { healthDataProps } from './index';
|
||||
import { Tooltip, Utils } from 'knowdesign';
|
||||
import api from '@src/api';
|
||||
import { HealthStateEnum } from '../HealthState';
|
||||
import { InfoCircleOutlined } from '@ant-design/icons';
|
||||
|
||||
interface ConnectState {
|
||||
connectClusterCount: number;
|
||||
workerCount: number;
|
||||
aliveConnectorCount: number;
|
||||
aliveTaskCount: number;
|
||||
healthCheckPassed: number;
|
||||
healthCheckTotal: number;
|
||||
healthState: number;
|
||||
totalConnectorCount: string;
|
||||
totalTaskCount: number;
|
||||
totalServerCount: number;
|
||||
}
|
||||
|
||||
const getVal = (val: string | number | undefined | null) => {
|
||||
return val === undefined || val === null || val === '' ? '0' : val;
|
||||
};
|
||||
|
||||
const ConnectCard = ({ state }: { state?: boolean }) => {
|
||||
const { clusterId } = useParams<{
|
||||
clusterId: string;
|
||||
}>();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [cardData, setCardData] = useState([]);
|
||||
const [healthData, setHealthData] = useState<healthDataProps>({
|
||||
state: HealthStateEnum.UNKNOWN,
|
||||
passed: 0,
|
||||
total: 0,
|
||||
});
|
||||
|
||||
const getHealthData = () => {
|
||||
return Utils.post(api.getMetricPointsLatest(Number(clusterId)), [
|
||||
'HealthCheckPassed_Connector',
|
||||
'HealthCheckTotal_Connector',
|
||||
'HealthState_Connector',
|
||||
]).then((data: any) => {
|
||||
setHealthData({
|
||||
state: data?.metrics?.['HealthState_Connector'],
|
||||
passed: data?.metrics?.['HealthCheckPassed_Connector'] || 0,
|
||||
total: data?.metrics?.['HealthCheckTotal_Connector'] || 0,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const getCardInfo = () => {
|
||||
return Utils.request(api.getConnectState(clusterId)).then((res: ConnectState) => {
|
||||
const { connectClusterCount, aliveConnectorCount, aliveTaskCount, totalConnectorCount, totalTaskCount, workerCount } = res || {};
|
||||
const cardMap = [
|
||||
{
|
||||
title: 'Connect集群数',
|
||||
value: getVal(connectClusterCount),
|
||||
customStyle: {
|
||||
// 自定义cardbar样式
|
||||
marginLeft: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Workers',
|
||||
value: getVal(workerCount),
|
||||
},
|
||||
{
|
||||
title() {
|
||||
return (
|
||||
<div>
|
||||
<span style={{ display: 'inline-block', marginRight: '8px' }}>Connectors</span>
|
||||
<Tooltip overlayClassName="rebalance-tooltip" title="conector运行数/总数">
|
||||
<InfoCircleOutlined />
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
value() {
|
||||
return (
|
||||
<span>
|
||||
{getVal(aliveConnectorCount)}/{getVal(totalConnectorCount)}
|
||||
</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title() {
|
||||
return (
|
||||
<div>
|
||||
<span style={{ display: 'inline-block', marginRight: '8px' }}>Tasks</span>
|
||||
<Tooltip overlayClassName="rebalance-tooltip" title="Task运行数/总数">
|
||||
<InfoCircleOutlined />
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
value() {
|
||||
return (
|
||||
<span>
|
||||
{getVal(aliveTaskCount)}/{getVal(totalTaskCount)}
|
||||
</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
setCardData(cardMap);
|
||||
});
|
||||
};
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
Promise.all([getHealthData(), getCardInfo()]).finally(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
}, [clusterId, state]);
|
||||
return <CardBar scene="connect" healthData={healthData} cardColumns={cardData} loading={loading}></CardBar>;
|
||||
};
|
||||
|
||||
export default ConnectCard;
|
||||
@@ -0,0 +1,122 @@
|
||||
/* eslint-disable react/display-name */
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useLocation, useParams } from 'react-router-dom';
|
||||
import CardBar from '@src/components/CardBar';
|
||||
import { healthDataProps } from '.';
|
||||
import { Tooltip, Utils } from 'knowdesign';
|
||||
import Api from '@src/api';
|
||||
import { hashDataParse } from '@src/constants/common';
|
||||
import { HealthStateEnum } from '../HealthState';
|
||||
import { InfoCircleOutlined } from '@ant-design/icons';
|
||||
import { stateEnum } from '@src/pages/Connect/config';
|
||||
const getVal = (val: string | number | undefined | null) => {
|
||||
return val === undefined || val === null || val === '' ? '0' : val;
|
||||
};
|
||||
|
||||
const ConnectDetailCard = (props: { record: any }) => {
|
||||
const { record } = props;
|
||||
const urlParams = useParams<{ clusterId: string; brokerId: string }>();
|
||||
const urlLocation = useLocation<any>();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [cardData, setCardData] = useState([]);
|
||||
const [healthData, setHealthData] = useState<healthDataProps>({
|
||||
state: HealthStateEnum.UNKNOWN,
|
||||
passed: 0,
|
||||
total: 0,
|
||||
});
|
||||
|
||||
const getHealthData = () => {
|
||||
return Utils.post(Api.getConnectDetailMetricPoints(record.connectorName, record?.connectClusterId), [
|
||||
'HealthCheckPassed',
|
||||
'HealthCheckTotal',
|
||||
'HealthState',
|
||||
]).then((data: any) => {
|
||||
setHealthData({
|
||||
state: data?.metrics?.['HealthState'],
|
||||
passed: data?.metrics?.['HealthCheckPassed'] || 0,
|
||||
total: data?.metrics?.['HealthCheckTotal'] || 0,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const getCardInfo = () => {
|
||||
return Utils.request(Api.getConnectDetailState(record.connectorName, record?.connectClusterId)).then((res: any) => {
|
||||
const { type, aliveTaskCount, state, totalTaskCount, totalWorkerCount } = res || {};
|
||||
const cordRightMap = [
|
||||
{
|
||||
title: 'Type',
|
||||
value: () => {
|
||||
return (
|
||||
<>
|
||||
{
|
||||
<span style={{ fontFamily: 'HelveticaNeue-Medium', fontSize: 32, color: '#212529' }}>
|
||||
{Utils.firstCharUppercase(type) || '-'}
|
||||
</span>
|
||||
}
|
||||
</>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Status',
|
||||
// value: Utils.firstCharUppercase(state) || '-',
|
||||
value: () => {
|
||||
return (
|
||||
<>
|
||||
{
|
||||
<span style={{ fontFamily: 'HelveticaNeue-Medium', fontSize: 32, color: stateEnum[state].color }}>
|
||||
{Utils.firstCharUppercase(state) || '-'}
|
||||
</span>
|
||||
}
|
||||
</>
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
title() {
|
||||
return (
|
||||
<div>
|
||||
<span style={{ display: 'inline-block', marginRight: '8px' }}>Tasks</span>
|
||||
<Tooltip overlayClassName="rebalance-tooltip" title="Task运行数/总数">
|
||||
<InfoCircleOutlined />
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
value() {
|
||||
return (
|
||||
<span>
|
||||
{getVal(aliveTaskCount)}/{getVal(totalTaskCount)}
|
||||
</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Workers',
|
||||
value: getVal(totalWorkerCount),
|
||||
},
|
||||
];
|
||||
setCardData(cordRightMap);
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
Promise.all([getHealthData(), getCardInfo()]).finally(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
}, [record]);
|
||||
return (
|
||||
<CardBar
|
||||
record={record}
|
||||
scene="connector"
|
||||
healthData={healthData}
|
||||
cardColumns={cardData}
|
||||
showCardBg={false}
|
||||
loading={loading}
|
||||
></CardBar>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConnectDetailCard;
|
||||
@@ -66,5 +66,5 @@ export default () => {
|
||||
});
|
||||
});
|
||||
}, []);
|
||||
return <CardBar scene="topic" healthData={healthData} cardColumns={cardData} loading={loading}></CardBar>;
|
||||
return <CardBar scene="topics" healthData={healthData} cardColumns={cardData} loading={loading}></CardBar>;
|
||||
};
|
||||
|
||||
@@ -12,7 +12,8 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.card-bar-health {
|
||||
width: 240px;
|
||||
// width: 240px; // 去掉固定宽度自适应
|
||||
margin-right: 10px;
|
||||
height: 70px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -18,7 +18,7 @@ export interface CardBarProps {
|
||||
cardColumns?: any[];
|
||||
healthData?: healthDataProps;
|
||||
showCardBg?: boolean;
|
||||
scene: 'topic' | 'broker' | 'group' | 'zookeeper';
|
||||
scene: 'topics' | 'brokers' | 'topic' | 'broker' | 'group' | 'zookeeper' | 'connect' | 'connector';
|
||||
record?: any;
|
||||
loading?: boolean;
|
||||
needProgress?: boolean;
|
||||
@@ -27,16 +27,26 @@ const renderValue = (v: string | number | ((visibleType?: boolean) => JSX.Elemen
|
||||
return typeof v === 'function' ? v(visibleType) : v;
|
||||
};
|
||||
const sceneCodeMap = {
|
||||
broker: {
|
||||
brokers: {
|
||||
code: 1,
|
||||
fieldName: 'brokerId',
|
||||
alias: 'Brokers',
|
||||
},
|
||||
topic: {
|
||||
broker: {
|
||||
code: 1,
|
||||
fieldName: 'brokerId',
|
||||
alias: 'Broker',
|
||||
},
|
||||
topics: {
|
||||
code: 2,
|
||||
fieldName: 'topicName',
|
||||
alias: 'Topics',
|
||||
},
|
||||
topic: {
|
||||
code: 2,
|
||||
fieldName: 'topicName',
|
||||
alias: 'Topic',
|
||||
},
|
||||
group: {
|
||||
code: 3,
|
||||
fieldName: 'groupName',
|
||||
@@ -47,6 +57,16 @@ const sceneCodeMap = {
|
||||
fieldName: 'zookeeperId',
|
||||
alias: 'Zookeeper',
|
||||
},
|
||||
connect: {
|
||||
code: 5,
|
||||
fieldName: 'connectClusterId',
|
||||
alias: 'Connect',
|
||||
},
|
||||
connector: {
|
||||
code: 6,
|
||||
fieldName: 'connectorName',
|
||||
alias: 'Connector',
|
||||
},
|
||||
};
|
||||
const CardColumnsItem: any = (cardItem: any) => {
|
||||
const { cardColumnsItemData, showCardBg } = cardItem;
|
||||
@@ -87,12 +107,17 @@ const CardBar = (props: CardBarProps) => {
|
||||
useEffect(() => {
|
||||
const sceneObj = sceneCodeMap[scene];
|
||||
const path = record
|
||||
? api.getResourceHealthDetail(Number(routeParams.clusterId), sceneObj.code, record[sceneObj.fieldName])
|
||||
? api.getResourceHealthDetail(
|
||||
scene === 'connector' ? Number(record?.connectClusterId) : Number(routeParams.clusterId),
|
||||
sceneObj.code,
|
||||
record[sceneObj.fieldName]
|
||||
)
|
||||
: api.getResourceListHealthDetail(Number(routeParams.clusterId));
|
||||
const promise = record
|
||||
? Utils.request(path)
|
||||
: Utils.request(path, {
|
||||
params: { dimensionCode: sceneObj.code },
|
||||
method: 'POST',
|
||||
data: scene === 'connect' ? JSON.parse(JSON.stringify([5, 6])) : JSON.parse(JSON.stringify([sceneObj.code])),
|
||||
});
|
||||
promise.then((data: any[]) => {
|
||||
setHealthCheckDetailList(data);
|
||||
@@ -102,6 +127,7 @@ const CardBar = (props: CardBarProps) => {
|
||||
{
|
||||
title: '检查项',
|
||||
dataIndex: 'checkConfig',
|
||||
width: '40%',
|
||||
render(config: any, record: any) {
|
||||
let valueGroup = {};
|
||||
try {
|
||||
@@ -109,7 +135,12 @@ const CardBar = (props: CardBarProps) => {
|
||||
} catch (e) {
|
||||
//
|
||||
}
|
||||
return getConfigItemDetailDesc(record.configItem, valueGroup) || record.configDesc || '-';
|
||||
return (
|
||||
getConfigItemDetailDesc(record.configItem, valueGroup) ||
|
||||
getConfigItemDetailDesc(config.configItem, valueGroup) ||
|
||||
record.configDesc ||
|
||||
'-'
|
||||
);
|
||||
},
|
||||
},
|
||||
// {
|
||||
@@ -119,13 +150,15 @@ const CardBar = (props: CardBarProps) => {
|
||||
{
|
||||
title: '检查时间',
|
||||
dataIndex: 'updateTime',
|
||||
width: '30%',
|
||||
render: (value: number) => {
|
||||
return moment(value).format('YYYY-MM-DD HH:mm:ss');
|
||||
return value ? moment(value).format('YYYY-MM-DD HH:mm:ss') : '-';
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '检查结果',
|
||||
dataIndex: 'passed',
|
||||
width: '30%',
|
||||
render(value: boolean, record: any) {
|
||||
const icon = value ? <IconFont type="icon-zhengchang"></IconFont> : <IconFont type="icon-yichang"></IconFont>;
|
||||
const txt = value ? '已通过' : '未通过';
|
||||
@@ -145,7 +178,7 @@ const CardBar = (props: CardBarProps) => {
|
||||
<Spin spinning={loading}>
|
||||
<div className="card-bar-container">
|
||||
<div className="card-bar-content">
|
||||
{!loading && healthData && needProgress && (
|
||||
{healthData && needProgress && (
|
||||
<div className="card-bar-health">
|
||||
<div className="card-bar-health-process">
|
||||
<HealthState state={healthData?.state} width={74} height={74} />
|
||||
@@ -181,7 +214,7 @@ const CardBar = (props: CardBarProps) => {
|
||||
onClose={(_) => setDetailDrawerVisible(false)}
|
||||
visible={detailDrawerVisible}
|
||||
>
|
||||
<Table rowKey={'topicName'} columns={columns} dataSource={healthCheckDetailList} pagination={false} />
|
||||
<Table rowKey={'configName'} columns={columns} dataSource={healthCheckDetailList} pagination={false} />
|
||||
</Drawer>
|
||||
</Spin>
|
||||
);
|
||||
|
||||
@@ -1,18 +1,25 @@
|
||||
import React, { useState, useEffect, forwardRef, useImperativeHandle } from 'react';
|
||||
import React, { useState, useEffect, forwardRef, useImperativeHandle, useRef } from 'react';
|
||||
import { Drawer, Button, Space, Divider, AppContainer, ProTable, Utils } from 'knowdesign';
|
||||
import { IconFont } from '@knowdesign/icons';
|
||||
import { MetricSelect } from './index';
|
||||
import { arrayMoveImmutable } from 'array-move';
|
||||
import './style/indicator-drawer.less';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { useLocation, useParams } from 'react-router-dom';
|
||||
import api, { MetricType } from '@src/api';
|
||||
import { MetricInfo, resolveMetricsRank } from '@src/constants/chartConfig';
|
||||
|
||||
interface PropsType extends React.HTMLAttributes<HTMLDivElement> {
|
||||
metricSelect: MetricSelect;
|
||||
export interface Inode {
|
||||
name: string;
|
||||
desc: string;
|
||||
}
|
||||
|
||||
interface MetricInfo {
|
||||
name: string;
|
||||
unit: string;
|
||||
desc: string;
|
||||
export interface MetricSelectProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
metricType: MetricType;
|
||||
hide?: boolean;
|
||||
drawerTitle?: string;
|
||||
selectedRows: (string | number)[];
|
||||
checkboxProps?: (record: any) => { [props: string]: any };
|
||||
tableData?: Inode[];
|
||||
submitCallback?: (value: (string | number)[]) => Promise<any>;
|
||||
}
|
||||
|
||||
interface SelectedMetrics {
|
||||
@@ -21,10 +28,14 @@ interface SelectedMetrics {
|
||||
|
||||
type CategoryData = {
|
||||
category: string;
|
||||
metrics: MetricInfo[];
|
||||
metrics: {
|
||||
name: string;
|
||||
unit: string;
|
||||
desc: string;
|
||||
}[];
|
||||
};
|
||||
|
||||
const expandedRowColumns = [
|
||||
export const expandedRowColumns = [
|
||||
{
|
||||
title: '指标名称',
|
||||
dataIndex: 'name',
|
||||
@@ -44,16 +55,7 @@ const expandedRowColumns = [
|
||||
|
||||
const ExpandedRow = ({ metrics, category, selectedMetrics, selectedMetricChange }: any) => {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: 'relative',
|
||||
padding: '12px 16px',
|
||||
margin: '0 7px',
|
||||
border: '1px solid #EFF2F7',
|
||||
borderRadius: '8px',
|
||||
backgroundColor: '#ffffff',
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<ProTable
|
||||
tableProps={{
|
||||
showHeader: false,
|
||||
@@ -76,7 +78,7 @@ const ExpandedRow = ({ metrics, category, selectedMetrics, selectedMetricChange
|
||||
);
|
||||
};
|
||||
|
||||
const MetricSelect = forwardRef(({ metricSelect }: PropsType, ref) => {
|
||||
export const MetricSelect = forwardRef((metricSelect: MetricSelectProps, ref) => {
|
||||
const [global] = AppContainer.useGlobalValue();
|
||||
const { pathname } = useLocation();
|
||||
const [confirmLoading, setConfirmLoading] = useState<boolean>(false);
|
||||
@@ -96,7 +98,11 @@ const MetricSelect = forwardRef(({ metricSelect }: PropsType, ref) => {
|
||||
const formateTableData = () => {
|
||||
const tableData = metricSelect.tableData;
|
||||
const categoryData: {
|
||||
[category: string]: MetricInfo[];
|
||||
[category: string]: {
|
||||
name: string;
|
||||
unit: string;
|
||||
desc: string;
|
||||
}[];
|
||||
} = {};
|
||||
|
||||
tableData.forEach(({ name, desc }) => {
|
||||
@@ -328,4 +334,106 @@ const MetricSelect = forwardRef(({ metricSelect }: PropsType, ref) => {
|
||||
);
|
||||
});
|
||||
|
||||
export default MetricSelect;
|
||||
interface MetricsFilterProps {
|
||||
metricType: MetricType;
|
||||
onSelectChange: (list: (string | number)[], rankList: string[]) => void;
|
||||
}
|
||||
|
||||
const MetricsFilter = forwardRef((props: MetricsFilterProps, ref) => {
|
||||
const { metricType, onSelectChange } = props;
|
||||
const { clusterId } = useParams<{
|
||||
clusterId: string;
|
||||
}>();
|
||||
const [metricsList, setMetricsList] = useState<MetricInfo[]>([]); // 指标列表
|
||||
const [metricRankList, setMetricRankList] = useState<string[]>([]);
|
||||
const [selectedMetricNames, setSelectedMetricNames] = useState<(string | number)[]>(undefined); // 默认选中的指标的列表
|
||||
const metricSelectRef = useRef(null);
|
||||
|
||||
// 更新指标
|
||||
const setMetricList = (metricDetailDTOList: { metric: string; rank: number; set: boolean }[]) => {
|
||||
return Utils.request(api.getDashboardMetricList(clusterId, metricType), {
|
||||
method: 'POST',
|
||||
data: {
|
||||
metricDetailDTOList,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// 图表展示顺序变更
|
||||
const rankChange = (oldIndex: number, newIndex: number) => {
|
||||
const newList = arrayMoveImmutable(metricRankList, oldIndex, newIndex);
|
||||
setMetricRankList(newList);
|
||||
setMetricList(newList.map((metric, rank) => ({ metric, rank, set: metricsList.find(({ name }) => metric === name)?.set || false })));
|
||||
};
|
||||
|
||||
// 更新 rank
|
||||
const updateRank = (metricList: MetricInfo[]) => {
|
||||
const { list, listInfo, shouldUpdate } = resolveMetricsRank(metricList);
|
||||
setMetricRankList(list);
|
||||
if (shouldUpdate) {
|
||||
setMetricList(listInfo);
|
||||
}
|
||||
};
|
||||
|
||||
// 获取指标列表
|
||||
const getMetricList = () => {
|
||||
Utils.request(api.getDashboardMetricList(clusterId, metricType)).then((res: MetricInfo[] | null) => {
|
||||
if (!res) return;
|
||||
const supportMetrics = res.filter((metric) => metric.support);
|
||||
const selectedMetrics = supportMetrics.filter((metric) => metric.set).map((metric) => metric.name);
|
||||
updateRank([...supportMetrics]);
|
||||
setMetricsList(supportMetrics);
|
||||
setSelectedMetricNames(selectedMetrics);
|
||||
});
|
||||
};
|
||||
|
||||
// 指标选中项更新回调
|
||||
const metricSelectCallback = (newMetricNames: (string | number)[]) => {
|
||||
const updateMetrics: { metric: string; set: boolean; rank: number }[] = [];
|
||||
// 需要选中的指标
|
||||
newMetricNames.forEach(
|
||||
(name) =>
|
||||
!selectedMetricNames.includes(name) &&
|
||||
updateMetrics.push({ metric: name as string, set: true, rank: metricsList.find(({ name: metric }) => metric === name)?.rank })
|
||||
);
|
||||
// 取消选中的指标
|
||||
selectedMetricNames.forEach(
|
||||
(name) =>
|
||||
!newMetricNames.includes(name) &&
|
||||
updateMetrics.push({ metric: name as string, set: false, rank: metricsList.find(({ name: metric }) => metric === name)?.rank })
|
||||
);
|
||||
|
||||
const requestPromise = Object.keys(updateMetrics).length ? setMetricList(updateMetrics) : Promise.resolve();
|
||||
requestPromise.then(
|
||||
() => getMetricList(),
|
||||
() => getMetricList()
|
||||
);
|
||||
|
||||
return requestPromise;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
onSelectChange(selectedMetricNames, metricRankList);
|
||||
}, [selectedMetricNames, metricRankList]);
|
||||
|
||||
useEffect(() => {
|
||||
getMetricList();
|
||||
}, []);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
rankChange,
|
||||
open: () => metricSelectRef.current?.open(),
|
||||
}));
|
||||
|
||||
return (
|
||||
<MetricSelect
|
||||
ref={metricSelectRef}
|
||||
metricType={metricType}
|
||||
tableData={metricsList}
|
||||
selectedRows={selectedMetricNames}
|
||||
submitCallback={metricSelectCallback}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
export default MetricsFilter;
|
||||
|
||||
@@ -1,203 +0,0 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Radio, Input, Popover, Space, Checkbox, Row, Col, Button } from 'knowdesign';
|
||||
import { IconFont } from '@knowdesign/icons';
|
||||
import { InodeScopeModule } from './index';
|
||||
import './style/node-scope.less';
|
||||
|
||||
interface propsType {
|
||||
change: Function;
|
||||
nodeScopeModule: InodeScopeModule;
|
||||
}
|
||||
|
||||
const OptionsDefault = [
|
||||
{
|
||||
label: 'Top 5',
|
||||
value: 5,
|
||||
},
|
||||
{
|
||||
label: 'Top 10',
|
||||
value: 10,
|
||||
},
|
||||
{
|
||||
label: 'Top 15',
|
||||
value: 15,
|
||||
},
|
||||
];
|
||||
|
||||
const NodeScope = ({ nodeScopeModule, change }: propsType) => {
|
||||
const {
|
||||
customScopeList: customList,
|
||||
scopeName = '',
|
||||
scopeLabel = '自定义范围',
|
||||
searchPlaceholder = '输入内容进行搜索',
|
||||
} = nodeScopeModule;
|
||||
const [topNum, setTopNum] = useState<number>(5);
|
||||
const [isTop, setIsTop] = useState(true);
|
||||
const [audioOptions, setAudioOptions] = useState(OptionsDefault);
|
||||
const [scopeSearchValue, setScopeSearchValue] = useState('');
|
||||
const [inputValue, setInputValue] = useState<string>(null);
|
||||
const [indeterminate, setIndeterminate] = useState(false);
|
||||
const [popVisible, setPopVisible] = useState(false);
|
||||
const [checkAll, setCheckAll] = useState(false);
|
||||
const [checkedListTemp, setCheckedListTemp] = useState([]);
|
||||
const [checkedList, setCheckedList] = useState([]);
|
||||
const [allCheckedList, setAllCheckedList] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
const all = customList?.map((item) => item.value) || [];
|
||||
setAllCheckedList(all);
|
||||
}, [customList]);
|
||||
|
||||
useEffect(() => {
|
||||
if (topNum) {
|
||||
const timeOption = audioOptions.find((item) => item.value === topNum);
|
||||
|
||||
setInputValue(timeOption?.label);
|
||||
setCheckedListTemp([]);
|
||||
setCheckedList([]);
|
||||
|
||||
setPopVisible(false);
|
||||
}
|
||||
}, [topNum]);
|
||||
|
||||
useEffect(() => {
|
||||
setIndeterminate(!!checkedListTemp.length && checkedListTemp.length < allCheckedList.length);
|
||||
setCheckAll(checkedListTemp?.length === allCheckedList.length);
|
||||
}, [checkedListTemp]);
|
||||
|
||||
const customSure = () => {
|
||||
if (checkedListTemp?.length > 0) {
|
||||
setCheckedList(checkedListTemp);
|
||||
change(checkedListTemp, false);
|
||||
setIsTop(false);
|
||||
setTopNum(null);
|
||||
setInputValue(`${checkedListTemp?.length}项`);
|
||||
setPopVisible(false);
|
||||
}
|
||||
};
|
||||
|
||||
const customCancel = () => {
|
||||
setCheckedListTemp(checkedList);
|
||||
setPopVisible(false);
|
||||
};
|
||||
|
||||
const visibleChange = (visible: any) => {
|
||||
setCheckedListTemp(checkedList);
|
||||
setPopVisible(visible);
|
||||
};
|
||||
|
||||
const periodtimeChange = (e: any) => {
|
||||
const topNum = e.target.value;
|
||||
setTopNum(topNum);
|
||||
change(topNum, true);
|
||||
setIsTop(true);
|
||||
};
|
||||
|
||||
const onCheckAllChange = (e: any) => {
|
||||
setCheckedListTemp(e.target.checked ? allCheckedList : []);
|
||||
setIndeterminate(false);
|
||||
setCheckAll(e.target.checked);
|
||||
};
|
||||
|
||||
const checkChange = (val: any) => {
|
||||
setCheckedListTemp(val);
|
||||
// setIndeterminate(!!val.length && val.length < allCheckedList.length);
|
||||
// setCheckAll(val?.length === allCheckedList.length);
|
||||
};
|
||||
|
||||
const clickContent = (
|
||||
<div className="dd-node-scope-module">
|
||||
{/* <span>时间:</span> */}
|
||||
<div className="flx_con">
|
||||
<div className="flx_l">
|
||||
<h6 className="time_title">选择 top 范围</h6>
|
||||
<Radio.Group
|
||||
optionType="button"
|
||||
buttonStyle="solid"
|
||||
className="topNum-radio-group"
|
||||
// options={audioOptions}
|
||||
onChange={periodtimeChange}
|
||||
value={topNum}
|
||||
>
|
||||
<Space direction="vertical" size={16}>
|
||||
{audioOptions.map((item, index) => (
|
||||
<Radio value={item.value} key={index}>
|
||||
{item.label}
|
||||
</Radio>
|
||||
))}
|
||||
</Space>
|
||||
</Radio.Group>
|
||||
</div>
|
||||
<div className="flx_r">
|
||||
<h6 className="time_title">{scopeLabel}</h6>
|
||||
<div className="custom-scope">
|
||||
<div className="check-row">
|
||||
<Checkbox className="check-all" indeterminate={indeterminate} onChange={onCheckAllChange} checked={checkAll}>
|
||||
全选
|
||||
</Checkbox>
|
||||
<Input
|
||||
className="search-input"
|
||||
suffix={<IconFont type="icon-fangdajing" style={{ fontSize: '16px' }} />}
|
||||
size="small"
|
||||
placeholder={searchPlaceholder}
|
||||
onChange={(e) => setScopeSearchValue(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="fixed-height">
|
||||
<Checkbox.Group style={{ width: '100%' }} onChange={checkChange} value={checkedListTemp}>
|
||||
<Row gutter={[10, 12]}>
|
||||
{customList
|
||||
.filter((item) => item.label.includes(scopeSearchValue))
|
||||
.map((item) => (
|
||||
<Col span={12} key={item.value}>
|
||||
<Checkbox value={item.value}>{item.label}</Checkbox>
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
</Checkbox.Group>
|
||||
</div>
|
||||
|
||||
<div className="btn-con">
|
||||
<Button
|
||||
type="primary"
|
||||
size="small"
|
||||
className="btn-sure"
|
||||
onClick={customSure}
|
||||
disabled={checkedListTemp?.length > 0 ? false : true}
|
||||
>
|
||||
确定
|
||||
</Button>
|
||||
<Button size="small" onClick={customCancel}>
|
||||
取消
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div id="d-node-scope">
|
||||
<div className="scope-title">{scopeName}筛选:</div>
|
||||
<Popover
|
||||
trigger={['click']}
|
||||
visible={popVisible}
|
||||
content={clickContent}
|
||||
placement="bottomRight"
|
||||
overlayClassName="d-node-scope-popover large-size"
|
||||
onVisibleChange={visibleChange}
|
||||
>
|
||||
<span className="input-span">
|
||||
<Input
|
||||
className={isTop ? 'relativeTime d-node-scope-input' : 'absoluteTime d-node-scope-input'}
|
||||
value={inputValue}
|
||||
readOnly={true}
|
||||
suffix={<IconFont type="icon-jiantou1" rotate={90} style={{ color: '#74788D' }}></IconFont>}
|
||||
/>
|
||||
</span>
|
||||
</Popover>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default NodeScope;
|
||||
@@ -0,0 +1,115 @@
|
||||
import React, { useState, useEffect, useRef, PropsWithChildren } from 'react';
|
||||
import { Radio, Input, Popover, Space, Checkbox, Row, Col, Button } from 'knowdesign';
|
||||
import { IconFont } from '@knowdesign/icons';
|
||||
import './style/node-scope.less';
|
||||
|
||||
interface NodeSelectProps {
|
||||
name?: string;
|
||||
onChange: (data: any, isTop: boolean) => void;
|
||||
}
|
||||
|
||||
const TOP_SELECT_OPTIONS = [
|
||||
{
|
||||
label: 'Top 5',
|
||||
value: 5,
|
||||
},
|
||||
{
|
||||
label: 'Top 10',
|
||||
value: 10,
|
||||
},
|
||||
{
|
||||
label: 'Top 15',
|
||||
value: 15,
|
||||
},
|
||||
];
|
||||
|
||||
const NodeSelect = ({ name, onChange, children }: PropsWithChildren<NodeSelectProps>) => {
|
||||
const [topNum, setTopNum] = useState<number>(5);
|
||||
const [isTop, setIsTop] = useState(true);
|
||||
const [audioOptions] = useState(TOP_SELECT_OPTIONS);
|
||||
const [inputValue, setInputValue] = useState<string>(null);
|
||||
const [popVisible, setPopVisible] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (topNum) {
|
||||
const timeOption = audioOptions.find((item) => item.value === topNum);
|
||||
|
||||
setInputValue(timeOption?.label);
|
||||
|
||||
setPopVisible(false);
|
||||
}
|
||||
}, [topNum]);
|
||||
|
||||
const visibleChange = (visible: any) => {
|
||||
setPopVisible(visible);
|
||||
};
|
||||
|
||||
const periodtimeChange = (e: any) => {
|
||||
const topNum = e.target.value;
|
||||
setTopNum(topNum);
|
||||
onChange(topNum, true);
|
||||
setIsTop(true);
|
||||
};
|
||||
|
||||
const clickContent = (
|
||||
<div className="dd-node-scope-module">
|
||||
<div className="flx_con">
|
||||
<div className="flx_l">
|
||||
<h6 className="time_title">选择 top 范围</h6>
|
||||
<Radio.Group optionType="button" buttonStyle="solid" className="topNum-radio-group" onChange={periodtimeChange} value={topNum}>
|
||||
<Space direction="vertical" size={16}>
|
||||
{audioOptions.map((item, index) => (
|
||||
<Radio value={item.value} key={index}>
|
||||
{item.label}
|
||||
</Radio>
|
||||
))}
|
||||
</Space>
|
||||
</Radio.Group>
|
||||
</div>
|
||||
<div className="flx_r">
|
||||
{children ? (
|
||||
React.Children.map(children, (child) => {
|
||||
return React.cloneElement(child as React.ReactElement<any>, {
|
||||
isTop,
|
||||
visibleChange: visibleChange,
|
||||
onChange: (data: any, inputValue: string) => {
|
||||
onChange(data, false);
|
||||
setIsTop(false);
|
||||
setTopNum(null);
|
||||
setInputValue(inputValue);
|
||||
setPopVisible(false);
|
||||
},
|
||||
});
|
||||
})
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div id="d-node-scope">
|
||||
<div className="scope-title">{name}筛选:</div>
|
||||
<Popover
|
||||
trigger={['click']}
|
||||
visible={popVisible}
|
||||
content={clickContent}
|
||||
placement="bottomRight"
|
||||
overlayClassName="d-node-scope-popover large-size"
|
||||
onVisibleChange={visibleChange}
|
||||
>
|
||||
<span className="input-span">
|
||||
<Input
|
||||
className={isTop ? 'relativeTime d-node-scope-input' : 'absoluteTime d-node-scope-input'}
|
||||
value={inputValue}
|
||||
readOnly={true}
|
||||
suffix={<IconFont type="icon-jiantou1" rotate={90} style={{ color: '#74788D' }}></IconFont>}
|
||||
/>
|
||||
</span>
|
||||
</Popover>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default NodeSelect;
|
||||
@@ -3,16 +3,8 @@ import { Select, Divider, Button } from 'knowdesign';
|
||||
import { IconFont } from '@knowdesign/icons';
|
||||
import moment from 'moment';
|
||||
import { DRangeTime } from 'knowdesign';
|
||||
import MetricSelect from './MetricSelect';
|
||||
import NodeScope from './NodeScope';
|
||||
|
||||
import NodeSelect from './NodeSelect';
|
||||
import './style/index.less';
|
||||
import { MetricType } from 'src/api';
|
||||
|
||||
export interface Inode {
|
||||
name: string;
|
||||
desc: string;
|
||||
}
|
||||
|
||||
export interface KsHeaderOptions {
|
||||
rangeTime: [number, number];
|
||||
@@ -21,18 +13,9 @@ export interface KsHeaderOptions {
|
||||
gridNum?: number;
|
||||
scopeData?: {
|
||||
isTop: boolean;
|
||||
data: number | number[];
|
||||
data: any;
|
||||
};
|
||||
}
|
||||
export interface MetricSelect {
|
||||
metricType: MetricType;
|
||||
hide?: boolean;
|
||||
drawerTitle?: string;
|
||||
selectedRows: (string | number)[];
|
||||
checkboxProps?: (record: any) => { [props: string]: any };
|
||||
tableData?: Inode[];
|
||||
submitCallback?: (value: (string | number)[]) => Promise<any>;
|
||||
}
|
||||
|
||||
export interface IfilterData {
|
||||
hostName?: string;
|
||||
@@ -41,25 +24,15 @@ export interface IfilterData {
|
||||
agent?: string;
|
||||
}
|
||||
|
||||
export interface IcustomScope {
|
||||
label: string;
|
||||
value: string | number;
|
||||
}
|
||||
|
||||
export interface InodeScopeModule {
|
||||
customScopeList: IcustomScope[];
|
||||
scopeName?: string;
|
||||
scopeLabel?: string;
|
||||
searchPlaceholder?: string;
|
||||
change?: () => void;
|
||||
}
|
||||
|
||||
interface PropsType {
|
||||
metricSelect?: MetricSelect;
|
||||
hideNodeScope?: boolean;
|
||||
hideGridSelect?: boolean;
|
||||
nodeScopeModule?: InodeScopeModule;
|
||||
nodeSelect?: {
|
||||
name?: string;
|
||||
customContent?: React.ReactElement<any>;
|
||||
};
|
||||
onChange: (options: KsHeaderOptions) => void;
|
||||
openMetricFilter: () => void;
|
||||
}
|
||||
|
||||
interface ScopeData {
|
||||
@@ -84,15 +57,12 @@ const GRID_SIZE_OPTIONS = [
|
||||
];
|
||||
|
||||
const MetricOperateBar = ({
|
||||
metricSelect,
|
||||
nodeScopeModule = {
|
||||
customScopeList: [],
|
||||
},
|
||||
nodeSelect = {},
|
||||
hideNodeScope = false,
|
||||
hideGridSelect = false,
|
||||
onChange: onChangeCallback,
|
||||
openMetricFilter,
|
||||
}: PropsType): JSX.Element => {
|
||||
const metricSelectRef = useRef(null);
|
||||
const [gridNum, setGridNum] = useState<number>(GRID_SIZE_OPTIONS[1].value);
|
||||
const [rangeTime, setRangeTime] = useState<[number, number]>(() => {
|
||||
const curTimeStamp = moment().valueOf();
|
||||
@@ -170,20 +140,22 @@ const MetricOperateBar = ({
|
||||
</div>
|
||||
<div className="header-right">
|
||||
{/* 节点范围 */}
|
||||
{!hideNodeScope && <NodeScope nodeScopeModule={nodeScopeModule} change={nodeScopeChange} />}
|
||||
{!hideNodeScope && (
|
||||
<NodeSelect name={nodeSelect.name || ''} onChange={nodeScopeChange}>
|
||||
{nodeSelect.customContent}
|
||||
</NodeSelect>
|
||||
)}
|
||||
{/* 分栏 */}
|
||||
{!hideGridSelect && (
|
||||
<Select className="grid-select" style={{ width: 70 }} value={gridNum} options={GRID_SIZE_OPTIONS} onChange={sizeChange} />
|
||||
)}
|
||||
{(!hideNodeScope || !hideGridSelect) && <Divider type="vertical" style={{ height: 20, top: 0 }} />}
|
||||
<Button type="primary" onClick={() => metricSelectRef.current.open()}>
|
||||
<Button type="primary" onClick={() => openMetricFilter()}>
|
||||
指标筛选
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* 指标筛选 */}
|
||||
{!metricSelect?.hide && <MetricSelect ref={metricSelectRef} metricSelect={metricSelect} />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -2,135 +2,15 @@
|
||||
.dcloud-drawer-body {
|
||||
padding-top: 2px !important;
|
||||
}
|
||||
.dcloud-table-cell.dcloud-table-selection-column {
|
||||
padding: 11px 0;
|
||||
}
|
||||
.dcloud-table .dcloud-table-tbody .dcloud-table-cell:nth-child(1) {
|
||||
padding-right: 8px;
|
||||
}
|
||||
.dcloud-table-expanded-row {
|
||||
> td {
|
||||
padding: 0 0 0 56px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
// .dd-indicator-drawer {
|
||||
// @drawerItemH: 27px;
|
||||
// @primary-color: #556ee6;
|
||||
// &.contain-tab {
|
||||
// .@{ant-prefix}-drawer-body {
|
||||
// padding: 0 20px 20px 20px;
|
||||
// }
|
||||
// .@{ant-prefix}-layout-sider {
|
||||
// height: ~'calc(100vh - 268px)';
|
||||
// }
|
||||
// .@{ant-prefix}-spin-container {
|
||||
// height: ~'calc(100vh - 268px - 12px)';
|
||||
// .@{ant-prefix}-table {
|
||||
// height: ~'calc(100% - 36px)';
|
||||
// overflow: auto;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// .hide {
|
||||
// display: none;
|
||||
// }
|
||||
// .@{ant-prefix}-drawer-body {
|
||||
// padding: 12px 20px;
|
||||
// .label-name {
|
||||
// height: 34px;
|
||||
// line-height: 34px;
|
||||
// font-size: 13px;
|
||||
// color: #495057;
|
||||
// }
|
||||
// }
|
||||
// .@{ant-prefix}-spin-container {
|
||||
// height: ~'calc(100vh - 218px - 12px)';
|
||||
// .@{ant-prefix}-table {
|
||||
// height: ~'calc(100% - 36px)';
|
||||
// overflow: auto;
|
||||
// }
|
||||
// }
|
||||
// .@{ant-prefix}-layout-sider {
|
||||
// height: ~'calc(100vh - 218px)';
|
||||
// overflow: auto;
|
||||
// }
|
||||
// .@{ant-prefix}-menu-overflow {
|
||||
// // display: inline-flex;
|
||||
// &::before,
|
||||
// &::after {
|
||||
// width: 0;
|
||||
// }
|
||||
// }
|
||||
|
||||
// .@{ant-prefix}-tree {
|
||||
// padding: 20px;
|
||||
// &.@{ant-prefix}-tree-directory {
|
||||
// .@{ant-prefix}-tree-treenode {
|
||||
// padding-right: 24px;
|
||||
// &:not(.@{ant-prefix}-tree-treenode-switcher-open) {
|
||||
// padding-right: 0;
|
||||
// }
|
||||
// &:not(.@{ant-prefix}-tree-treenode-switcher-close) {
|
||||
// padding-right: 0;
|
||||
// }
|
||||
// &.@{ant-prefix}-tree-treenode-selected {
|
||||
// &::before {
|
||||
// background: transparent;
|
||||
// }
|
||||
// }
|
||||
// height: 27px;
|
||||
// &::before {
|
||||
// bottom: 0;
|
||||
// }
|
||||
// .@{ant-prefix}-tree-switcher {
|
||||
// position: absolute;
|
||||
// right: 0;
|
||||
// z-index: 8;
|
||||
// line-height: @drawerItemH;
|
||||
// color: #495057;
|
||||
// .anticon {
|
||||
// vertical-align: middle;
|
||||
// font-size: 16px;
|
||||
// transform: rotate(90deg);
|
||||
// }
|
||||
// &_close {
|
||||
// .@{ant-prefix}-tree-switcher-icon svg {
|
||||
// transform: rotate(-180deg);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// .@{ant-prefix}-tree-node-content-wrapper {
|
||||
// padding-left: 4px;
|
||||
// width: 100%;
|
||||
// height: @drawerItemH;
|
||||
// line-height: @drawerItemH;
|
||||
// min-height: @drawerItemH;
|
||||
// font-weight: bold;
|
||||
// color: #495057;
|
||||
// &.@{ant-prefix}-tree-node-content-wrapper-normal {
|
||||
// font-weight: normal;
|
||||
// color: #74788d;
|
||||
// .@{ant-prefix}-tree-title {
|
||||
// width: ~'calc(100%)';
|
||||
// }
|
||||
// }
|
||||
// &.@{ant-prefix}-tree-node-selected {
|
||||
// color: @primary-color;
|
||||
// }
|
||||
// .@{ant-prefix}-tree-icon__customize {
|
||||
// line-height: 22px;
|
||||
// .anticon {
|
||||
// vertical-align: middle;
|
||||
// font-size: 16px;
|
||||
// }
|
||||
// }
|
||||
// .@{ant-prefix}-tree-iconEle {
|
||||
// width: auto;
|
||||
// margin-right: 8px;
|
||||
// }
|
||||
// .@{ant-prefix}-tree-title {
|
||||
// display: inline-block;
|
||||
// width: ~'calc(100% - 24px)';
|
||||
// overflow: hidden;
|
||||
// overflow: hidden;
|
||||
// text-overflow: ellipsis;
|
||||
// white-space: nowrap;
|
||||
// & > span {
|
||||
// padding-left: 3px;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { MetricType } from '@src/api';
|
||||
import { FormattedMetricData } from '@src/constants/chartConfig';
|
||||
import { AppContainer, Empty, SingleChart, Spin, Tooltip } from 'knowdesign';
|
||||
import { arrayMoveImmutable } from 'array-move';
|
||||
import DragGroup from '../DragGroup';
|
||||
import { IconFont } from '@knowdesign/icons';
|
||||
import { getChartConfig } from './config';
|
||||
import { EventBus } from 'knowdesign/lib/utils/event-bus';
|
||||
|
||||
const DRAG_GROUP_GUTTER_NUM: [number, number] = [16, 16];
|
||||
|
||||
interface ChartListProps {
|
||||
busInstance: EventBus;
|
||||
loading: boolean;
|
||||
gridNum: number;
|
||||
data: FormattedMetricData[];
|
||||
autoReload: boolean;
|
||||
dragCallback: (oldIndex: number, newIndex: number) => void;
|
||||
onExpand: (metricName: string, metricType: MetricType) => void;
|
||||
}
|
||||
|
||||
const ChartList = (props: ChartListProps) => {
|
||||
const { loading, gridNum, data, autoReload, busInstance, dragCallback, onExpand } = props;
|
||||
const [global] = AppContainer.useGlobalValue();
|
||||
const [chartData, setChartData] = useState(data);
|
||||
|
||||
// 拖拽开始回调,触发图表的 onDrag 事件( 设置为 true ),禁止同步展示图表的 tooltip
|
||||
const dragStart = () => {
|
||||
busInstance.emit('onDrag', true);
|
||||
};
|
||||
|
||||
// 拖拽结束回调,更新图表顺序,并触发图表的 onDrag 事件( 设置为 false ),允许同步展示图表的 tooltip
|
||||
const dragEnd = ({ oldIndex, newIndex }: { oldIndex: number; newIndex: number }) => {
|
||||
dragCallback(oldIndex, newIndex);
|
||||
busInstance.emit('onDrag', false);
|
||||
setChartData(arrayMoveImmutable(chartData, oldIndex, newIndex));
|
||||
};
|
||||
|
||||
// 监听盒子宽度变化,重置图表宽度
|
||||
const observeDashboardWidthChange = () => {
|
||||
const targetNode = document.getElementsByClassName('dcd-two-columns-layout-sider-footer')[0];
|
||||
targetNode && targetNode.addEventListener('click', () => busInstance.emit('chartResize'));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setChartData(data);
|
||||
}, [data]);
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(() => observeDashboardWidthChange());
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="topic-dashboard-container">
|
||||
<Spin spinning={loading} style={{ height: 400 }}>
|
||||
{chartData && chartData.length ? (
|
||||
<div className="no-group-con">
|
||||
<DragGroup
|
||||
sortableContainerProps={{
|
||||
onSortStart: dragStart,
|
||||
onSortEnd: dragEnd,
|
||||
axis: 'xy',
|
||||
useDragHandle: true,
|
||||
}}
|
||||
gridProps={{
|
||||
span: gridNum,
|
||||
gutter: DRAG_GROUP_GUTTER_NUM,
|
||||
}}
|
||||
>
|
||||
{chartData.map((data) => {
|
||||
const { metricName, metricType, metricUnit, metricLines, showLegend } = data;
|
||||
|
||||
return (
|
||||
<div key={metricName} className="dashboard-drag-item-box">
|
||||
<div className="dashboard-drag-item-box-title">
|
||||
<Tooltip
|
||||
placement="topLeft"
|
||||
title={() => {
|
||||
let content = '';
|
||||
const metricDefine = global.getMetricDefine(metricType, metricName);
|
||||
if (metricDefine) {
|
||||
content = metricDefine.desc;
|
||||
}
|
||||
return content;
|
||||
}}
|
||||
>
|
||||
<span>
|
||||
<span className="name">{metricName}</span>
|
||||
<span className="unit">({metricUnit})</span>
|
||||
{(metricType === MetricType.Connect || metricType === MetricType.Connectors) && (
|
||||
<span
|
||||
style={{
|
||||
padding: '2px 8px',
|
||||
borderRadius: 4,
|
||||
fontSize: 12,
|
||||
background: metricType === MetricType.Connect ? '#ECECF6' : 'rgba(85,110,230,0.10)',
|
||||
color: metricType === MetricType.Connect ? '#495057' : '#5664FF',
|
||||
}}
|
||||
>
|
||||
{metricType === MetricType.Connect ? 'Cluster' : 'Connector'}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className="expand-icon-box" onClick={() => onExpand(metricName, metricType)}>
|
||||
<IconFont type="icon-chuangkoufangda" className="expand-icon" />
|
||||
</div>
|
||||
<SingleChart
|
||||
chartKey={metricName}
|
||||
chartTypeProp="line"
|
||||
showHeader={false}
|
||||
wrapStyle={{
|
||||
width: 'auto',
|
||||
height: 222,
|
||||
}}
|
||||
connectEventName={`${metricType}BoardDragChart`}
|
||||
eventBus={busInstance}
|
||||
propChartData={metricLines}
|
||||
optionMergeProps={{ replaceMerge: autoReload ? ['xAxis'] : ['series'] }}
|
||||
{...getChartConfig(`${metricName}{unit|(${metricUnit})}`, metricLines.length, showLegend)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</DragGroup>
|
||||
</div>
|
||||
) : loading ? (
|
||||
<></>
|
||||
) : (
|
||||
<Empty description="数据为空,请选择指标或刷新" image={Empty.PRESENTED_IMAGE_CUSTOM} style={{ padding: '100px 0' }} />
|
||||
)}
|
||||
</Spin>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChartList;
|
||||
@@ -165,12 +165,28 @@ const ChartDetail = (props: ChartDetailProps) => {
|
||||
|
||||
// 请求图表数据
|
||||
const getMetricChartData = ([startTime, endTime]: readonly [number, number]) => {
|
||||
return Utils.post(api.getDashboardMetricChartData(clusterId, metricType), {
|
||||
const getQueryUrl = () => {
|
||||
switch (metricType) {
|
||||
case MetricType.Connect: {
|
||||
return api.getConnectClusterMetrics(clusterId);
|
||||
}
|
||||
default: {
|
||||
return api.getDashboardMetricChartData(clusterId, metricType);
|
||||
}
|
||||
}
|
||||
};
|
||||
const queryMap = {
|
||||
[MetricType.Broker]: 'brokerIds',
|
||||
[MetricType.Topic]: 'topics',
|
||||
[MetricType.Connect]: 'connectClusterIdList',
|
||||
[MetricType.Connectors]: 'connectorNameList',
|
||||
};
|
||||
return Utils.post(getQueryUrl(), {
|
||||
startTime,
|
||||
endTime,
|
||||
metricsNames: [metricName],
|
||||
topNu: null,
|
||||
[metricType === MetricType.Broker ? 'brokerIds' : 'topics']: queryLines,
|
||||
[queryMap[metricType as keyof typeof queryMap]]: queryLines,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { arrayMoveImmutable } from 'array-move';
|
||||
import { Utils, Empty, Spin, AppContainer, SingleChart, Tooltip } from 'knowdesign';
|
||||
import { IconFont } from '@knowdesign/icons';
|
||||
import { Utils, AppContainer, Checkbox, Input, Row, Col, Button } from 'knowdesign';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import api, { MetricType } from '@src/api';
|
||||
import { MetricInfo, OriginMetricData, FormattedMetricData, formatChartData, resolveMetricsRank } from '@src/constants/chartConfig';
|
||||
import { OriginMetricData, FormattedMetricData, formatChartData } from '@src/constants/chartConfig';
|
||||
import ChartOperateBar, { KsHeaderOptions } from '../ChartOperateBar';
|
||||
import DragGroup from '../DragGroup';
|
||||
import ChartDetail from './Detail';
|
||||
import { getChartConfig, getMetricDashboardReq } from './config';
|
||||
import { getMetricDashboardReq } from './config';
|
||||
import './index.less';
|
||||
import MetricsFilter from '../ChartOperateBar/MetricSelect';
|
||||
import ChartList from './ChartList';
|
||||
import { IconFont } from '@knowdesign/icons';
|
||||
|
||||
interface IcustomScope {
|
||||
label: string;
|
||||
@@ -25,7 +25,111 @@ type PropsType = {
|
||||
const { EventBus } = Utils;
|
||||
const busInstance = new EventBus();
|
||||
|
||||
const DRAG_GROUP_GUTTER_NUM: [number, number] = [16, 16];
|
||||
interface SelectContentProps {
|
||||
title: string;
|
||||
list: {
|
||||
label: string;
|
||||
value: string | number;
|
||||
}[];
|
||||
isTop?: boolean;
|
||||
visibleChange?: (v: boolean) => void;
|
||||
onChange?: (list: any[], inputValue: string) => void;
|
||||
searchPlaceholder?: string;
|
||||
}
|
||||
|
||||
const SelectContent = (props: SelectContentProps) => {
|
||||
const { searchPlaceholder = '输入内容进行搜索', list, isTop, visibleChange, onChange } = props;
|
||||
const [scopeSearchValue, setScopeSearchValue] = useState('');
|
||||
// 全选属性
|
||||
const [indeterminate, setIndeterminate] = useState(false);
|
||||
const [checkAll, setCheckAll] = useState(false);
|
||||
const [checkedListTemp, setCheckedListTemp] = useState([]);
|
||||
const [allCheckedList, setAllCheckedList] = useState([]);
|
||||
const defaultChecked = useRef([]);
|
||||
|
||||
const customSure = () => {
|
||||
defaultChecked.current = checkedListTemp;
|
||||
onChange?.(checkedListTemp, `${checkedListTemp?.length}项`);
|
||||
};
|
||||
|
||||
const customCancel = () => {
|
||||
setCheckedListTemp(defaultChecked.current);
|
||||
visibleChange(false);
|
||||
};
|
||||
|
||||
const onCheckAllChange = (e: any) => {
|
||||
setCheckedListTemp(e.target.checked ? allCheckedList : []);
|
||||
};
|
||||
|
||||
const checkChange = (val: any) => {
|
||||
setCheckedListTemp(val);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setIndeterminate(!!checkedListTemp.length && checkedListTemp.length < allCheckedList.length);
|
||||
setCheckAll(checkedListTemp?.length === allCheckedList.length);
|
||||
}, [checkedListTemp]);
|
||||
|
||||
useEffect(() => {
|
||||
const all = list?.map((item) => item.value) || [];
|
||||
setAllCheckedList(all);
|
||||
}, [list]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isTop) {
|
||||
setCheckedListTemp([]);
|
||||
defaultChecked.current = [];
|
||||
}
|
||||
}, [isTop]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<h6 className="time_title">{props.title}</h6>
|
||||
<div className="custom-scope">
|
||||
<div className="check-row">
|
||||
<Checkbox className="check-all" indeterminate={indeterminate} checked={checkAll} onChange={onCheckAllChange}>
|
||||
全选
|
||||
</Checkbox>
|
||||
<Input
|
||||
className="search-input"
|
||||
suffix={<IconFont type="icon-fangdajing" style={{ fontSize: '16px' }} />}
|
||||
size="small"
|
||||
placeholder={searchPlaceholder}
|
||||
onChange={(e) => setScopeSearchValue(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="fixed-height">
|
||||
<Checkbox.Group style={{ width: '100%' }} onChange={checkChange} value={checkedListTemp}>
|
||||
<Row gutter={[10, 12]}>
|
||||
{list
|
||||
.filter((item) => item.label.includes(scopeSearchValue))
|
||||
.map((item) => (
|
||||
<Col span={12} key={item.value}>
|
||||
<Checkbox value={item.value}>{item.label}</Checkbox>
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
</Checkbox.Group>
|
||||
</div>
|
||||
|
||||
<div className="btn-con">
|
||||
<Button
|
||||
type="primary"
|
||||
size="small"
|
||||
className="btn-sure"
|
||||
onClick={customSure}
|
||||
disabled={checkedListTemp?.length > 0 ? false : true}
|
||||
>
|
||||
确定
|
||||
</Button>
|
||||
<Button size="small" onClick={customCancel}>
|
||||
取消
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const DraggableCharts = (props: PropsType): JSX.Element => {
|
||||
const [global] = AppContainer.useGlobalValue();
|
||||
@@ -35,14 +139,14 @@ const DraggableCharts = (props: PropsType): JSX.Element => {
|
||||
}>();
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
const [scopeList, setScopeList] = useState<IcustomScope[]>([]); // 节点范围列表
|
||||
const [metricsList, setMetricsList] = useState<MetricInfo[]>([]); // 指标列表
|
||||
const [selectedMetricNames, setSelectedMetricNames] = useState<(string | number)[]>([]); // 默认选中的指标的列表
|
||||
const [curHeaderOptions, setCurHeaderOptions] = useState<ChartFilterOptions>();
|
||||
const [metricList, setMetricList] = useState<(string | number)[]>([]);
|
||||
const [metricChartData, setMetricChartData] = useState<FormattedMetricData[]>([]); // 指标图表数据列表
|
||||
const [gridNum, setGridNum] = useState<number>(12); // 图表列布局
|
||||
const metricRankList = useRef<string[]>([]);
|
||||
const chartDetailRef = useRef(null);
|
||||
const curFetchingTimestamp = useRef(0);
|
||||
const metricRankList = useRef<string[]>([]);
|
||||
const metricFilterRef = useRef(null);
|
||||
const chartDetailRef = useRef(null);
|
||||
|
||||
// 获取节点范围列表
|
||||
const getScopeList = async () => {
|
||||
@@ -61,40 +165,6 @@ const DraggableCharts = (props: PropsType): JSX.Element => {
|
||||
setScopeList(list);
|
||||
};
|
||||
|
||||
// 更新 rank
|
||||
const updateRank = (metricList: MetricInfo[]) => {
|
||||
const { list, listInfo, shouldUpdate } = resolveMetricsRank(metricList);
|
||||
metricRankList.current = list;
|
||||
if (shouldUpdate) {
|
||||
setMetricList(listInfo);
|
||||
}
|
||||
};
|
||||
|
||||
// 获取指标列表
|
||||
const getMetricList = () => {
|
||||
Utils.request(api.getDashboardMetricList(clusterId, dashboardType)).then((res: MetricInfo[] | null) => {
|
||||
if (!res) return;
|
||||
const supportMetrics = res.filter((metric) => metric.support);
|
||||
const selectedMetrics = supportMetrics.filter((metric) => metric.set).map((metric) => metric.name);
|
||||
updateRank([...supportMetrics]);
|
||||
setMetricsList(supportMetrics);
|
||||
setSelectedMetricNames(selectedMetrics);
|
||||
if (!selectedMetrics.length) {
|
||||
setLoading(false);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 更新指标
|
||||
const setMetricList = (metricDetailDTOList: { metric: string; rank: number; set: boolean }[]) => {
|
||||
return Utils.request(api.getDashboardMetricList(clusterId, dashboardType), {
|
||||
method: 'POST',
|
||||
data: {
|
||||
metricDetailDTOList,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// 根据筛选项获取图表信息
|
||||
const getMetricChartData = () => {
|
||||
!curHeaderOptions.isAutoReload && setLoading(true);
|
||||
@@ -107,7 +177,7 @@ const DraggableCharts = (props: PropsType): JSX.Element => {
|
||||
{
|
||||
startTime,
|
||||
endTime,
|
||||
metricsNames: selectedMetricNames,
|
||||
metricsNames: metricList || [],
|
||||
},
|
||||
dashboardType === MetricType.Broker || dashboardType === MetricType.Topic
|
||||
? {
|
||||
@@ -168,65 +238,29 @@ const DraggableCharts = (props: PropsType): JSX.Element => {
|
||||
}
|
||||
};
|
||||
|
||||
// 指标选中项更新回调
|
||||
const metricSelectCallback = (newMetricNames: (string | number)[]) => {
|
||||
const updateMetrics: { metric: string; set: boolean; rank: number }[] = [];
|
||||
// 需要选中的指标
|
||||
newMetricNames.forEach(
|
||||
(name) =>
|
||||
!selectedMetricNames.includes(name) &&
|
||||
updateMetrics.push({ metric: name as string, set: true, rank: metricsList.find(({ name: metric }) => metric === name)?.rank })
|
||||
);
|
||||
// 取消选中的指标
|
||||
selectedMetricNames.forEach(
|
||||
(name) =>
|
||||
!newMetricNames.includes(name) &&
|
||||
updateMetrics.push({ metric: name as string, set: false, rank: metricsList.find(({ name: metric }) => metric === name)?.rank })
|
||||
);
|
||||
|
||||
const requestPromise = Object.keys(updateMetrics).length ? setMetricList(updateMetrics) : Promise.resolve();
|
||||
requestPromise.then(
|
||||
() => getMetricList(),
|
||||
() => getMetricList()
|
||||
);
|
||||
|
||||
return requestPromise;
|
||||
};
|
||||
|
||||
// 拖拽开始回调,触发图表的 onDrag 事件( 设置为 true ),禁止同步展示图表的 tooltip
|
||||
const dragStart = () => {
|
||||
busInstance.emit('onDrag', true);
|
||||
};
|
||||
|
||||
// 拖拽结束回调,更新图表顺序,并触发图表的 onDrag 事件( 设置为 false ),允许同步展示图表的 tooltip
|
||||
const dragEnd = ({ oldIndex, newIndex }: { oldIndex: number; newIndex: number }) => {
|
||||
busInstance.emit('onDrag', false);
|
||||
// 图表拖拽
|
||||
const dragCallback = (oldIndex: number, newIndex: number) => {
|
||||
const originFrom = metricRankList.current.indexOf(metricChartData[oldIndex].metricName);
|
||||
const originTarget = metricRankList.current.indexOf(metricChartData[newIndex].metricName);
|
||||
const newList = arrayMoveImmutable(metricRankList.current, originFrom, originTarget);
|
||||
metricRankList.current = newList;
|
||||
setMetricList(newList.map((metric, rank) => ({ metric, rank, set: metricsList.find(({ name }) => metric === name)?.set || false })));
|
||||
setMetricChartData(arrayMoveImmutable(metricChartData, oldIndex, newIndex));
|
||||
metricFilterRef.current?.rankChange(originFrom, originTarget);
|
||||
};
|
||||
|
||||
// 监听盒子宽度变化,重置图表宽度
|
||||
const observeDashboardWidthChange = () => {
|
||||
const targetNode = document.getElementsByClassName('dcd-two-columns-layout-sider-footer')[0];
|
||||
targetNode && targetNode.addEventListener('click', () => busInstance.emit('chartResize'));
|
||||
// 展开图表详情
|
||||
const onExpand = (metricName: string) => {
|
||||
const linesName = scopeList.map((item) => item.value);
|
||||
chartDetailRef.current.onOpen(dashboardType, metricName, linesName);
|
||||
};
|
||||
|
||||
// 获取图表指标
|
||||
useEffect(() => {
|
||||
if (selectedMetricNames.length && curHeaderOptions) {
|
||||
if (metricList?.length && curHeaderOptions) {
|
||||
getMetricChartData();
|
||||
}
|
||||
}, [curHeaderOptions, selectedMetricNames]);
|
||||
}, [curHeaderOptions, metricList]);
|
||||
|
||||
useEffect(() => {
|
||||
// 初始化页面,获取 scope 和 metric 信息
|
||||
(dashboardType === MetricType.Broker || dashboardType === MetricType.Topic) && getScopeList();
|
||||
getMetricList();
|
||||
|
||||
setTimeout(() => observeDashboardWidthChange());
|
||||
}, []);
|
||||
|
||||
return (
|
||||
@@ -234,95 +268,36 @@ const DraggableCharts = (props: PropsType): JSX.Element => {
|
||||
<ChartOperateBar
|
||||
onChange={ksHeaderChange}
|
||||
hideNodeScope={dashboardType === MetricType.Zookeeper}
|
||||
nodeScopeModule={{
|
||||
customScopeList: scopeList,
|
||||
scopeName: dashboardType === MetricType.Broker ? 'Broker' : dashboardType === MetricType.Topic ? 'Topic' : 'Zookeeper',
|
||||
scopeLabel: `自定义 ${
|
||||
dashboardType === MetricType.Broker ? 'Broker' : dashboardType === MetricType.Topic ? 'Topic' : 'Zookeeper'
|
||||
} 范围`,
|
||||
}}
|
||||
metricSelect={{
|
||||
hide: false,
|
||||
metricType: dashboardType,
|
||||
tableData: metricsList,
|
||||
selectedRows: selectedMetricNames,
|
||||
submitCallback: metricSelectCallback,
|
||||
openMetricFilter={() => metricFilterRef.current?.open()}
|
||||
nodeSelect={{
|
||||
name: dashboardType === MetricType.Broker ? 'Broker' : dashboardType === MetricType.Topic ? 'Topic' : 'Zookeeper',
|
||||
customContent: (
|
||||
<SelectContent
|
||||
title={`自定义 ${
|
||||
dashboardType === MetricType.Broker ? 'Broker' : dashboardType === MetricType.Topic ? 'Topic' : 'Zookeeper'
|
||||
} 范围`}
|
||||
list={scopeList}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<div className="topic-dashboard-container">
|
||||
<Spin spinning={loading} style={{ height: 400 }}>
|
||||
{metricChartData && metricChartData.length ? (
|
||||
<div className="no-group-con">
|
||||
<DragGroup
|
||||
sortableContainerProps={{
|
||||
onSortStart: dragStart,
|
||||
onSortEnd: dragEnd,
|
||||
axis: 'xy',
|
||||
useDragHandle: true,
|
||||
}}
|
||||
gridProps={{
|
||||
span: gridNum,
|
||||
gutter: DRAG_GROUP_GUTTER_NUM,
|
||||
}}
|
||||
>
|
||||
{metricChartData.map((data) => {
|
||||
const { metricName, metricUnit, metricLines, showLegend } = data;
|
||||
|
||||
return (
|
||||
<div key={metricName} className="dashboard-drag-item-box">
|
||||
<div className="dashboard-drag-item-box-title">
|
||||
<Tooltip
|
||||
placement="topLeft"
|
||||
title={() => {
|
||||
let content = '';
|
||||
const metricDefine = global.getMetricDefine(dashboardType, metricName);
|
||||
if (metricDefine) {
|
||||
content = metricDefine.desc;
|
||||
}
|
||||
return content;
|
||||
}}
|
||||
>
|
||||
<span>
|
||||
<span className="name">{metricName}</span>
|
||||
<span className="unit">({metricUnit})</span>
|
||||
</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div
|
||||
className="expand-icon-box"
|
||||
onClick={() => {
|
||||
const linesName = scopeList.map((item) => item.value);
|
||||
chartDetailRef.current.onOpen(dashboardType, metricName, linesName);
|
||||
}}
|
||||
>
|
||||
<IconFont type="icon-chuangkoufangda" className="expand-icon" />
|
||||
</div>
|
||||
<SingleChart
|
||||
chartKey={metricName}
|
||||
chartTypeProp="line"
|
||||
showHeader={false}
|
||||
wrapStyle={{
|
||||
width: 'auto',
|
||||
height: 222,
|
||||
}}
|
||||
connectEventName={`${dashboardType}BoardDragChart`}
|
||||
eventBus={busInstance}
|
||||
propChartData={metricLines}
|
||||
optionMergeProps={{ replaceMerge: curHeaderOptions.isAutoReload ? ['xAxis'] : ['series'] }}
|
||||
{...getChartConfig(`${metricName}{unit|(${metricUnit})}`, metricLines.length, showLegend)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</DragGroup>
|
||||
</div>
|
||||
) : loading ? (
|
||||
<></>
|
||||
) : (
|
||||
<Empty description="数据为空,请选择指标或刷新" image={Empty.PRESENTED_IMAGE_CUSTOM} style={{ padding: '100px 0' }} />
|
||||
)}
|
||||
</Spin>
|
||||
</div>
|
||||
<MetricsFilter
|
||||
ref={metricFilterRef}
|
||||
metricType={dashboardType}
|
||||
onSelectChange={(list, rankList) => {
|
||||
metricRankList.current = rankList;
|
||||
setMetricList(list);
|
||||
}}
|
||||
/>
|
||||
<ChartList
|
||||
busInstance={busInstance}
|
||||
loading={loading}
|
||||
gridNum={gridNum}
|
||||
data={metricChartData}
|
||||
autoReload={curHeaderOptions?.isAutoReload}
|
||||
dragCallback={dragCallback}
|
||||
onExpand={onExpand}
|
||||
/>
|
||||
{/* 图表详情 */}
|
||||
<ChartDetail ref={chartDetailRef} />
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,8 @@ import React, { useLayoutEffect, useRef, useState } from 'react';
|
||||
import './index.less';
|
||||
|
||||
interface SwitchTabProps {
|
||||
defaultKey: string;
|
||||
defaultKey?: string;
|
||||
activeKey?: string | number;
|
||||
onChange: (key: string) => void;
|
||||
children: any;
|
||||
}
|
||||
@@ -18,9 +19,9 @@ const TabItem = (props: TabItemProps) => {
|
||||
};
|
||||
|
||||
const SwitchTab = (props: SwitchTabProps) => {
|
||||
const { defaultKey, onChange, children } = props;
|
||||
const { defaultKey, activeKey, onChange, children } = props;
|
||||
const tabRef = useRef();
|
||||
const [activeKey, setActiveKey] = useState<string>(defaultKey);
|
||||
const [active, setActive] = useState<string | number>(activeKey || defaultKey);
|
||||
const [pos, setPos] = useState({
|
||||
left: 0,
|
||||
width: 0,
|
||||
@@ -39,6 +40,10 @@ const SwitchTab = (props: SwitchTabProps) => {
|
||||
return false;
|
||||
});
|
||||
}
|
||||
}, [active]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
activeKey && setActive(activeKey);
|
||||
}, [activeKey]);
|
||||
|
||||
return (
|
||||
@@ -48,9 +53,10 @@ const SwitchTab = (props: SwitchTabProps) => {
|
||||
return (
|
||||
<div
|
||||
key={key}
|
||||
className={`d-switch-tab-content d-switch-tab-content-${activeKey === key ? 'active' : ''}`}
|
||||
className={`d-switch-tab-content d-switch-tab-content-${active === key ? 'active' : ''}`}
|
||||
onClick={() => {
|
||||
setActiveKey(key);
|
||||
// 受控模式下不自动更新状态
|
||||
!activeKey && setActive(key);
|
||||
onChange(key);
|
||||
}}
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user