初始化3.0.0版本

This commit is contained in:
zengqiao
2022-08-18 17:04:05 +08:00
parent 462303fca0
commit 51832385b1
2446 changed files with 93177 additions and 127211 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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}&nbsp;{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;

View File

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

View File

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