This commit is contained in:
孙超
2022-12-15 18:48:41 +08:00
committed by EricZeng
parent 5bceed7105
commit 860d0b92e2
59 changed files with 25972 additions and 1497 deletions

View File

@@ -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>;
};

View File

@@ -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;

View File

@@ -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;

View File

@@ -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>;
};

View File

@@ -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;

View File

@@ -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>
);

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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} />}
</>
);
};

View File

@@ -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;
// }
// }
// }
// }
// }
// }
// }

View File

@@ -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;

View File

@@ -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,
});
};

View File

@@ -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>

View File

@@ -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);
}}
>