mirror of
https://github.com/didi/KnowStreaming.git
synced 2026-01-15 04:38:16 +08:00
初始化3.0.0版本
This commit is contained in:
@@ -0,0 +1,462 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { AppContainer, Button, Empty, IconFont, List, Popover, ProTable, Radio, Spin, Utils } from 'knowdesign';
|
||||
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';
|
||||
|
||||
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 RenderEmpty = (props: { message: string }) => {
|
||||
const { message } = props;
|
||||
return (
|
||||
<>
|
||||
<div className="empty-panel">
|
||||
<div className="img" />
|
||||
<div className="text">{message}</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
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">
|
||||
{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="暂无数据" />
|
||||
)
|
||||
) : (
|
||||
<RenderEmpty message="暂无数据" />
|
||||
)}
|
||||
</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');
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="brokers-tab-container">
|
||||
<div className="overview">
|
||||
<div className="left">
|
||||
<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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user