Files
KnowStreaming/km-console/packages/layout-clusters-fe/src/pages/TopicDetail/BrokersDetail.tsx
lucasun b1892c21e2 修复前端新增角色失败等问题 (#1107)
1.新增角色不选择系统管理权限点报错问题;
2.Connect配置项里面涉及敏感字段的值用*号代替;
3.Topic详情、ConsumerGroup详情,ConsumerGroup表格支持手动刷;
4.Topic Message预览,Offset为0不显示数值,添加offset排序;

---------

Co-authored-by: 孙超 <jacksuny@foxmail.com>
Co-authored-by: EricZeng <zengqiao_cn@163.com>
2023-08-01 16:34:30 +08:00

465 lines
16 KiB
TypeScript

import React, { useCallback } from 'react';
import { useEffect, useState } from 'react';
import { AppContainer, Button, Empty, List, Popover, ProTable, Radio, Spin, Utils } from 'knowdesign';
import { IconFont } from '@knowdesign/icons';
import { CloseOutlined } from '@ant-design/icons';
import api, { MetricType } from '@src/api';
import { useParams } from 'react-router-dom';
import TagsWithHide from '@src/components/TagsWithHide';
import SwitchTab from '@src/components/SwitchTab';
import RenderEmpty from '@src/components/RenderEmpty';
import { useForceRefresh } from '@src/components/utils';
interface PropsType {
hashData: any;
// searchKeywords: string;
}
interface PartitionsSummary {
brokerCount: number;
deadBrokerCount: number;
liveBrokerCount: number;
noLeaderPartitionCount: number;
partitionCount: number;
underReplicatedPartitionCount: number;
}
const PARTITION_DETAIL_METRICS = ['LogEndOffset', 'LogStartOffset', 'Messages', 'LogSize'] as const;
interface PartitionDetail {
partitionId: number;
topicName: string;
leaderBrokerId: number;
assignReplicas: number[];
inSyncReplicas: number[];
latestMetrics: {
clusterPhyId: number;
metrics: {
[K in typeof PARTITION_DETAIL_METRICS[number]]: number;
};
timestamp: number;
};
}
interface BrokersDetail {
live: number;
dead: number;
total: number;
partitionCount: number;
noLeaderPartitionIdList: number[];
underReplicatedPartitionIdList: number[];
brokerPartitionStateList: brokerPartitionState[];
}
interface brokerPartitionState {
alive: boolean;
brokerId: number;
bytesInOneMinuteRate: number;
bytesOutOneMinuteRate: number;
host: string;
replicaList: brokerReplica[];
}
interface brokerReplica {
leaderBrokerId: number;
partitionId: number;
topicName: string;
isLeaderReplace: boolean;
inSync: boolean;
}
function getTranformedBytes(bytes: number) {
const unit = ['B', 'KB', 'MB', 'GB', 'TB'];
let i = 0;
let isUp = true,
outBytes = bytes;
if (typeof outBytes !== 'number') {
outBytes = Number(outBytes);
if (isNaN(outBytes)) return [outBytes, unit[i]];
}
while (isUp) {
if (outBytes / 1024 >= 1) {
outBytes /= 1024;
i++;
} else {
isUp = false;
}
}
return [outBytes.toFixed(2), unit[i]];
}
const PartitionPopoverContent = (props: {
clusterId: string;
hashData: any;
record: brokerReplica;
brokerId: brokerPartitionState['brokerId'];
close: () => void;
}) => {
const [global] = AppContainer.useGlobalValue();
const { clusterId, hashData, record, brokerId, close } = props;
const [loading, setLoading] = useState<boolean>(true);
const [partitionInfo, setPartitionInfo] = useState<{ label: string; value: string | number }[]>([]);
// 获取单个 Partition 详情
const getDetail = () => {
const { partitionId, leaderBrokerId, inSync } = record;
Utils.request(api.getPartitionMetricInfo(clusterId, hashData.topicName, brokerId, partitionId), {
method: 'POST',
data: PARTITION_DETAIL_METRICS,
}).then((res: any) => {
const type = MetricType.Replication;
const metricsData = res?.metrics || {};
const partitionInfo = [
{ label: 'LeaderBroker', value: leaderBrokerId },
{
label: 'BeginningOffset',
value: `${metricsData.LogStartOffset === undefined ? '-' : metricsData.LogStartOffset} ${
global.getMetricDefine(type, 'LogStartOffset')?.unit || ''
}`,
},
{
label: 'EndOffset',
value: `${metricsData.LogEndOffset === undefined ? '-' : metricsData.LogEndOffset} ${
global.getMetricDefine(type, 'LogEndOffset')?.unit || ''
}`,
},
{
label: 'MsgNum',
value: `${metricsData.Messages === undefined ? '-' : metricsData.Messages} ${
global.getMetricDefine(type, 'Messages')?.unit || ''
}`,
},
{
label: 'LogSize',
value: `${metricsData.LogSize === undefined ? '-' : Utils.formatAssignSize(metricsData.LogSize, 'MB')} MB`,
},
{ label: '是否同步', value: inSync ? '是' : '否' },
];
setPartitionInfo(partitionInfo);
setLoading(false);
});
};
useEffect(() => {
getDetail();
}, []);
return (
<Spin spinning={loading}>
<div className="header">
<div className="title"></div>
<Button
type="text"
icon={<CloseOutlined className="close-icon" />}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
close();
}}
/>
</div>
<div className="main">
<List
itemLayout="vertical"
size="small"
dataSource={partitionInfo}
renderItem={(item) => (
<List.Item key={item.label} extra={item.value}>
{item.label}
</List.Item>
)}
/>
</div>
</Spin>
);
};
const PartitionSummary = (props: { clusterId: string; topicName: string }) => {
const { clusterId, topicName } = props;
const [partitionsSummary, setPartitionsSummary] = useState<PartitionsSummary>(null);
useEffect(() => {
// 获取统计信息
Utils.request(api.getTopicPartitionsSummary(clusterId, topicName)).then((res: PartitionsSummary) => {
setPartitionsSummary(res);
});
}, []);
return (
<>
<div className="info-box">
<div className="info-detail info-detail-title">
<span className="desc">Brokers </span>
<span className="num">{partitionsSummary?.brokerCount}</span>
</div>
<div className="info-detail">
<span className="desc">live</span>
<span className="num">{partitionsSummary?.liveBrokerCount}</span>
</div>
<div className="info-detail">
<span className="desc">down</span>
<span className="num">{partitionsSummary?.deadBrokerCount}</span>
</div>
</div>
<div className="info-box">
<div className="info-detail info-detail-title">
<span className="desc">Partition </span>
<span className="num">{partitionsSummary?.partitionCount}</span>
</div>
<div className="info-detail">
<span className="desc">No Leader</span>
<span className="num">{partitionsSummary?.noLeaderPartitionCount}</span>
</div>
<div className="info-detail">
<span className="desc">URP</span>
<span className="num">{partitionsSummary?.underReplicatedPartitionCount}</span>
</div>
</div>
</>
);
};
const PartitionCard = (props: { clusterId: string; hashData: any }) => {
const { clusterId, hashData } = props;
const [brokersDetail, setBrokersDetail] = useState<BrokersDetail>();
const [hoverPartitionId, setHoverPartitionId] = useState<brokerReplica['partitionId']>(-1);
const [clickPartition, setClickPartition] = useState<string>('');
const [loading, setLoading] = useState<boolean>(true);
const closePartitionDetail = useCallback(() => setClickPartition(''), []);
useEffect(() => {
Utils.request(api.getTopicBrokersList(clusterId, hashData.topicName)).then(
(res: any) => {
setBrokersDetail(res);
setLoading(false);
},
() => setLoading(false)
);
}, []);
return (
<Spin spinning={loading}>
<div className="broker-container">
{brokersDetail?.brokerPartitionStateList?.length ? (
brokersDetail?.brokerPartitionStateList.map((partitionState) => {
return (
<div className="broker-container-box" key={partitionState.brokerId}>
<div className="broker-container-box-header">
<div className="header-info">
<div className="label">Broker</div>
<div className="value">{partitionState.brokerId}</div>
</div>
<div className="header-info">
<div className="label">Host ID</div>
<div className="value">{partitionState.host || '-'}</div>
</div>
{['BytesIn', 'BytesOut'].map((type) => {
return (
<div className="header-info" key={type}>
<div className="label">{type}</div>
<div className="value">
{getTranformedBytes(
type === 'BytesIn' ? partitionState.bytesInOneMinuteRate : partitionState.bytesOutOneMinuteRate
).map((val, i) => {
return i ? <span className="unit">{val}/s</span> : <span>{val}</span>;
})}
</div>
</div>
);
})}
</div>
<div className="broker-container-box-detail">
{partitionState.alive ? (
partitionState?.replicaList?.length ? (
<div className={`partition-list ${hoverPartitionId !== -1 ? 'partition-list-hover-state' : ''}`}>
{partitionState?.replicaList?.map((partition) => {
return (
<div
key={partition.partitionId}
className={`partition-list-item partition-list-item-${
partition.isLeaderReplace ? 'leader' : partition.inSync ? 'isr' : 'osr'
} ${partition.partitionId === hoverPartitionId ? 'partition-active' : ''}`}
onMouseEnter={() => setHoverPartitionId(partition.partitionId)}
onMouseLeave={() => setHoverPartitionId(-1)}
onClick={() => setClickPartition(`${partitionState.brokerId}&${partition.partitionId}`)}
>
<Popover
visible={clickPartition === `${partitionState.brokerId}&${partition.partitionId}`}
onVisibleChange={(v) => !v && closePartitionDetail()}
overlayClassName="broker-partition-popover"
content={
<PartitionPopoverContent
clusterId={clusterId}
hashData={hashData}
record={partition}
brokerId={partitionState.brokerId}
close={closePartitionDetail}
/>
}
destroyTooltipOnHide={true}
trigger="click"
placement="rightTop"
>
{partition.partitionId}
</Popover>
</div>
);
})}
</div>
) : (
<RenderEmpty message="暂无数据" height="unset" />
)
) : (
<RenderEmpty message="暂无数据" height="unset" />
)}
</div>
</div>
);
})
) : loading ? (
<></>
) : (
<Empty style={{ margin: '0 auto', marginTop: 100 }} />
)}
</div>
</Spin>
);
};
const PartitionTable = (props: { clusterId: string; hashData: any }) => {
const { clusterId, hashData } = props;
const [loading, setLoading] = useState<boolean>(true);
const [partitionsDetail, setPartitionsDetail] = useState<PartitionDetail[]>([]);
const columns = [
{
title: 'Partition ID',
dataIndex: 'partitionId',
},
{
title: 'StartOffset',
dataIndex: ['latestMetrics', 'metrics', 'LogStartOffset'],
},
{
title: 'EndOffset',
dataIndex: ['latestMetrics', 'metrics', 'LogEndOffset'],
},
{
title: 'MsgNum',
dataIndex: ['latestMetrics', 'metrics', 'Messages'],
},
{
title: 'Leader Broker',
dataIndex: 'leaderBrokerId',
},
{
title: 'LogSize(MB)',
dataIndex: ['latestMetrics', 'metrics', 'LogSize'],
render: (size: number | undefined) => (size === undefined ? '-' : Utils.formatAssignSize(size, 'MB')),
},
{
title: 'AR',
dataIndex: 'assignReplicas',
width: 180,
render: (arr: PartitionDetail['assignReplicas']) => <TagsWithHide list={arr} expandTagContent={(len) => `共有${len}`} />,
},
{
title: 'ISR',
dataIndex: 'inSyncReplicas',
width: 180,
render: (arr: PartitionDetail['inSyncReplicas']) => <TagsWithHide list={arr} expandTagContent={(len) => `共有${len}`} />,
},
];
useEffect(() => {
Utils.request(api.getTopicPartitionsDetail(clusterId, hashData.topicName), {
method: 'POST',
data: PARTITION_DETAIL_METRICS,
}).then((res: PartitionDetail[]) => {
setPartitionsDetail(res);
setLoading(false);
});
}, []);
return (
<ProTable
tableProps={{
loading,
rowKey: 'partitionId',
columns: columns as any,
dataSource: partitionsDetail,
showHeader: false,
}}
/>
);
};
export default (props: PropsType) => {
const { clusterId } = useParams<{ clusterId: string }>();
const { hashData } = props;
const [showMode, setShowMode] = useState<string>('card');
const [refreshKey, forceRefresh] = useForceRefresh();
return (
<>
<div className="brokers-tab-container" key={`${refreshKey}`}>
<div className="overview">
<div className="left">
<span
style={{ display: 'inline-block', padding: '0 10px', marginRight: '10px', borderRight: '1px solid #ccc', fontSize: '15px' }}
onClick={forceRefresh as () => void}
>
<i className="iconfont icon-shuaxin1" style={{ cursor: 'pointer' }} />
</span>
<PartitionSummary clusterId={clusterId} topicName={hashData.topicName} />
</div>
<div className="cases-box">
{showMode === 'card' && (
<div className="broker-cases">
<div className="case case-leader">
<div className="icon"></div>
<div className="desc">Leader</div>
</div>
<div className="case case-isr">
<div className="icon"></div>
<div className="desc">ISR</div>
</div>
<div className="case case-osr">
<div className="icon"></div>
<div className="desc">OSR</div>
</div>
</div>
)}
<SwitchTab
defaultKey="card"
onChange={(key) => {
setShowMode(key);
}}
>
<SwitchTab.TabItem key="card">
<div style={{ width: 34, height: 23 }}>
<IconFont type="icon-tubiao" />
</div>
</SwitchTab.TabItem>
<SwitchTab.TabItem key="table">
<div style={{ width: 34, height: 23 }}>
<IconFont type="icon-biaoge" />
</div>
</SwitchTab.TabItem>
</SwitchTab>
</div>
</div>
{showMode === 'card' ? (
<PartitionCard clusterId={clusterId} hashData={hashData} />
) : (
<PartitionTable clusterId={clusterId} hashData={hashData} />
)}
</div>
</>
);
};