mirror of
https://github.com/didi/KnowStreaming.git
synced 2026-01-13 19:42:15 +08:00
初始化3.0.0版本
This commit is contained in:
@@ -0,0 +1,151 @@
|
||||
import { Collapse, Divider, Spin, Utils } from 'knowdesign';
|
||||
import * as React from 'react';
|
||||
import API from '../../api';
|
||||
import { Link, useParams } from 'react-router-dom';
|
||||
import InfiniteScroll from 'react-infinite-scroll-component';
|
||||
import moment from 'moment';
|
||||
import { timeFormat } from '../../constants/common';
|
||||
import { DownOutlined } from '@ant-design/icons';
|
||||
import { renderToolTipValue } from './config';
|
||||
|
||||
const { Panel } = Collapse;
|
||||
|
||||
interface ILog {
|
||||
clusterPhyId: number;
|
||||
createTime: number;
|
||||
operateTime: string;
|
||||
resName: string;
|
||||
resTypeCode: number;
|
||||
resTypeName: string;
|
||||
updateTime: number;
|
||||
}
|
||||
|
||||
const ChangeLog = () => {
|
||||
const { clusterId } = useParams<{ clusterId: string }>();
|
||||
const [data, setData] = React.useState<ILog[]>([]);
|
||||
const [loading, setLoading] = React.useState<boolean>(true);
|
||||
const [pagination, setPagination] = React.useState({
|
||||
pageNo: 0,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
});
|
||||
|
||||
const getChangeLog = () => {
|
||||
const promise = Utils.request(API.getClusterChangeLog(+clusterId), {
|
||||
params: {
|
||||
pageNo: pagination.pageNo + 1,
|
||||
pageSize: 10,
|
||||
},
|
||||
});
|
||||
promise.then((res: any) => {
|
||||
setData((cur) => cur.concat(res?.bizData));
|
||||
setPagination(res?.pagination);
|
||||
});
|
||||
return promise;
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
getChangeLog().then(
|
||||
() => setLoading(false),
|
||||
() => setLoading(false)
|
||||
);
|
||||
}, []);
|
||||
|
||||
const renderEmpty = () => {
|
||||
return (
|
||||
<>
|
||||
<div className="empty-panel">
|
||||
<div className="img" />
|
||||
<div className="text">暂无配置记录</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const getHref = (item: any) => {
|
||||
if (item.resTypeName.toLowerCase().includes('topic')) return `/cluster/${clusterId}/topic/list#topicName=${item.resName}`;
|
||||
if (item.resTypeName.toLowerCase().includes('broker')) return `/cluster/${clusterId}/broker/list#brokerId=${item.resName}`;
|
||||
return '';
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="change-log-panel">
|
||||
<div className="title">历史变更记录</div>
|
||||
{!loading && !data.length ? (
|
||||
renderEmpty()
|
||||
) : (
|
||||
<div id="changelog-scroll-box">
|
||||
<Spin spinning={loading} style={{ paddingLeft: '42%', marginTop: 100 }} />
|
||||
<InfiniteScroll
|
||||
dataLength={data.length}
|
||||
next={getChangeLog as any}
|
||||
hasMore={data.length < pagination.total}
|
||||
loader={<Spin style={{ paddingLeft: '42%', paddingTop: 10 }} spinning={true} />}
|
||||
endMessage={
|
||||
!pagination.total ? (
|
||||
''
|
||||
) : (
|
||||
<Divider className="load-completed-tip" plain>
|
||||
加载完成 共 {pagination.total} 条
|
||||
</Divider>
|
||||
)
|
||||
}
|
||||
scrollableTarget="changelog-scroll-box"
|
||||
>
|
||||
<Collapse defaultActiveKey={['log-0']} accordion>
|
||||
{data.map((item, index) => {
|
||||
return (
|
||||
<Panel
|
||||
header={
|
||||
<>
|
||||
<div className="header">
|
||||
<div className="label">{renderToolTipValue(`[${item.resTypeName}] ${item.resName}`, 24)}</div>
|
||||
<span className="icon">
|
||||
<DownOutlined />
|
||||
</span>
|
||||
</div>
|
||||
<div className="header-time">{moment(item.updateTime).format(timeFormat)}</div>
|
||||
</>
|
||||
}
|
||||
key={`log-${index}`}
|
||||
showArrow={false}
|
||||
>
|
||||
<div className="log-item">
|
||||
<span>名称</span>
|
||||
<div className="value">
|
||||
{getHref(item) ? (
|
||||
<Link to={getHref(item)}>{renderToolTipValue(item.resName, 18)}</Link>
|
||||
) : (
|
||||
renderToolTipValue(item.resName, 18)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<Divider />
|
||||
<div className="log-item">
|
||||
<span>时间</span>
|
||||
<span className="value">{moment(item.updateTime).format(timeFormat)}</span>
|
||||
</div>
|
||||
<Divider />
|
||||
<div className="log-item">
|
||||
<span>内容</span>
|
||||
<span className="value">{'修改配置'}</span>
|
||||
</div>
|
||||
<Divider />
|
||||
<div className="log-item">
|
||||
<span>类型</span>
|
||||
<span className="value">{item.resTypeName}</span>
|
||||
</div>
|
||||
</Panel>
|
||||
);
|
||||
})}
|
||||
</Collapse>
|
||||
</InfiniteScroll>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChangeLog;
|
||||
@@ -0,0 +1,59 @@
|
||||
/* eslint-disable react/display-name */
|
||||
import { Drawer, Form, Spin, Table, Utils } from 'knowdesign';
|
||||
import React, { useEffect, useState, forwardRef, useImperativeHandle } from 'react';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { getDetailColumn } from './config';
|
||||
import API from '../../api';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
const CheckDetail = forwardRef((props: any, ref): JSX.Element => {
|
||||
const intl = useIntl();
|
||||
const [form] = Form.useForm();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [data, setData] = useState([]);
|
||||
const { clusterId } = useParams<{ clusterId: string }>();
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
setVisible,
|
||||
getHealthDetail,
|
||||
}));
|
||||
|
||||
const getHealthDetail = () => {
|
||||
setLoading(true);
|
||||
return Utils.request(API.getResourceListHealthDetail(+clusterId)).then((res: any) => {
|
||||
setData(res);
|
||||
setLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
const onCancel = () => {
|
||||
form.resetFields();
|
||||
setVisible(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (visible) {
|
||||
getHealthDetail();
|
||||
}
|
||||
}, [visible]);
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
className="drawer-content"
|
||||
onClose={onCancel}
|
||||
maskClosable={false}
|
||||
title="Cluster健康状态详情"
|
||||
// title={intl.formatMessage({ id: 'check.detail' })}
|
||||
visible={visible}
|
||||
placement="right"
|
||||
width={1080}
|
||||
>
|
||||
<Spin spinning={loading}>
|
||||
<Table dataSource={data} columns={getDetailColumn(+clusterId)} pagination={false} />
|
||||
</Spin>
|
||||
</Drawer>
|
||||
);
|
||||
});
|
||||
|
||||
export default CheckDetail;
|
||||
@@ -0,0 +1,98 @@
|
||||
import { getBasicChartConfig } from '@src/constants/chartConfig';
|
||||
import moment from 'moment';
|
||||
|
||||
const DEFAULT_METRIC = 'MessagesIn';
|
||||
|
||||
// 图表 tooltip 展示的样式
|
||||
const messagesInTooltipFormatter = (date: any, arr: any) => {
|
||||
// 面积图只有一条线,这里直接取 arr 的第 0 项
|
||||
const params = arr[0];
|
||||
// MessageIn 的指标数据存放在 data 数组第 3 项
|
||||
const metricsData = params.data[2];
|
||||
|
||||
const str = `<div style="margin: 3px 0;">
|
||||
<div style="display:flex;align-items:center;">
|
||||
<div style="margin-right:4px;width:8px;height:2px;background-color:${params.color};"></div>
|
||||
<div style="flex:1;display:flex;justify-content:space-between;align-items:center;overflow: hidden;">
|
||||
<span style="flex: 1;font-size:12px;color:#74788D;pointer-events:auto;margin-left:2px;line-height: 18px;font-family: HelveticaNeue;overflow: hidden; text-overflow: ellipsis; white-space: no-wrap;">
|
||||
${params.seriesName}
|
||||
</span>
|
||||
<span style="font-size:12px;color:#212529;line-height:18px;font-family:HelveticaNeue-Medium;margin-left: 10px;">
|
||||
${parseFloat(Number(params.value[1]).toFixed(3))}
|
||||
<span style="font-family: PingFangSC-Regular;color: #495057;">${metricsData[DEFAULT_METRIC]?.unit || ''}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
return `<div style="margin: 0px 0 0; position: relative; z-index: 99;width: fit-content;">
|
||||
<div style="padding: 8px 0;height: 100%;">
|
||||
<div style="font-size:12px;padding: 0 12px;color:#212529;line-height:20px;font-family: HelveticaNeue;">
|
||||
${date}
|
||||
</div>
|
||||
<div style="margin: 4px 0 0 0;padding: 0 12px;">
|
||||
${str}
|
||||
<div style="width: 100%; height: 1px; background: #EFF2F7;margin: 8px 0;"></div>
|
||||
${metricsData
|
||||
.map(({ key, value, unit }: { key: string; value: number; unit: string }) => {
|
||||
if (key === DEFAULT_METRIC) return '';
|
||||
return `
|
||||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||||
<span style="font-size:12px;color:#74788D;pointer-events:auto;margin-left:2px;line-height: 18px;font-family: HelveticaNeue;margin-right: 10px;">
|
||||
${key}
|
||||
</span>
|
||||
<span style="font-size:12px;color:#212529;pointer-events:auto;margin-left:2px;line-height: 18px;font-family: HelveticaNeue;">
|
||||
${parseFloat(Number(value).toFixed(3))}
|
||||
<span style="font-family: PingFangSC-Regular;color: #495057;">${unit}</span>
|
||||
</span>
|
||||
</div>
|
||||
`;
|
||||
})
|
||||
.join('')}
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
};
|
||||
|
||||
export const getChartConfig = (props: any) => {
|
||||
const { metricName, lineColor, isDefaultMetric = false } = props;
|
||||
return {
|
||||
option: getBasicChartConfig({
|
||||
// TODO: time 轴图表联动有问题,先切换为 category
|
||||
// xAxis: { type: 'time', boundaryGap: isDefaultMetric ? ['2%', '2%'] : ['5%', '5%'] },
|
||||
title: { show: false },
|
||||
legend: { show: false },
|
||||
grid: { top: 24, bottom: 12 },
|
||||
lineColor: [lineColor],
|
||||
tooltip: isDefaultMetric
|
||||
? {
|
||||
formatter: function (params: any) {
|
||||
let res = '';
|
||||
if (params != null && params.length > 0) {
|
||||
res += messagesInTooltipFormatter(moment(Number(params[0].axisValue)).format('YYYY-MM-DD HH:mm'), params);
|
||||
}
|
||||
return res;
|
||||
},
|
||||
}
|
||||
: {},
|
||||
}),
|
||||
seriesCallback: (lineList: { name: string; data: [number, string | number][] }[]) => {
|
||||
// 补充线条配置
|
||||
return lineList.map((line) => {
|
||||
return {
|
||||
...line,
|
||||
lineStyle: {
|
||||
width: 1,
|
||||
},
|
||||
symbol: 'emptyCircle',
|
||||
symbolSize: 4,
|
||||
// 面积图样式
|
||||
areaStyle: {
|
||||
color: lineColor,
|
||||
opacity: 0.06,
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,135 @@
|
||||
.cluster-container-border {
|
||||
background: #ffffff;
|
||||
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.01), 0 3px 6px 3px rgba(0, 0, 0, 0.01), 0 2px 6px 0 rgba(0, 0, 0, 0.03);
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.cluster-detail-container {
|
||||
width: 100%;
|
||||
|
||||
&-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 36px;
|
||||
|
||||
.refresh-icon-box {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
|
||||
.refresh-icon {
|
||||
font-size: 14px;
|
||||
color: #74788d;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: #21252904;
|
||||
|
||||
.refresh-icon {
|
||||
color: #495057;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-main {
|
||||
.header-chart-container {
|
||||
&-loading {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
width: 100%;
|
||||
height: 244px;
|
||||
margin-bottom: 12px;
|
||||
.cluster-container-border();
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: calc(100vh - 404px);
|
||||
min-height: 526px;
|
||||
|
||||
.multiple-chart-container {
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
.cluster-container-border();
|
||||
> div {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 16px;
|
||||
overflow: hidden auto;
|
||||
}
|
||||
&-loading {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.chart-box {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 244px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
|
||||
.expand-icon-box {
|
||||
position: absolute;
|
||||
z-index: 1000;
|
||||
top: 14px;
|
||||
right: 16px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
text-align: center;
|
||||
border-radius: 50%;
|
||||
transition: background-color 0.3s ease;
|
||||
|
||||
.expand-icon {
|
||||
color: #adb5bc;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: rgba(33, 37, 41, 0.06);
|
||||
.expand-icon {
|
||||
color: #74788d;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.config-change-records-container {
|
||||
width: 240px;
|
||||
height: 100%;
|
||||
margin-left: 12px;
|
||||
.cluster-container-border();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chart-box-title {
|
||||
padding: 18px 0 0 20px;
|
||||
font-family: @font-family-bold;
|
||||
line-height: 16px;
|
||||
.name {
|
||||
font-size: 14px;
|
||||
color: #212529;
|
||||
}
|
||||
.unit {
|
||||
font-size: 12px;
|
||||
color: #495057;
|
||||
}
|
||||
> span {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,452 @@
|
||||
import { Col, Row, SingleChart, IconFont, Utils, Modal, Spin, Empty, AppContainer, Tooltip } from 'knowdesign';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import api from '@src/api';
|
||||
import { getChartConfig } from './config';
|
||||
import './index.less';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import {
|
||||
MetricDefaultChartDataType,
|
||||
MetricChartDataType,
|
||||
formatChartData,
|
||||
supplementaryPoints,
|
||||
} from '@src/components/DashboardDragChart/config';
|
||||
import { MetricType } from '@src/api';
|
||||
import { getDataNumberUnit, getUnit } from '@src/constants/chartConfig';
|
||||
import SingleChartHeader, { KsHeaderOptions } from '@src/components/SingleChartHeader';
|
||||
import { MAX_TIME_RANGE_WITH_SMALL_POINT_INTERVAL } from '@src/constants/common';
|
||||
|
||||
type ChartFilterOptions = Omit<KsHeaderOptions, 'gridNum'>;
|
||||
interface MetricInfo {
|
||||
type: number;
|
||||
name: string;
|
||||
desc: string;
|
||||
set: boolean;
|
||||
support: boolean;
|
||||
}
|
||||
|
||||
interface MessagesInDefaultData {
|
||||
aggType: string | null;
|
||||
createTime: string | null;
|
||||
updateTime: string | null;
|
||||
timeStamp: number;
|
||||
values: {
|
||||
[metric: string]: string;
|
||||
};
|
||||
}
|
||||
|
||||
type MessagesInMetric = {
|
||||
name: 'MessagesIn';
|
||||
unit: string;
|
||||
data: (readonly [number, number | string, { key: string; value: number; unit: string }[]])[];
|
||||
};
|
||||
|
||||
const { EventBus } = Utils;
|
||||
const busInstance = new EventBus();
|
||||
|
||||
// 图表颜色定义 & 计算
|
||||
const CHART_LINE_COLORS = ['#556EE6', '#3991FF'];
|
||||
const calculateChartColor = (i: number) => {
|
||||
const isEvenRow = ((i / 2) | 0) % 2;
|
||||
const isEvenCol = i % 2;
|
||||
return CHART_LINE_COLORS[isEvenRow ^ isEvenCol];
|
||||
};
|
||||
|
||||
const DEFAULT_METRIC = 'MessagesIn';
|
||||
// 默认指标图表固定需要获取展示的指标项
|
||||
const DEFUALT_METRIC_NEED_METRICS = [DEFAULT_METRIC, 'TotalLogSize', 'TotalProduceRequests', 'Topics', 'Partitions'];
|
||||
|
||||
const DetailChart = (props: { children: JSX.Element }): JSX.Element => {
|
||||
const [global] = AppContainer.useGlobalValue();
|
||||
const { clusterId } = useParams<{ clusterId: string }>();
|
||||
const [metricList, setMetricList] = useState<MetricInfo[]>([]); // 指标列表
|
||||
const [selectedMetricNames, setSelectedMetricNames] = useState<(string | number)[]>([]); // 默认选中的指标的列表
|
||||
const [metricDataList, setMetricDataList] = useState<any>([]);
|
||||
const [messagesInMetricData, setMessagesInMetricData] = useState<MessagesInMetric>({
|
||||
name: 'MessagesIn',
|
||||
unit: '',
|
||||
data: [],
|
||||
});
|
||||
const [curHeaderOptions, setCurHeaderOptions] = useState<ChartFilterOptions>();
|
||||
const [defaultChartLoading, setDefaultChartLoading] = useState<boolean>(true);
|
||||
const [chartLoading, setChartLoading] = useState<boolean>(true);
|
||||
const [showChartDetailModal, setShowChartDetailModal] = useState<boolean>(false);
|
||||
const [chartDetail, setChartDetail] = useState<any>();
|
||||
const curFetchingTimestamp = useRef({
|
||||
messagesIn: 0,
|
||||
other: 0,
|
||||
});
|
||||
|
||||
// 筛选项变化或者点击刷新按钮
|
||||
const ksHeaderChange = (ksOptions: KsHeaderOptions) => {
|
||||
// 如果为相对时间,则当前时间减去 1 分钟,避免最近一分钟的数据还没采集到时前端多补一个点
|
||||
if (ksOptions.isRelativeRangeTime) {
|
||||
ksOptions.rangeTime = ksOptions.rangeTime.map((timestamp) => timestamp - 60 * 1000) as [number, number];
|
||||
}
|
||||
setCurHeaderOptions({
|
||||
isRelativeRangeTime: ksOptions.isRelativeRangeTime,
|
||||
isAutoReload: ksOptions.isAutoReload,
|
||||
rangeTime: ksOptions.rangeTime,
|
||||
});
|
||||
};
|
||||
|
||||
// 获取指标列表
|
||||
const getMetricList = () => {
|
||||
Utils.request(api.getDashboardMetricList(clusterId, MetricType.Cluster)).then((res: MetricInfo[] | null) => {
|
||||
if (!res) return;
|
||||
const showMetrics = res.filter((metric) => metric.support);
|
||||
const selectedMetrics = showMetrics.filter((metric) => metric.set).map((metric) => metric.name);
|
||||
!selectedMetrics.includes(DEFAULT_METRIC) && selectedMetrics.push(DEFAULT_METRIC);
|
||||
setMetricList(showMetrics);
|
||||
setSelectedMetricNames(selectedMetrics);
|
||||
});
|
||||
};
|
||||
|
||||
// 更新指标
|
||||
const updateMetricList = (metricsSet: { [name: string]: boolean }) => {
|
||||
return Utils.request(api.getDashboardMetricList(clusterId, MetricType.Cluster), {
|
||||
method: 'POST',
|
||||
data: {
|
||||
metricsSet,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// 指标选中项更新回调
|
||||
const indicatorChangeCallback = (newMetricNames: (string | number)[]) => {
|
||||
const updateMetrics: { [name: string]: boolean } = {};
|
||||
// 需要选中的指标
|
||||
newMetricNames.forEach((name) => !selectedMetricNames.includes(name) && (updateMetrics[name] = true));
|
||||
// 取消选中的指标
|
||||
selectedMetricNames.forEach((name) => !newMetricNames.includes(name) && (updateMetrics[name] = false));
|
||||
|
||||
const requestPromise = Object.keys(updateMetrics).length ? updateMetricList(updateMetrics) : Promise.resolve();
|
||||
requestPromise.then(
|
||||
() => getMetricList(),
|
||||
() => getMetricList()
|
||||
);
|
||||
|
||||
return requestPromise;
|
||||
};
|
||||
|
||||
// 获取 metric 列表的图表数据
|
||||
const getMetricData = () => {
|
||||
if (!selectedMetricNames.length) return;
|
||||
!curHeaderOptions.isAutoReload && setChartLoading(true);
|
||||
const [startTime, endTime] = curHeaderOptions.rangeTime;
|
||||
|
||||
const curTimestamp = Date.now();
|
||||
curFetchingTimestamp.current = {
|
||||
...curFetchingTimestamp.current,
|
||||
messagesIn: curTimestamp,
|
||||
};
|
||||
Utils.request(api.getClusterMetricDataList(), {
|
||||
method: 'POST',
|
||||
data: {
|
||||
startTime,
|
||||
endTime,
|
||||
clusterPhyIds: [clusterId],
|
||||
metricsNames: selectedMetricNames.filter((name) => name !== DEFAULT_METRIC),
|
||||
},
|
||||
}).then(
|
||||
(res: MetricDefaultChartDataType[]) => {
|
||||
// 如果当前请求不是最新请求,则不做任何操作
|
||||
if (curFetchingTimestamp.current.messagesIn !== curTimestamp) {
|
||||
return;
|
||||
}
|
||||
|
||||
const supplementaryInterval = (endTime - startTime > MAX_TIME_RANGE_WITH_SMALL_POINT_INTERVAL ? 10 : 1) * 60 * 1000;
|
||||
const formattedMetricData: MetricChartDataType[] = formatChartData(
|
||||
res,
|
||||
global.getMetricDefine || {},
|
||||
MetricType.Cluster,
|
||||
curHeaderOptions.rangeTime,
|
||||
supplementaryInterval
|
||||
);
|
||||
formattedMetricData.forEach((data) => (data.metricLines[0].name = data.metricName));
|
||||
setMetricDataList(formattedMetricData);
|
||||
setChartLoading(false);
|
||||
},
|
||||
() => setChartLoading(false)
|
||||
);
|
||||
};
|
||||
|
||||
// 获取默认展示指标的图表数据
|
||||
const getDefaultMetricData = () => {
|
||||
!curHeaderOptions.isAutoReload && setDefaultChartLoading(true);
|
||||
|
||||
const curTimestamp = Date.now();
|
||||
curFetchingTimestamp.current = {
|
||||
...curFetchingTimestamp.current,
|
||||
other: curTimestamp,
|
||||
};
|
||||
Utils.request(api.getClusterDefaultMetricData(), {
|
||||
method: 'POST',
|
||||
data: {
|
||||
startTime: curHeaderOptions.rangeTime[0],
|
||||
endTime: curHeaderOptions.rangeTime[1],
|
||||
clusterPhyIds: [clusterId],
|
||||
metricsNames: DEFUALT_METRIC_NEED_METRICS,
|
||||
},
|
||||
}).then(
|
||||
(res: MessagesInDefaultData[]) => {
|
||||
// 如果当前请求不是最新请求,则不做任何操作
|
||||
if (curFetchingTimestamp.current.other !== curTimestamp) {
|
||||
return;
|
||||
}
|
||||
// TODO: 这里直接将指标数据放到数组第三项中,之后可以尝试优化,优化需要注意 tooltipFormatter 函数也要修改
|
||||
let maxValue = -1;
|
||||
const result = res.map((item) => {
|
||||
const { timeStamp, values } = item;
|
||||
let parsedValue: string | number = Number(values.MessagesIn);
|
||||
if (Number.isNaN(parsedValue)) {
|
||||
parsedValue = values.MessagesIn;
|
||||
} else {
|
||||
if (maxValue < parsedValue) maxValue = parsedValue;
|
||||
}
|
||||
const valuesWithUnit = Object.entries(values).map(([key, value]) => {
|
||||
let valueWithUnit = Number(value);
|
||||
let unit = ((global.getMetricDefine && global.getMetricDefine(MetricType.Cluster, key)?.unit) || '') as string;
|
||||
if (unit.toLowerCase().includes('byte')) {
|
||||
const [unitName, unitSize]: [string, number] = getUnit(Number(value));
|
||||
unit = unit.toLowerCase().replace('byte', unitName);
|
||||
valueWithUnit /= unitSize;
|
||||
}
|
||||
const returnValue = {
|
||||
key,
|
||||
value: valueWithUnit,
|
||||
unit,
|
||||
};
|
||||
return returnValue;
|
||||
});
|
||||
return [timeStamp, values.MessagesIn || '0', valuesWithUnit] as [number, number | string, typeof valuesWithUnit];
|
||||
});
|
||||
result.sort((a, b) => (a[0] as number) - (b[0] as number));
|
||||
const line = {
|
||||
name: 'MessagesIn' as const,
|
||||
unit: global.getMetricDefine(MetricType.Cluster, 'MessagesIn')?.unit,
|
||||
data: result as any,
|
||||
};
|
||||
if (maxValue > 0) {
|
||||
const [unitName, unitSize]: [string, number] = getDataNumberUnit(maxValue);
|
||||
line.unit = `${unitName}${line.unit}`;
|
||||
result.forEach((point) => ((point[1] as number) /= unitSize));
|
||||
}
|
||||
|
||||
// 补充缺少的图表点
|
||||
const extraMetrics = result[0][2].map((info) => ({
|
||||
...info,
|
||||
value: 0,
|
||||
}));
|
||||
const supplementaryInterval =
|
||||
(curHeaderOptions.rangeTime[1] - curHeaderOptions.rangeTime[0] > MAX_TIME_RANGE_WITH_SMALL_POINT_INTERVAL ? 10 : 1) * 60 * 1000;
|
||||
supplementaryPoints([line], curHeaderOptions.rangeTime, supplementaryInterval, (point) => {
|
||||
point.push(extraMetrics as any);
|
||||
return point;
|
||||
});
|
||||
|
||||
setMessagesInMetricData(line);
|
||||
setDefaultChartLoading(false);
|
||||
},
|
||||
() => setDefaultChartLoading(false)
|
||||
);
|
||||
};
|
||||
|
||||
// 监听盒子宽度变化,重置图表宽度
|
||||
const observeDashboardWidthChange = () => {
|
||||
const targetNode = document.getElementsByClassName('dcd-two-columns-layout-sider-footer')[0];
|
||||
targetNode && targetNode.addEventListener('click', () => busInstance.emit('chartResize'));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
getMetricData();
|
||||
}, [selectedMetricNames]);
|
||||
|
||||
useEffect(() => {
|
||||
if (curHeaderOptions && curHeaderOptions?.rangeTime.join(',') !== '0,0') {
|
||||
getDefaultMetricData();
|
||||
getMetricData();
|
||||
}
|
||||
}, [curHeaderOptions]);
|
||||
|
||||
useEffect(() => {
|
||||
getMetricList();
|
||||
setTimeout(() => observeDashboardWidthChange());
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="cluster-detail-container">
|
||||
<SingleChartHeader
|
||||
onChange={ksHeaderChange}
|
||||
hideNodeScope={true}
|
||||
hideGridSelect={true}
|
||||
indicatorSelectModule={{
|
||||
hide: false,
|
||||
metricType: MetricType.Cluster,
|
||||
tableData: metricList,
|
||||
selectedRows: selectedMetricNames,
|
||||
checkboxProps: (record: MetricInfo) => {
|
||||
return record.name === DEFAULT_METRIC
|
||||
? {
|
||||
disabled: true,
|
||||
}
|
||||
: {};
|
||||
},
|
||||
submitCallback: indicatorChangeCallback,
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="cluster-detail-container-main">
|
||||
{/* MessageIn 图表 */}
|
||||
<div className={`header-chart-container ${!messagesInMetricData.data.length ? 'header-chart-container-loading' : ''}`}>
|
||||
<Spin spinning={defaultChartLoading}>
|
||||
{/* TODO: 暂时通过判断是否有图表数据来修复,有时间可以查找下宽度溢出的原因 */}
|
||||
{messagesInMetricData.data.length ? (
|
||||
<>
|
||||
<div className="chart-box-title">
|
||||
<Tooltip
|
||||
placement="topLeft"
|
||||
title={() => {
|
||||
let content = '';
|
||||
const metricDefine = global.getMetricDefine(MetricType.Cluster, messagesInMetricData.name);
|
||||
if (metricDefine) {
|
||||
content = metricDefine.desc;
|
||||
}
|
||||
return content;
|
||||
}}
|
||||
>
|
||||
<span>
|
||||
<span className="name">{messagesInMetricData.name}</span>
|
||||
<span className="unit">({messagesInMetricData.unit})</span>
|
||||
</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<SingleChart
|
||||
chartKey="messagesIn"
|
||||
chartTypeProp="line"
|
||||
showHeader={false}
|
||||
wrapStyle={{
|
||||
width: 'auto',
|
||||
height: 210,
|
||||
}}
|
||||
connectEventName="clusterChart"
|
||||
eventBus={busInstance}
|
||||
propChartData={[messagesInMetricData]}
|
||||
{...getChartConfig({
|
||||
// metricName: `${messagesInMetricData.name}{unit|(${messagesInMetricData.unit})}`,
|
||||
lineColor: CHART_LINE_COLORS[0],
|
||||
isDefaultMetric: true,
|
||||
})}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
</Spin>
|
||||
</div>
|
||||
|
||||
<div className="content">
|
||||
{/* 其余指标图表 */}
|
||||
<div className="multiple-chart-container">
|
||||
<div className={!metricDataList.length ? 'multiple-chart-container-loading' : ''}>
|
||||
<Spin spinning={chartLoading}>
|
||||
<Row gutter={[16, 16]}>
|
||||
{metricDataList.length ? (
|
||||
metricDataList.map((data: any, i: number) => {
|
||||
const { metricName, metricUnit, metricLines } = data;
|
||||
return (
|
||||
<Col key={metricName} span={12}>
|
||||
<div className="chart-box">
|
||||
<div className="chart-box-title">
|
||||
<Tooltip
|
||||
placement="topLeft"
|
||||
title={() => {
|
||||
let content = '';
|
||||
const metricDefine = global.getMetricDefine(MetricType.Cluster, 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={() => {
|
||||
setChartDetail(data);
|
||||
setShowChartDetailModal(true);
|
||||
}}
|
||||
>
|
||||
<IconFont type="icon-chuangkoufangda" className="expand-icon" />
|
||||
</div>
|
||||
<SingleChart
|
||||
chartKey={metricName}
|
||||
showHeader={false}
|
||||
chartTypeProp="line"
|
||||
wrapStyle={{
|
||||
width: 'auto',
|
||||
height: 210,
|
||||
}}
|
||||
connectEventName="clusterChart"
|
||||
eventBus={busInstance}
|
||||
propChartData={metricLines}
|
||||
{...getChartConfig({
|
||||
metricName: `${metricName}{unit|(${metricUnit})}`,
|
||||
lineColor: calculateChartColor(i),
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
</Col>
|
||||
);
|
||||
})
|
||||
) : chartLoading ? (
|
||||
<></>
|
||||
) : (
|
||||
<Empty description="请先选择指标或刷新" style={{ width: '100%', height: '100%' }} />
|
||||
)}
|
||||
</Row>
|
||||
</Spin>
|
||||
</div>
|
||||
</div>
|
||||
{/* 历史配置变更记录内容 */}
|
||||
<div className="config-change-records-container">{props.children}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 图表详情 */}
|
||||
<Modal
|
||||
width={1080}
|
||||
visible={showChartDetailModal}
|
||||
centered={true}
|
||||
footer={null}
|
||||
closable={false}
|
||||
onCancel={() => setShowChartDetailModal(false)}
|
||||
>
|
||||
<div className="chart-detail-modal-container">
|
||||
<div className="expand-icon-box" onClick={() => setShowChartDetailModal(false)}>
|
||||
<IconFont type="icon-chuangkousuoxiao" className="expand-icon" />
|
||||
</div>
|
||||
{chartDetail && (
|
||||
<SingleChart
|
||||
chartTypeProp="line"
|
||||
wrapStyle={{
|
||||
width: 'auto',
|
||||
height: 462,
|
||||
}}
|
||||
propChartData={chartDetail.metricLines}
|
||||
{...getChartConfig({
|
||||
metricName: `${chartDetail.metricName}{unit|(${chartDetail.metricUnit})}`,
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DetailChart;
|
||||
@@ -0,0 +1,135 @@
|
||||
/* eslint-disable react/display-name */
|
||||
import { Button, Divider, Drawer, Form, message, ProTable, Table, Utils } from 'knowdesign';
|
||||
import React, { useState } from 'react';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { getHealthySettingColumn } from './config';
|
||||
import API from '../../api';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
const HealthySetting = React.forwardRef((props: any, ref): JSX.Element => {
|
||||
const intl = useIntl();
|
||||
const [form] = Form.useForm();
|
||||
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [initialValues, setInitialValues] = useState({} as any);
|
||||
|
||||
const [data, setData] = React.useState([]);
|
||||
const { clusterId } = useParams<{ clusterId: string }>();
|
||||
|
||||
React.useImperativeHandle(ref, () => ({
|
||||
setVisible,
|
||||
getHealthconfig,
|
||||
}));
|
||||
|
||||
const getHealthconfig = () => {
|
||||
return Utils.request(API.getClusterHealthyConfigs(+clusterId)).then((res: any) => {
|
||||
const values = {} as any;
|
||||
|
||||
try {
|
||||
res = res.map((item: any) => {
|
||||
const itemValue = JSON.parse(item.value);
|
||||
item.weight = itemValue?.weight;
|
||||
|
||||
item.configItemName =
|
||||
item.configItem.indexOf('Group Re-Balance') > -1
|
||||
? 'ReBalance'
|
||||
: item.configItem.includes('副本未同步')
|
||||
? 'UNDER_REPLICA'
|
||||
: item.configItem;
|
||||
|
||||
values[`weight_${item.configItemName}`] = itemValue?.weight;
|
||||
values[`value_${item.configItemName}`] = itemValue?.value;
|
||||
values[`latestMinutes_${item.configItemName}`] = itemValue?.latestMinutes;
|
||||
values[`detectedTimes_${item.configItemName}`] = itemValue?.detectedTimes;
|
||||
return item;
|
||||
});
|
||||
} catch (err) {
|
||||
//
|
||||
}
|
||||
const formItemsValue = {
|
||||
...initialValues,
|
||||
...values,
|
||||
};
|
||||
setInitialValues(formItemsValue);
|
||||
form.setFieldsValue(formItemsValue);
|
||||
setData(res);
|
||||
});
|
||||
};
|
||||
|
||||
const onCancel = () => {
|
||||
form.resetFields();
|
||||
setVisible(false);
|
||||
};
|
||||
|
||||
const onSubmit = () => {
|
||||
form.validateFields().then((res) => {
|
||||
const params = [] as any;
|
||||
data.map((item) => {
|
||||
params.push({
|
||||
clusterId: +clusterId,
|
||||
value: JSON.stringify({
|
||||
clusterPhyId: +clusterId,
|
||||
detectedTimes: res[`detectedTimes_${item.configItemName}`],
|
||||
latestMinutes: res[`latestMinutes_${item.configItemName}`],
|
||||
weight: res[`weight_${item.configItemName}`],
|
||||
value: item.configItemName === 'Controller' ? 1 : res[`value_${item.configItemName}`],
|
||||
}),
|
||||
valueGroup: item.configGroup,
|
||||
valueName: item.configName,
|
||||
});
|
||||
});
|
||||
Utils.put(API.putPlatformConfigs, params)
|
||||
.then((res) => {
|
||||
message.success('修改成功');
|
||||
form.resetFields();
|
||||
setVisible(false);
|
||||
})
|
||||
.catch((err) => {
|
||||
message.error('操作失败' + err.message);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const onHandleValuesChange = (value: any, allValues: any) => {
|
||||
//
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Drawer
|
||||
className="drawer-content healthy-drawer-content"
|
||||
onClose={onCancel}
|
||||
maskClosable={false}
|
||||
extra={
|
||||
<div className="operate-wrap">
|
||||
<Button size="small" style={{ marginRight: 8 }} onClick={onCancel}>
|
||||
取消
|
||||
</Button>
|
||||
<Button size="small" type="primary" onClick={onSubmit}>
|
||||
确定
|
||||
</Button>
|
||||
<Divider type="vertical" />
|
||||
</div>
|
||||
}
|
||||
title={intl.formatMessage({ id: 'healthy.setting' })}
|
||||
visible={visible}
|
||||
placement="right"
|
||||
width={1080}
|
||||
>
|
||||
<Form form={form} layout="vertical" onValuesChange={onHandleValuesChange}>
|
||||
<ProTable
|
||||
tableProps={{
|
||||
rowKey: 'dimensionCode',
|
||||
showHeader: false,
|
||||
dataSource: data,
|
||||
columns: getHealthySettingColumn(form, data, clusterId),
|
||||
noPagination: true,
|
||||
}}
|
||||
/>
|
||||
</Form>
|
||||
</Drawer>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
export default HealthySetting;
|
||||
@@ -0,0 +1,253 @@
|
||||
import { AppContainer, Divider, IconFont, Progress, Tooltip, Utils } from 'knowdesign';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import AccessClusters from '../MutliClusterPage/AccessCluster';
|
||||
import './index.less';
|
||||
import API from '../../api';
|
||||
import HealthySetting from './HealthySetting';
|
||||
import CheckDetail from './CheckDetail';
|
||||
import { Link, useHistory, useParams } from 'react-router-dom';
|
||||
import { getHealthClassName, getHealthProcessColor, getHealthState, getHealthText, renderToolTipValue } from './config';
|
||||
import { ClustersPermissionMap } from '../CommonConfig';
|
||||
|
||||
const LeftSider = () => {
|
||||
const [global] = AppContainer.useGlobalValue();
|
||||
const history = useHistory();
|
||||
const [kafkaVersion, setKafkaVersion] = useState({});
|
||||
const [clusterInfo, setClusterInfo] = useState({} as any);
|
||||
const [loading, setLoading] = React.useState(false);
|
||||
const [clusterMetrics, setClusterMetrics] = useState({} as any);
|
||||
const [brokerState, setBrokerState] = useState({} as any);
|
||||
|
||||
const detailDrawerRef: any = React.createRef();
|
||||
const healthyDrawerRef: any = React.createRef();
|
||||
const { clusterId } = useParams<{ clusterId: string }>();
|
||||
const [visible, setVisible] = React.useState(false);
|
||||
|
||||
const getSupportKafkaVersion = () => {
|
||||
Utils.request(API.supportKafkaVersion).then((res) => {
|
||||
setKafkaVersion(res || {});
|
||||
});
|
||||
};
|
||||
|
||||
const getBrokerState = () => {
|
||||
return Utils.request(API.getBrokersState(clusterId)).then((res) => {
|
||||
setBrokerState(res);
|
||||
});
|
||||
};
|
||||
|
||||
const getPhyClusterMetrics = () => {
|
||||
return Utils.post(
|
||||
API.getPhyClusterMetrics(+clusterId),
|
||||
[
|
||||
'HealthScore',
|
||||
'HealthCheckPassed',
|
||||
'HealthCheckTotal',
|
||||
'Topics',
|
||||
'PartitionURP',
|
||||
'PartitionNoLeader', // > 0 error
|
||||
'PartitionMinISR_S', // > 0 error
|
||||
'Groups',
|
||||
'GroupDeads',
|
||||
'Alive',
|
||||
].concat(process.env.BUSINESS_VERSION ? ['LoadReBalanceEnable', 'LoadReBalanceNwIn', 'LoadReBalanceNwOut', 'LoadReBalanceDisk'] : [])
|
||||
).then((res: any) => {
|
||||
setClusterMetrics(res?.metrics || {});
|
||||
});
|
||||
};
|
||||
|
||||
const getPhyClusterInfo = () => {
|
||||
setLoading(true);
|
||||
Utils.request(API.getPhyClusterBasic(+clusterId))
|
||||
.then((res: any) => {
|
||||
let jmxProperties = null;
|
||||
try {
|
||||
jmxProperties = JSON.parse(res?.jmxProperties);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
|
||||
// 转化值对应成表单值
|
||||
if (jmxProperties?.openSSL) {
|
||||
jmxProperties.security = 'Password';
|
||||
}
|
||||
|
||||
if (jmxProperties) {
|
||||
res = Object.assign({}, res || {}, jmxProperties);
|
||||
}
|
||||
setClusterInfo(res);
|
||||
setLoading(false);
|
||||
})
|
||||
.catch((err) => {
|
||||
setLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
getBrokerState();
|
||||
getPhyClusterMetrics();
|
||||
getSupportKafkaVersion();
|
||||
getPhyClusterInfo();
|
||||
}, []);
|
||||
|
||||
const renderIcon = (type: string) => {
|
||||
return (
|
||||
<span className={`icon`}>
|
||||
<IconFont type={type === 'green' ? 'icon-zhengchang' : type === 'warning' ? 'icon-yichang' : 'icon-warning'} />
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="left-sider">
|
||||
<div className="state-card">
|
||||
<Progress
|
||||
type="circle"
|
||||
status="active"
|
||||
strokeWidth={4}
|
||||
strokeColor={getHealthProcessColor(clusterMetrics?.HealthScore, clusterMetrics?.Alive)}
|
||||
percent={clusterMetrics?.HealthScore ?? '-'}
|
||||
format={() => (
|
||||
<div className={`healthy-percent ${getHealthClassName(clusterMetrics?.HealthScore, clusterMetrics?.Alive)}`}>
|
||||
{getHealthText(clusterMetrics?.HealthScore, clusterMetrics?.Alive)}
|
||||
</div>
|
||||
)}
|
||||
width={75}
|
||||
/>
|
||||
<div className="healthy-state">
|
||||
<div className="healthy-state-status">
|
||||
<span>{getHealthState(clusterMetrics?.HealthScore, clusterMetrics?.Alive)}</span>
|
||||
{/* 健康分设置 */}
|
||||
{global.hasPermission && global.hasPermission(ClustersPermissionMap.CLUSTER_CHANGE_HEALTHY) ? (
|
||||
<span
|
||||
className="icon"
|
||||
onClick={() => {
|
||||
healthyDrawerRef.current.getHealthconfig().then(() => {
|
||||
healthyDrawerRef.current.setVisible(true);
|
||||
});
|
||||
}}
|
||||
>
|
||||
<IconFont type="icon-shezhi" size={13} />
|
||||
</span>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<span className="healthy-state-num">
|
||||
{clusterMetrics?.HealthCheckPassed}/{clusterMetrics?.HealthCheckTotal}
|
||||
</span>
|
||||
{/* 健康度详情 */}
|
||||
<span
|
||||
className="healthy-state-btn"
|
||||
onClick={() => {
|
||||
detailDrawerRef.current.setVisible(true);
|
||||
}}
|
||||
>
|
||||
查看详情
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Divider />
|
||||
<div className="title">
|
||||
<div className="name">{renderToolTipValue(clusterInfo?.name, 35)}</div>
|
||||
{global.hasPermission && global.hasPermission(ClustersPermissionMap.CLUSTER_CHANGE_INFO) ? (
|
||||
<div className="edit-icon-box" onClick={() => setVisible(true)}>
|
||||
<IconFont className="edit-icon" type="icon-bianji2" />
|
||||
</div>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</div>
|
||||
<div className="tag-panel">
|
||||
<div className="tag default">{clusterInfo?.kafkaVersion ?? '-'}</div>
|
||||
{clusterMetrics?.LoadReBalanceEnable !== undefined &&
|
||||
[
|
||||
['BytesIn', clusterMetrics?.LoadReBalanceEnable && clusterMetrics?.LoadReBalanceNwIn],
|
||||
['BytesOut', clusterMetrics?.LoadReBalanceEnable && clusterMetrics?.LoadReBalanceNwOut],
|
||||
['Disk', clusterMetrics?.LoadReBalanceEnable && clusterMetrics?.LoadReBalanceDisk],
|
||||
].map(([name, isBalanced]) => {
|
||||
return isBalanced ? (
|
||||
<div className="tag balanced">{name} 已均衡</div>
|
||||
) : clusterMetrics?.LoadReBalanceEnable ? (
|
||||
<div className="tag unbalanced">{name} 未均衡</div>
|
||||
) : (
|
||||
<Tooltip
|
||||
title={
|
||||
<span>
|
||||
尚未开启 {name} 均衡策略,
|
||||
<Link to={`/cluster/${clusterId}/cluster/balance`}>前往开启</Link>
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<div className="tag unbalanced">{name} 未均衡</div>
|
||||
</Tooltip>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="desc">{renderToolTipValue(clusterInfo?.description, 35)}</div>
|
||||
<div className="card-panel">
|
||||
<div className="card-item" onClick={() => history.push(`/cluster/${clusterId}/broker`)}>
|
||||
<div className="title">Brokers总数</div>
|
||||
<div className="count">
|
||||
<span className="num">{brokerState?.brokerCount ?? '-'}</span>
|
||||
<span className="unit">个</span>
|
||||
</div>
|
||||
<div className="metric">
|
||||
<span className="type">Controller</span>
|
||||
{renderIcon(brokerState?.kafkaControllerAlive ? 'green' : 'red')}
|
||||
</div>
|
||||
<div className="metric">
|
||||
<span className="type">Similar Config</span>
|
||||
{renderIcon(brokerState?.configSimilar ? 'green' : 'warning')}
|
||||
</div>
|
||||
</div>
|
||||
<div className="card-item" onClick={() => history.push(`/cluster/${clusterId}/topic`)}>
|
||||
<div className="title">Topics总数</div>
|
||||
<div className="count">
|
||||
<span className="num">{clusterMetrics?.Topics ?? '-'}</span>
|
||||
<span className="unit">个</span>
|
||||
</div>
|
||||
<div className="metric">
|
||||
<span className="type">No leader</span>
|
||||
{renderIcon(clusterMetrics?.PartitionNoLeader === 0 ? 'green' : 'red')}
|
||||
</div>
|
||||
<div className="metric">
|
||||
<span className="type">{'< Min ISR'}</span>
|
||||
{renderIcon(clusterMetrics?.PartitionMinISR_S === 0 ? 'green' : 'red')}
|
||||
</div>
|
||||
<div className="metric">
|
||||
<span className="type">URP</span>
|
||||
{renderIcon(clusterMetrics?.PartitionURP === 0 ? 'green' : 'red')}
|
||||
</div>
|
||||
</div>
|
||||
<div className="card-item" onClick={() => history.push(`/cluster/${clusterId}/consumers`)}>
|
||||
<div className="title">ConsumerGroup总数</div>
|
||||
<div className="count">
|
||||
<span className="num">{clusterMetrics?.Groups ?? '-'}</span>
|
||||
<span className="unit">个</span>
|
||||
</div>
|
||||
<div className="metric">
|
||||
<span className="type">Dead</span>
|
||||
{renderIcon(clusterMetrics?.GroupDeads === 0 ? 'green' : 'red')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<AccessClusters
|
||||
visible={visible}
|
||||
setVisible={setVisible}
|
||||
title={'edit.cluster'}
|
||||
infoLoading={loading}
|
||||
afterSubmitSuccess={getPhyClusterInfo}
|
||||
clusterInfo={clusterInfo}
|
||||
kafkaVersion={Object.keys(kafkaVersion)}
|
||||
/>
|
||||
<HealthySetting ref={healthyDrawerRef} />
|
||||
<CheckDetail ref={detailDrawerRef} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default LeftSider;
|
||||
@@ -0,0 +1,334 @@
|
||||
import moment from 'moment';
|
||||
import React from 'react';
|
||||
import { timeFormat } from '../../constants/common';
|
||||
import TagsWithHide from '../../components/TagsWithHide/index';
|
||||
import { Form, IconFont, InputNumber, Tooltip } from 'knowdesign';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { systemKey } from '../../constants/menu';
|
||||
|
||||
const statusTxtEmojiMap = {
|
||||
success: {
|
||||
emoji: '👍',
|
||||
txt: '优异',
|
||||
},
|
||||
normal: {
|
||||
emoji: '😊',
|
||||
txt: '正常',
|
||||
},
|
||||
exception: {
|
||||
emoji: '👻',
|
||||
txt: '异常',
|
||||
},
|
||||
};
|
||||
|
||||
export const dimensionMap = {
|
||||
'-1': {
|
||||
label: 'Unknown',
|
||||
href: ``,
|
||||
},
|
||||
0: {
|
||||
label: 'Cluster',
|
||||
href: ``,
|
||||
},
|
||||
1: {
|
||||
label: 'Broker',
|
||||
href: `/broker`,
|
||||
},
|
||||
2: {
|
||||
label: 'Topic',
|
||||
href: `/topic`,
|
||||
},
|
||||
3: {
|
||||
label: 'ConsumerGroup',
|
||||
href: `/consumers`,
|
||||
},
|
||||
} as any;
|
||||
|
||||
export const getHealthState = (value: number, down: number) => {
|
||||
if (value === undefined) return '-';
|
||||
const progressStatus = +down <= 0 ? 'exception' : value >= 90 ? 'success' : 'normal';
|
||||
return (
|
||||
<span>
|
||||
{statusTxtEmojiMap[progressStatus].emoji} 集群状态{statusTxtEmojiMap[progressStatus].txt}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
export const getHealthText = (value: number, down: number) => {
|
||||
return +down <= 0 ? 'Down' : value ? value.toFixed(0) : '-';
|
||||
};
|
||||
|
||||
export const getHealthProcessColor = (value: number, down: number) => {
|
||||
return +down <= 0 ? '#FF7066' : +value < 90 ? '#556EE6' : '#00C0A2';
|
||||
};
|
||||
|
||||
export const getHealthClassName = (value: number, down: number) => {
|
||||
return +down <= 0 ? 'down' : value === undefined ? 'no-info' : +value < 90 ? 'less-90' : '';
|
||||
};
|
||||
|
||||
export const renderToolTipValue = (value: string, num: number) => {
|
||||
return (
|
||||
<>
|
||||
{value?.length > num ? (
|
||||
<>
|
||||
<Tooltip placement="topLeft" title={value}>
|
||||
{value ?? '-'}
|
||||
</Tooltip>
|
||||
</>
|
||||
) : (
|
||||
value ?? '-'
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const getDetailColumn = (clusterId: number) => [
|
||||
{
|
||||
title: '检查模块',
|
||||
dataIndex: 'dimension',
|
||||
// eslint-disable-next-line react/display-name
|
||||
render: (text: number) => {
|
||||
if (text === 0 || text === -1) return dimensionMap[text]?.label;
|
||||
return <Link to={`/${systemKey}/${clusterId}${dimensionMap[text]?.href}`}>{dimensionMap[text]?.label}</Link>;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '检查项',
|
||||
dataIndex: 'checkConfig',
|
||||
render(config: any, record: any) {
|
||||
const valueGroup = JSON.parse(config.value);
|
||||
if (record.configItem === 'Controller') {
|
||||
return '集群 Controller 数等于 1';
|
||||
} else if (record.configItem === 'RequestQueueSize') {
|
||||
return `Broker-RequestQueueSize 小于 ${valueGroup.value}`;
|
||||
} else if (record.configItem === 'NoLeader') {
|
||||
return `Topic 无 Leader 数小于 ${valueGroup.value}`;
|
||||
} else if (record.configItem === 'NetworkProcessorAvgIdlePercent') {
|
||||
return `Broker-NetworkProcessorAvgIdlePercent 的 idle 大于 ${valueGroup.value}%`;
|
||||
} else if (record.configItem === 'UnderReplicaTooLong') {
|
||||
return `Topic 小于 ${parseFloat(((valueGroup.detectedTimes / valueGroup.latestMinutes) * 100).toFixed(2))}% 周期处于未同步状态`;
|
||||
} else if (record.configItem === 'Group Re-Balance') {
|
||||
return `Consumer Group 小于 ${parseFloat(
|
||||
((valueGroup.detectedTimes / valueGroup.latestMinutes) * 100).toFixed(2)
|
||||
)}% 周期处于 Re-balance 状态`;
|
||||
}
|
||||
|
||||
return <></>;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '权重',
|
||||
dataIndex: 'weightPercent',
|
||||
width: 80,
|
||||
render(value: number) {
|
||||
return `${value}%`;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '得分',
|
||||
dataIndex: 'score',
|
||||
width: 60,
|
||||
},
|
||||
{
|
||||
title: '检查时间',
|
||||
width: 190,
|
||||
dataIndex: 'updateTime',
|
||||
render: (text: string) => {
|
||||
return moment(text).format(timeFormat);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '检查结果',
|
||||
dataIndex: 'passed',
|
||||
width: 280,
|
||||
// eslint-disable-next-line react/display-name
|
||||
render: (passed: boolean, record: any) => {
|
||||
if (passed) {
|
||||
return (
|
||||
<>
|
||||
<IconFont type="icon-zhengchang" />
|
||||
<span style={{ marginLeft: 4 }}>通过</span>
|
||||
</>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div style={{ display: 'flex', alignItems: 'center', width: '240px' }}>
|
||||
<IconFont type="icon-yichang" />
|
||||
<div style={{ marginLeft: 4, marginRight: 6, flexShrink: 0 }}>未通过</div>
|
||||
<TagsWithHide list={record.notPassedResNameList || []} expandTagContent="更多" />
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export const getHealthySettingColumn = (form: any, data: any, clusterId: string) =>
|
||||
[
|
||||
{
|
||||
title: '检查模块',
|
||||
dataIndex: 'dimensionCode',
|
||||
// eslint-disable-next-line react/display-name
|
||||
render: (text: number) => {
|
||||
if (text === 0 || text === -1) return dimensionMap[text]?.label;
|
||||
return <Link to={`/${systemKey}/${clusterId}${dimensionMap[text]?.href}`}>{dimensionMap[text]?.label}</Link>;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '检查项',
|
||||
dataIndex: 'configItem',
|
||||
width: 200,
|
||||
needTooltip: true,
|
||||
},
|
||||
{
|
||||
title: '检查项描述',
|
||||
dataIndex: 'configDesc',
|
||||
width: 240,
|
||||
needToolTip: true,
|
||||
},
|
||||
{
|
||||
title: '权重',
|
||||
dataIndex: 'weight',
|
||||
// width: 180,
|
||||
// eslint-disable-next-line react/display-name
|
||||
render: (text: number, record: any) => {
|
||||
return (
|
||||
<>
|
||||
<Form.Item
|
||||
name={`weight_${record.configItemName}`}
|
||||
label=""
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
validator: async (rule: any, value: string) => {
|
||||
const otherWeightCongigName: string[] = [];
|
||||
let totalPercent = 0;
|
||||
data.map((item: any) => {
|
||||
if (item.configItemName !== record.configItemName) {
|
||||
otherWeightCongigName.push(`weight_${item.configItemName}`);
|
||||
totalPercent += form.getFieldValue(`weight_${item.configItemName}`) ?? 0;
|
||||
}
|
||||
});
|
||||
if (!value) {
|
||||
return Promise.reject('请输入权重');
|
||||
}
|
||||
if (+value < 0) {
|
||||
return Promise.reject('最小为0');
|
||||
}
|
||||
if (+value + totalPercent !== 100) {
|
||||
return Promise.reject('总和应为100%');
|
||||
}
|
||||
form.setFields(otherWeightCongigName.map((i) => ({ name: i, errors: [] })));
|
||||
return Promise.resolve('');
|
||||
},
|
||||
},
|
||||
]}
|
||||
>
|
||||
<InputNumber
|
||||
size="small"
|
||||
min={0}
|
||||
max={100}
|
||||
formatter={(value) => `${value}%`}
|
||||
parser={(value: any) => value.replace('%', '')}
|
||||
/>
|
||||
</Form.Item>
|
||||
</>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '检查规则',
|
||||
// width: 350,
|
||||
dataIndex: 'passed',
|
||||
// eslint-disable-next-line react/display-name
|
||||
render: (text: any, record: any) => {
|
||||
const getFormItem = (params: { type?: string; percent?: boolean; attrs?: any; validator?: any }) => {
|
||||
const { validator, percent, type = 'value', attrs = { min: 0 } } = params;
|
||||
return (
|
||||
<Form.Item
|
||||
name={`${type}_${record.configItemName}`}
|
||||
label=""
|
||||
rules={
|
||||
validator
|
||||
? [
|
||||
{
|
||||
required: true,
|
||||
validator: validator,
|
||||
},
|
||||
]
|
||||
: [
|
||||
{
|
||||
required: true,
|
||||
message: '请输入',
|
||||
},
|
||||
]
|
||||
}
|
||||
>
|
||||
{percent ? (
|
||||
<InputNumber
|
||||
size="small"
|
||||
min={0}
|
||||
max={100}
|
||||
style={{ width: 86 }}
|
||||
formatter={(value) => `${value}%`}
|
||||
parser={(value: any) => value.replace('%', '')}
|
||||
/>
|
||||
) : (
|
||||
<InputNumber style={{ width: 86 }} size="small" {...attrs} />
|
||||
)}
|
||||
</Form.Item>
|
||||
);
|
||||
};
|
||||
|
||||
if (record.configItemName === 'Controller') {
|
||||
return <div className="table-form-item">不等于 1 则不通过</div>;
|
||||
}
|
||||
if (record.configItemName === 'RequestQueueSize' || record.configItemName === 'NoLeader') {
|
||||
return (
|
||||
<div className="table-form-item">
|
||||
<span className="left-text">≥</span>
|
||||
{getFormItem({ attrs: { min: 0, max: 99998 } })}
|
||||
<span className="right-text">则不通过</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (record.configItemName === 'NetworkProcessorAvgIdlePercent') {
|
||||
return (
|
||||
<div className="table-form-item">
|
||||
<span className="left-text">≤</span>
|
||||
{getFormItem({ percent: true })}
|
||||
<span className="right-text">则不通过</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (record.configItemName === 'UnderReplicaTooLong' || record.configItemName === 'ReBalance') {
|
||||
return (
|
||||
<div className="table-form-item">
|
||||
{getFormItem({ type: 'latestMinutes', attrs: { min: 1, max: 10080 } })}
|
||||
<span className="right-text left-text">周期内,≥</span>
|
||||
{getFormItem({
|
||||
type: 'detectedTimes',
|
||||
attrs: { min: 1, max: 10080 },
|
||||
validator: async (rule: any, value: string) => {
|
||||
const latestMinutesValue = form.getFieldValue(`latestMinutes_${record.configItemName}`);
|
||||
|
||||
if (!value) {
|
||||
return Promise.reject('请输入');
|
||||
}
|
||||
if (+value < 1) {
|
||||
return Promise.reject('最小为1');
|
||||
}
|
||||
if (+value > +latestMinutesValue) {
|
||||
return Promise.reject('值不能大于周期');
|
||||
}
|
||||
return Promise.resolve('');
|
||||
},
|
||||
})}
|
||||
<span className="right-text">则不通过</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <></>;
|
||||
},
|
||||
},
|
||||
] as any;
|
||||
@@ -0,0 +1,413 @@
|
||||
.single-cluster-detail {
|
||||
width: 100%;
|
||||
padding-bottom: 10px;
|
||||
|
||||
.cluster-detail {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
margin-top: 12px;
|
||||
|
||||
.left-sider {
|
||||
width: 240px;
|
||||
background: #fff;
|
||||
// height: calc(100vh - 128px);
|
||||
background: #fff;
|
||||
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.01), 0 3px 6px 3px rgba(0, 0, 0, 0.01), 0 2px 6px 0 rgba(0, 0, 0, 0.03);
|
||||
border-radius: 12px;
|
||||
padding: 24px 16px;
|
||||
|
||||
.title {
|
||||
font-family: @font-family-bold;
|
||||
font-size: 18px;
|
||||
color: #212529;
|
||||
letter-spacing: 0;
|
||||
text-align: justify;
|
||||
line-height: 20px;
|
||||
display: flex;
|
||||
|
||||
.name {
|
||||
max-width: 188px;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
overflow: hidden;
|
||||
word-break: break-all;
|
||||
}
|
||||
.edit-icon-box {
|
||||
position: relative;
|
||||
width: 20px;
|
||||
cursor: pointer;
|
||||
.edit-icon {
|
||||
position: absolute;
|
||||
bottom: 2px;
|
||||
margin-left: 4px;
|
||||
font-size: 16px;
|
||||
color: #74788d;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tag-panel {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
font-size: 10px;
|
||||
letter-spacing: 0;
|
||||
line-height: 12px;
|
||||
margin-top: 10px;
|
||||
|
||||
.tag {
|
||||
height: 18px;
|
||||
margin-right: 4px;
|
||||
margin-bottom: 4px;
|
||||
padding: 1px 6px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
|
||||
&.default {
|
||||
background: #ececf6;
|
||||
color: #495057;
|
||||
}
|
||||
&.balanced {
|
||||
background: rgba(85, 110, 230, 0.1);
|
||||
color: #556ee6;
|
||||
}
|
||||
&.unbalanced {
|
||||
background: rgba(255, 136, 0, 0.1);
|
||||
color: #f58342;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.desc {
|
||||
font-size: 11px;
|
||||
color: #74788d;
|
||||
text-align: left;
|
||||
line-height: 16px;
|
||||
margin: 12px 0px 16px;
|
||||
max-width: 208px;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
overflow: hidden;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.card-panel {
|
||||
.card-item {
|
||||
width: 208px;
|
||||
background: #f8f9fe;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
margin-bottom: 8px;
|
||||
cursor: pointer;
|
||||
.title {
|
||||
font-size: 14px;
|
||||
color: #212529;
|
||||
letter-spacing: 0;
|
||||
text-align: justify;
|
||||
line-height: 22px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.count {
|
||||
margin-bottom: 12px;
|
||||
|
||||
.num {
|
||||
font-family: DIDIFD-Regular;
|
||||
font-size: 40px;
|
||||
color: #212529;
|
||||
line-height: 36px;
|
||||
}
|
||||
|
||||
.unit {
|
||||
font-size: 16px;
|
||||
color: #212529;
|
||||
line-height: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.metric {
|
||||
font-size: 11px;
|
||||
color: #74788d;
|
||||
letter-spacing: 0;
|
||||
text-align: justify;
|
||||
line-height: 16px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-top: 4px;
|
||||
|
||||
.icon {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
|
||||
&.green {
|
||||
color: #34c38f;
|
||||
}
|
||||
|
||||
&.warning {
|
||||
color: #ffe4c6;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.state-card {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
|
||||
.healthy-percent {
|
||||
font-family: DIDIFD-Regular;
|
||||
font-size: 38px;
|
||||
color: #00c0a2;
|
||||
text-align: center;
|
||||
|
||||
&.less-90 {
|
||||
color: #556ee6;
|
||||
}
|
||||
|
||||
&.no-info {
|
||||
color: #e9e7e7;
|
||||
}
|
||||
|
||||
&.down {
|
||||
font-family: @font-family-bold;
|
||||
font-size: 22px;
|
||||
color: #ff7066;
|
||||
text-align: center;
|
||||
line-height: 30px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.healthy-state {
|
||||
margin-left: 14px;
|
||||
margin-top: 8px;
|
||||
|
||||
&-status {
|
||||
font-size: 13px;
|
||||
color: #495057;
|
||||
letter-spacing: 0;
|
||||
line-height: 20px;
|
||||
margin-bottom: 13px;
|
||||
|
||||
.icon {
|
||||
margin-left: 4px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
&-btn {
|
||||
font-size: 10px;
|
||||
color: #495057;
|
||||
line-height: 15px;
|
||||
background: #ececf6;
|
||||
border-radius: 4px;
|
||||
padding: 0px 6px;
|
||||
margin-left: 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.dcloud-divider-horizontal {
|
||||
margin: 16px 4px;
|
||||
padding: 0px 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.chart-panel {
|
||||
flex: 1;
|
||||
margin-left: 12px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.change-log-panel {
|
||||
height: 100%;
|
||||
padding: 12px 16px;
|
||||
|
||||
.title {
|
||||
font-family: @font-family-bold;
|
||||
font-size: 14px;
|
||||
color: #212529;
|
||||
letter-spacing: 0.5px;
|
||||
line-height: 22px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
#changelog-scroll-box {
|
||||
height: calc(100% - 34px);
|
||||
overflow: hidden auto;
|
||||
}
|
||||
|
||||
.dcloud-collapse {
|
||||
border: none;
|
||||
background-color: #fff;
|
||||
|
||||
.dcloud-collapse-header {
|
||||
background: #fff;
|
||||
height: 40px;
|
||||
display: block;
|
||||
padding: 0px 0px 8px !important;
|
||||
|
||||
.header {
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
height: 20px;
|
||||
display: flex;
|
||||
.label {
|
||||
font-size: 13px;
|
||||
color: #212529;
|
||||
max-width: 180px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.icon {
|
||||
width: 9px;
|
||||
height: 5px;
|
||||
color: #74788d;
|
||||
|
||||
.anticon {
|
||||
font-size: 9px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.header-time {
|
||||
height: 20px;
|
||||
font-size: 10px;
|
||||
color: #74788d;
|
||||
text-align: left;
|
||||
line-height: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.dcloud-collapse-header:hover {
|
||||
.label {
|
||||
color: #556ee6;
|
||||
font-family: @font-family-bold;
|
||||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.dcloud-collapse-content {
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
.dcloud-collapse-item {
|
||||
border: none;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.dcloud-collapse-item-active {
|
||||
.dcloud-collapse-header {
|
||||
.header {
|
||||
.anticon {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dcloud-collapse-content > .dcloud-collapse-content-box {
|
||||
padding: 7px 11px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
|
||||
.log-item {
|
||||
height: 28px;
|
||||
line-height: 28px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 10px;
|
||||
color: #74788d;
|
||||
|
||||
.value {
|
||||
color: #212529;
|
||||
max-width: 124px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.dcloud-divider-horizontal {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.page-panel {
|
||||
margin-top: 24px;
|
||||
text-align: center;
|
||||
|
||||
.dcloud-pagination-simple-pager {
|
||||
font-size: 13px;
|
||||
color: #495057;
|
||||
text-align: center;
|
||||
line-height: 22px;
|
||||
|
||||
input {
|
||||
width: 48px;
|
||||
height: 24px;
|
||||
background: #ececf6;
|
||||
border-radius: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.dcloud-pagination-simple {
|
||||
.anticon {
|
||||
font-size: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.empty-panel {
|
||||
margin-top: 96px;
|
||||
text-align: center;
|
||||
|
||||
.img {
|
||||
width: 51px;
|
||||
height: 34px;
|
||||
margin-left: 80px;
|
||||
margin-bottom: 7px;
|
||||
background-size: cover;
|
||||
background-image: url('../../assets/empty.png');
|
||||
}
|
||||
|
||||
.text {
|
||||
font-size: 10px;
|
||||
color: #919aac;
|
||||
line-height: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.healthy-drawer-content {
|
||||
.table-form-item {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: flex-start;
|
||||
.left-text,
|
||||
.right-text {
|
||||
line-height: 27px;
|
||||
}
|
||||
|
||||
.left-text {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.right-text {
|
||||
margin-left: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.dcloud-form-item {
|
||||
margin-bottom: 0px !important;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import DBreadcrumb from 'knowdesign/lib/extend/d-breadcrumb';
|
||||
import React from 'react';
|
||||
import TourGuide, { ClusterDetailSteps } from '@src/components/TourGuide';
|
||||
import './index.less';
|
||||
import LeftSider from './LeftSider';
|
||||
import ChartPanel from './DetailChart';
|
||||
import ChangeLog from './ChangeLog';
|
||||
|
||||
const SingleClusterDetail = (): JSX.Element => {
|
||||
return (
|
||||
<>
|
||||
<TourGuide guide={ClusterDetailSteps} run={true} />
|
||||
<div className="single-cluster-detail">
|
||||
<div className="breadcrumb">
|
||||
<DBreadcrumb
|
||||
breadcrumbs={[
|
||||
{ label: '多集群管理', aHref: '/' },
|
||||
{ label: '集群详情', aHref: '' },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
<div className="cluster-detail">
|
||||
<LeftSider />
|
||||
<div className="chart-panel">
|
||||
<ChartPanel>
|
||||
<ChangeLog />
|
||||
</ChartPanel>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default SingleClusterDetail;
|
||||
Reference in New Issue
Block a user