feat: add TopN to Monitor (#73)
* feat: add TopN to Monitor * chore: optimize TopN's UI --------- Co-authored-by: yaojiping <yaojiping@infini.ltd>
This commit is contained in:
parent
718b029b85
commit
f8eb0c2fc7
|
@ -68,6 +68,7 @@
|
||||||
"re-resizable": "^6.9.1",
|
"re-resizable": "^6.9.1",
|
||||||
"react": "^16.5.1",
|
"react": "^16.5.1",
|
||||||
"react-calendar-heatmap": "^1.8.1",
|
"react-calendar-heatmap": "^1.8.1",
|
||||||
|
"react-color": "^2.19.3",
|
||||||
"react-container-query": "^0.11.0",
|
"react-container-query": "^0.11.0",
|
||||||
"react-copy-to-clipboard": "^5.0.1",
|
"react-copy-to-clipboard": "^5.0.1",
|
||||||
"react-dnd": "^14.0.4",
|
"react-dnd": "^14.0.4",
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
export default () => {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
t="1735030192680"
|
||||||
|
viewBox="0 0 1024 1024"
|
||||||
|
version="1.1"
|
||||||
|
p-id="18425"
|
||||||
|
width="1em"
|
||||||
|
height="1em"
|
||||||
|
>
|
||||||
|
<path d="M853.333333 609.834667L652.501333 810.666667l-60.352-60.330667 55.168-55.146667-494.314666-0.021333v-85.333333H853.333333z m-499.498666-384l60.330666 60.330666L358.997333 341.333333H853.333333v85.333334H153.002667l200.832-200.832z" fill="currentColor" p-id="9542"></path>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
export default () => {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
t="1735030192680"
|
||||||
|
viewBox="0 0 1024 1024"
|
||||||
|
version="1.1"
|
||||||
|
p-id="18425"
|
||||||
|
width="1em"
|
||||||
|
height="1em"
|
||||||
|
>
|
||||||
|
<path d="M831.8 127.6l-640 0c-35.3 0-64 28.7-64 64l0 640c0 35.3 28.7 64 64 64l640 0c35.3 0 64-28.7 64-64l0-640C895.8 156.2 867.1 127.6 831.8 127.6zM448.2 414.6l193 0 0 418-193 0L448.2 414.6zM705.2 414.6l126.5 0 0 225L705.2 639.6 705.2 414.6zM831.8 350.6L448.2 350.6 448.2 192.6l383.5 0L831.7 350.6zM384.2 192.6L384.2 512 191.8 512 191.8 192.6 384.2 192.6zM191.8 576l192.5 0 0 256.6L191.8 832.6 191.8 576zM705.2 832.6l0-129 126.5 0 0 129L705.2 832.6z" fill="currentColor" p-id="18426"></path>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
|
@ -354,6 +354,9 @@ export default {
|
||||||
updateCluster(state, { payload }) {
|
updateCluster(state, { payload }) {
|
||||||
let idx = state.clusterList.findIndex((item) => item.id === payload.id);
|
let idx = state.clusterList.findIndex((item) => item.id === payload.id);
|
||||||
idx > -1 && (state.clusterList[idx].name = payload.name);
|
idx > -1 && (state.clusterList[idx].name = payload.name);
|
||||||
|
if (state.selectedCluster?.id === payload.id) {
|
||||||
|
state.selectedCluster.monitor_configs = payload.monitor_configs
|
||||||
|
}
|
||||||
state.clusterStatus[payload.id].config.monitored = payload.monitored;
|
state.clusterStatus[payload.id].config.monitored = payload.monitored;
|
||||||
return state;
|
return state;
|
||||||
},
|
},
|
||||||
|
|
|
@ -31,7 +31,9 @@ export default (props: IMeta & {
|
||||||
items,
|
items,
|
||||||
bucketSize: queries.getBucketSize(),
|
bucketSize: queries.getBucketSize(),
|
||||||
});
|
});
|
||||||
setData(res)
|
if (res && !res.error) {
|
||||||
|
setData(res.data)
|
||||||
|
}
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -35,7 +35,9 @@ export default (props: IProps) => {
|
||||||
...metric,
|
...metric,
|
||||||
bucketSize: queries.getBucketSize(),
|
bucketSize: queries.getBucketSize(),
|
||||||
});
|
});
|
||||||
setData(res)
|
if (res && !res.error) {
|
||||||
|
setData(res.data)
|
||||||
|
}
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -222,7 +222,7 @@ export default (props) => {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const newData = res.map((item) => Array.isArray(item) ? item : []);
|
const newData = res.map((item) => Array.isArray(item.data) ? item.data : []);
|
||||||
let group_mapping
|
let group_mapping
|
||||||
if (is_layered) {
|
if (is_layered) {
|
||||||
group_mapping = group_labels[layer_index]
|
group_mapping = group_labels[layer_index]
|
||||||
|
|
|
@ -0,0 +1,67 @@
|
||||||
|
import { Icon, Tabs } from "antd";
|
||||||
|
import { formatMessage } from "umi/locale";
|
||||||
|
import { useMemo, useState } from "react";
|
||||||
|
import NodeMetric from "../../components/node_metric";
|
||||||
|
import IndexMetric from "../../components/index_metric";
|
||||||
|
import ClusterMetric from "../../components/cluster_metric";
|
||||||
|
import QueueMetric from "../../components/queue_metric";
|
||||||
|
import { ESPrefix } from "@/services/common";
|
||||||
|
import { SearchEngines } from "@/lib/search_engines";
|
||||||
|
import TopN from "../../components/TopN";
|
||||||
|
|
||||||
|
export default (props) => {
|
||||||
|
|
||||||
|
const { isAgent } = props
|
||||||
|
|
||||||
|
const [param, setParam] = useState({
|
||||||
|
tab: "node",
|
||||||
|
});
|
||||||
|
return (
|
||||||
|
<Tabs
|
||||||
|
type="card"
|
||||||
|
tabBarGutter={10}
|
||||||
|
tabPosition="right"
|
||||||
|
destroyInactiveTabPane
|
||||||
|
animated={false}
|
||||||
|
activeKey={param?.tab}
|
||||||
|
onChange={(key) => {
|
||||||
|
setParam({
|
||||||
|
tab: key,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Tabs.TabPane
|
||||||
|
key="node"
|
||||||
|
tab={formatMessage({
|
||||||
|
id: "cluster.monitor.node.title",
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<TopN type={param?.tab} {...props}/>
|
||||||
|
</Tabs.TabPane>
|
||||||
|
{
|
||||||
|
!isAgent && (
|
||||||
|
<Tabs.TabPane
|
||||||
|
key="index"
|
||||||
|
tab={formatMessage({
|
||||||
|
id: "cluster.monitor.index.title",
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<TopN type={param?.tab} {...props}/>
|
||||||
|
</Tabs.TabPane>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
{
|
||||||
|
isAgent && (
|
||||||
|
<Tabs.TabPane
|
||||||
|
key="shard"
|
||||||
|
tab={formatMessage({
|
||||||
|
id: "cluster.monitor.shard.title",
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<TopN type={param?.tab} {...props}/>
|
||||||
|
</Tabs.TabPane>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</Tabs>
|
||||||
|
);
|
||||||
|
}
|
|
@ -8,10 +8,12 @@ import { formatMessage } from "umi/locale";
|
||||||
import Monitor from "@/components/Overview/Monitor";
|
import Monitor from "@/components/Overview/Monitor";
|
||||||
import StatisticBar from "./statistic_bar";
|
import StatisticBar from "./statistic_bar";
|
||||||
import { Empty } from "antd";
|
import { Empty } from "antd";
|
||||||
|
import TopN from "./TopN";
|
||||||
|
|
||||||
const panes = [
|
const panes = [
|
||||||
{ title: "Overview", component: Overview, key: "overview" },
|
{ title: "Overview", component: Overview, key: "overview" },
|
||||||
{ title: "Advanced", component: Advanced, key: "advanced" },
|
{ title: "Advanced", component: Advanced, key: "advanced" },
|
||||||
|
{ title: "TopN", component: TopN, key: "topn" },
|
||||||
{ title: "Nodes", component: Nodes, key: "nodes" },
|
{ title: "Nodes", component: Nodes, key: "nodes" },
|
||||||
{ title: "Indices", component: Indices, key: "indices" },
|
{ title: "Indices", component: Indices, key: "indices" },
|
||||||
];
|
];
|
||||||
|
|
|
@ -0,0 +1,49 @@
|
||||||
|
import { Button, Popover } from "antd";
|
||||||
|
import styles from "./ColorPicker.less";
|
||||||
|
import { SketchPicker } from 'react-color';
|
||||||
|
import { useEffect, useRef, useState } from 'react';
|
||||||
|
import { formatMessage } from "umi/locale";
|
||||||
|
|
||||||
|
export default (props) => {
|
||||||
|
const { children, color, onChange, onRemove } = props;
|
||||||
|
|
||||||
|
const [currentColor, setCurrentColor] = useState();
|
||||||
|
|
||||||
|
const targetRef = useRef(null)
|
||||||
|
|
||||||
|
const onClose = () => {
|
||||||
|
targetRef?.current?.click()
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setCurrentColor(color)
|
||||||
|
}, [color])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Popover overlayClassName={styles.colorPicker} placement="right" trigger="click" content={(
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
<SketchPicker
|
||||||
|
color={ currentColor }
|
||||||
|
onChangeComplete={(color) => setCurrentColor(color.hex) }
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '0 10px 10px 10px' }}>
|
||||||
|
<Button size="small" onClick={() => onClose()}>{formatMessage({ id: 'form.button.cancel'})}</Button>
|
||||||
|
{ onRemove && <Button type="danger" size="small" onClick={() => {
|
||||||
|
onRemove()
|
||||||
|
onClose()
|
||||||
|
}}>{formatMessage({ id: 'form.button.delete'})}</Button>}
|
||||||
|
<Button type="primary" size="small" onClick={() => {
|
||||||
|
onChange(currentColor)
|
||||||
|
onClose()
|
||||||
|
}}>{formatMessage({ id: 'form.button.ok'})}</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}>
|
||||||
|
<div ref={targetRef}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</Popover>
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
.colorPicker {
|
||||||
|
padding: 0;
|
||||||
|
:global {
|
||||||
|
.ant-popover-inner-content {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
.ant-popover-arrow {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.sketch-picker {
|
||||||
|
box-shadow: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,55 @@
|
||||||
|
import { Button, Popover } from "antd";
|
||||||
|
import ColorPicker from "./ColorPicker";
|
||||||
|
import styles from "./GradientColorPicker.less"
|
||||||
|
import { useMemo } from "react";
|
||||||
|
|
||||||
|
export default ({ value = [], onChange, style = {}, className = '' }) => {
|
||||||
|
|
||||||
|
const background = useMemo(() => {
|
||||||
|
if (value.length === 0) return undefined
|
||||||
|
if (value.length === 1) return value[0]
|
||||||
|
const each_percent = Math.round(100 / (value.length - 1))
|
||||||
|
return `linear-gradient(to right, ${value.map((item, index) => `${item} ${each_percent * index}%`).join(', ')})`
|
||||||
|
}, [JSON.stringify(value)])
|
||||||
|
|
||||||
|
const handleChange = (color, index) => {
|
||||||
|
const newValue = [...value];
|
||||||
|
newValue[index] = color
|
||||||
|
onChange(newValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onAdd = (color) => {
|
||||||
|
const newValue = [...value];
|
||||||
|
newValue.push(color)
|
||||||
|
onChange(newValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onRemove = (index) => {
|
||||||
|
const newValue = [...value];
|
||||||
|
newValue.splice(index, 1)
|
||||||
|
onChange(newValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Popover overlayClassName={styles.colors} placement="bottom" trigger="click" content={(
|
||||||
|
<div>
|
||||||
|
{
|
||||||
|
value.map((item, index) => (
|
||||||
|
<ColorPicker key={index} color={item} onRemove={() => onRemove(index)} onChange={(color) => handleChange(color, index)}>
|
||||||
|
<Button style={{ padding: 3, width: 120, marginBottom: 8 }} size="small" >
|
||||||
|
<div style={{ background: item, width: '100%', height: 16 }}></div>
|
||||||
|
</Button>
|
||||||
|
</ColorPicker>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
<ColorPicker onChange={onAdd}>
|
||||||
|
<Button size="small" icon="plus" style={{ width: 120 }}></Button>
|
||||||
|
</ColorPicker>
|
||||||
|
</div>
|
||||||
|
)}>
|
||||||
|
<div className={className} style={{ padding: '10px 8px', border: '1px solid #d9d9d9', width: 136, height: 32, cursor: 'pointer', ...style }}>
|
||||||
|
<div style={{ background, height: 12}}></div>
|
||||||
|
</div>
|
||||||
|
</Popover>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,11 @@
|
||||||
|
.colors {
|
||||||
|
:global {
|
||||||
|
.ant-popover-inner-content {
|
||||||
|
padding: 8px;
|
||||||
|
max-width: 136px;
|
||||||
|
}
|
||||||
|
.ant-popover-arrow {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,60 @@
|
||||||
|
import { getFormatter } from "@/utils/format";
|
||||||
|
import { Treemap } from "@ant-design/charts";
|
||||||
|
import { Table } from "antd";
|
||||||
|
import { useMemo } from "react";
|
||||||
|
import { formatMessage } from "umi/locale";
|
||||||
|
|
||||||
|
export default (props) => {
|
||||||
|
|
||||||
|
const { type, config = {}, data = [] } = props
|
||||||
|
const {
|
||||||
|
top,
|
||||||
|
statisticArea,
|
||||||
|
statisticColor,
|
||||||
|
sourceArea,
|
||||||
|
sourceColor,
|
||||||
|
} = config;
|
||||||
|
|
||||||
|
const columns = useMemo(() => {
|
||||||
|
const newColumns = [{
|
||||||
|
title: formatMessage({ id: `cluster.monitor.${type}.title` }) ,
|
||||||
|
dataIndex: 'displayName',
|
||||||
|
key: 'displayName',
|
||||||
|
}];
|
||||||
|
if (sourceArea) {
|
||||||
|
const { format: formatArea, unit: unitArea } = sourceArea || {}
|
||||||
|
const formatterArea = getFormatter(formatArea)
|
||||||
|
newColumns.push({
|
||||||
|
title: unitArea ? `${sourceArea.name}(${unitArea})` : sourceArea.name,
|
||||||
|
dataIndex: 'value',
|
||||||
|
key: 'value',
|
||||||
|
defaultSortOrder: 'descend',
|
||||||
|
sorter: (a, b) => a['value'] - b['value'],
|
||||||
|
render: (value) => formatterArea ? formatterArea(value) : value
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (sourceColor) {
|
||||||
|
const { format: formatColor, unit: unitColor } = sourceColor
|
||||||
|
const formatterColor = getFormatter(formatColor)
|
||||||
|
newColumns.push({
|
||||||
|
title: unitColor ? `${sourceColor.name}(${unitColor})` : sourceColor.name,
|
||||||
|
dataIndex: 'valueColor',
|
||||||
|
key: 'valueColor',
|
||||||
|
sorter: (a, b) => a['valueColor'] - b['valueColor'],
|
||||||
|
render: (value) => formatterColor ? formatterColor(value) : value
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return newColumns
|
||||||
|
}, [sourceArea, sourceColor])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Table
|
||||||
|
columns={columns}
|
||||||
|
dataSource={data}
|
||||||
|
size="small"
|
||||||
|
pagination={{
|
||||||
|
pageSize: top <= 20 ? top : 20
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,170 @@
|
||||||
|
import { getFormatter } from "@/utils/format";
|
||||||
|
import { Treemap } from "@ant-design/charts";
|
||||||
|
import { Empty } from "antd";
|
||||||
|
import { useMemo } from "react";
|
||||||
|
|
||||||
|
const generateGradientColors = (startColor, endColor, steps) => {
|
||||||
|
function colorToRgb(color) {
|
||||||
|
const rgb = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(color);
|
||||||
|
return rgb ? {
|
||||||
|
r: parseInt(rgb[1], 16),
|
||||||
|
g: parseInt(rgb[2], 16),
|
||||||
|
b: parseInt(rgb[3], 16)
|
||||||
|
} : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function rgbToHex(r, g, b) {
|
||||||
|
return "#" + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1).toUpperCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
const startRGB = colorToRgb(startColor);
|
||||||
|
const endRGB = colorToRgb(endColor);
|
||||||
|
const diffR = endRGB.r - startRGB.r;
|
||||||
|
const diffG = endRGB.g - startRGB.g;
|
||||||
|
const diffB = endRGB.b - startRGB.b;
|
||||||
|
|
||||||
|
const colors = [];
|
||||||
|
for (let i = 0; i <= steps; i++) {
|
||||||
|
const r = startRGB.r + (diffR * i / steps);
|
||||||
|
const g = startRGB.g + (diffG * i / steps);
|
||||||
|
const b = startRGB.b + (diffB * i / steps);
|
||||||
|
colors.push(rgbToHex(Math.round(r), Math.round(g), Math.round(b)));
|
||||||
|
}
|
||||||
|
|
||||||
|
return colors;
|
||||||
|
}
|
||||||
|
|
||||||
|
const generateColors = (colors, data) => {
|
||||||
|
if (!colors || colors.length <= 1 || !data || data.length <= 1 || data.length <= colors.length) return colors
|
||||||
|
const gradientSize = data.length - colors.length
|
||||||
|
const steps = Math.floor(gradientSize / (colors.length - 1)) + 1
|
||||||
|
let remainder = gradientSize % (colors.length - 1)
|
||||||
|
const newColors = []
|
||||||
|
for(let i=0; i<colors.length - 1; i++) {
|
||||||
|
let fixSteps = steps;
|
||||||
|
if (remainder > 0) {
|
||||||
|
fixSteps++
|
||||||
|
remainder--
|
||||||
|
}
|
||||||
|
const gradientColors = generateGradientColors(colors[i], colors[i+1], fixSteps)
|
||||||
|
newColors.push(...gradientColors.slice(0, gradientColors.length - 1))
|
||||||
|
}
|
||||||
|
newColors.push(colors[colors.length - 1])
|
||||||
|
return newColors
|
||||||
|
}
|
||||||
|
|
||||||
|
export default (props) => {
|
||||||
|
|
||||||
|
const { config = {}, data = [] } = props
|
||||||
|
const {
|
||||||
|
top,
|
||||||
|
colors = [],
|
||||||
|
sourceArea = {},
|
||||||
|
sourceColor = {},
|
||||||
|
} = config;
|
||||||
|
|
||||||
|
const color = useMemo(() => {
|
||||||
|
if (colors.length === 0 || !sourceColor?.key || data.length === 0) return undefined
|
||||||
|
const newColors = generateColors(colors, data)
|
||||||
|
const sortData = data.sort((a, b) => a.valueColor - b.valueColor)
|
||||||
|
return ({ name }) => {
|
||||||
|
const index = sortData.findIndex((item) => item.name === name)
|
||||||
|
if (index !== -1) {
|
||||||
|
return newColors[index] || newColors[0]
|
||||||
|
} else {
|
||||||
|
return newColors[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [data, colors, sourceColor])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ height: '100%' }}>
|
||||||
|
{ data.length === 0 || data.some((item) => !Number.isFinite(item.value)) ? (
|
||||||
|
<div style={{ width: '100%', height: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||||
|
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE}/>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Treemap {...{
|
||||||
|
data: {
|
||||||
|
name: 'root',
|
||||||
|
children: data
|
||||||
|
},
|
||||||
|
autoFit: true,
|
||||||
|
color,
|
||||||
|
colorField: 'name',
|
||||||
|
legend: {
|
||||||
|
position: 'top-left',
|
||||||
|
itemName: {
|
||||||
|
formatter: (text) => {
|
||||||
|
const item = data.find((item) => item.name === text)
|
||||||
|
return item?.groupName || text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
formatter: (item) => item.displayName
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
customContent: (title, items) => {
|
||||||
|
if (!items[0]) return;
|
||||||
|
const { color, data } = items[0];
|
||||||
|
const { displayName, value, metricArea, nameArea, metricColor, nameColor, valueColor } = data;
|
||||||
|
const { format: formatArea, unit: unitArea } = sourceArea || {}
|
||||||
|
const formatterArea = getFormatter(formatArea)
|
||||||
|
|
||||||
|
const markers = [
|
||||||
|
{
|
||||||
|
name: nameArea,
|
||||||
|
value: formatterArea ? formatterArea(value) : value,
|
||||||
|
unit: unitArea,
|
||||||
|
marker: <span style={{ position: 'absolute', left: 0, top: 0, fontSize: 12 }}><svg t="1735902367048" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="15719" width="1em" height="1em"><path d="M525.649872 2.562499l-4.199999-2.499999c8.862498 12.062497 8.862498 6.424998 4.199999 2.562499z m467.062386 236.662443A31.862492 31.862492 0 0 0 1024.73725 207.499949V39.53749a31.862492 31.862492 0 0 0-31.962492-31.687492H823.462299a31.862492 31.862492 0 0 0-31.962492 31.687492v52.162488h-103.937475a31.349992 31.349992 0 0 0-9.787497 0H233.237443V39.53749A31.849992 31.849992 0 0 0 201.249951 7.849998H31.974992A31.862492 31.862492 0 0 0 0 39.53749v167.824959a31.849992 31.849992 0 0 0 31.974992 31.687493h52.624987v553.749864h-52.624987A31.862492 31.862492 0 0 0 0 824.487299v167.824959a31.849992 31.849992 0 0 0 31.974992 31.687492H201.249951a31.837492 31.837492 0 0 0 31.962492-31.737492v-52.174988H791.374807v52.174988a31.862492 31.862492 0 0 0 31.974992 31.737492h169.299959a31.862492 31.862492 0 0 0 31.974992-31.737492V824.599799a31.862492 31.862492 0 0 0-31.974992-31.737493H939.999771V347.299915a15.574996 15.574996 0 0 0 0-3.237499v-104.899974h52.749987zM148.749964 462.499887a34.024992 34.024992 0 0 0 5.412498-4.312499l305.212426-302.874926H604.999852L148.749964 607.912352z m52.624987-223.337445A31.849992 31.849992 0 0 0 233.299943 207.499949v-52.249987h135.512467L148.649964 373.749909V239.162442zM148.749964 697.68733L695.46233 155.249962h95.974977v38.974991L187.787454 792.862306h-39.24999v-95.174976zM876.087286 564.999862L569.399861 869.149788a32.349992 32.349992 0 0 0-5.687499 7.624998H418.012398l458.074888-454.374889z m-52.624987 227.899944a31.862492 31.862492 0 0 0-31.962492 31.737493v52.174987H652.399841l223.749945-221.987446v138.037466z m52.624987-460.287387L327.39992 876.774786h-94.162477v-39.137491l603.362353-598.474853h39.48749z" p-id="15720" fill="#666"></path></svg></span>
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
if (metricColor) {
|
||||||
|
const { format: formatColor, unit: unitColor } = sourceColor || {}
|
||||||
|
const formatterColor = getFormatter(formatColor)
|
||||||
|
markers.push({
|
||||||
|
name: nameColor,
|
||||||
|
value: formatterColor ? formatterColor(valueColor) : valueColor,
|
||||||
|
unit: unitColor,
|
||||||
|
marker: <span style={{ position: 'absolute', left: 0, top: 0, display: 'block', borderRadius: '2px', backgroundColor: color, width: 12, height: 12 }}></span>
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ padding: 4 }}>
|
||||||
|
{
|
||||||
|
<h5 style={{ marginTop: 12, marginBottom: 12 }}>
|
||||||
|
{displayName}
|
||||||
|
</h5>
|
||||||
|
}
|
||||||
|
<div>
|
||||||
|
{
|
||||||
|
markers.map((item, index) => (
|
||||||
|
<div
|
||||||
|
style={{ display: 'block', paddingLeft: 18, marginBottom: 12, position: 'relative' }}
|
||||||
|
key={index}
|
||||||
|
>
|
||||||
|
{item.marker}
|
||||||
|
<span
|
||||||
|
style={{ display: 'inline-flex', flex: 1, justifyContent: 'space-between' }}
|
||||||
|
>
|
||||||
|
<span style={{ marginRight: 16 }}>{item.name}:</span>
|
||||||
|
<span className="g2-tooltip-list-item-value">
|
||||||
|
{item.unit ? `${item.value}${item.unit}` : item.value}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,523 @@
|
||||||
|
|
||||||
|
import TreemapSvg from "@/components/Icons/Treemap"
|
||||||
|
import styles from "./index.less"
|
||||||
|
import { Button, Icon, Input, InputNumber, message, Popover, Radio, Select, Spin, Tooltip } from "antd";
|
||||||
|
import { formatMessage } from "umi/locale";
|
||||||
|
import ConvertSvg from "@/components/Icons/Convert"
|
||||||
|
import { useEffect, useMemo, useRef, useState } from "react";
|
||||||
|
import ColorPicker from "./ColorPicker";
|
||||||
|
import Treemap from "./Treemap";
|
||||||
|
import Table from "./Table";
|
||||||
|
import GradientColorPicker from "./GradientColorPicker";
|
||||||
|
import { cloneDeep } from "lodash";
|
||||||
|
import request from "@/utils/request";
|
||||||
|
import { formatTimeRange } from "@/lib/elasticsearch/util";
|
||||||
|
import { CopyToClipboard } from "react-copy-to-clipboard";
|
||||||
|
|
||||||
|
export default (props) => {
|
||||||
|
|
||||||
|
const { type, clusterID, timeRange } = props;
|
||||||
|
|
||||||
|
const [currentMode, setCurrentMode] = useState('treemap')
|
||||||
|
|
||||||
|
const [metrics, setMetrics] = useState([])
|
||||||
|
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
top: 15,
|
||||||
|
colors: ['#00bb1b', '#fcca00', '#ff4d4f']
|
||||||
|
})
|
||||||
|
|
||||||
|
const [config, setConfig] = useState({})
|
||||||
|
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
const [data, setData] = useState([])
|
||||||
|
const [result, setResult] = useState()
|
||||||
|
const searchParamsRef = useRef()
|
||||||
|
|
||||||
|
const fetchMetrics = async (type) => {
|
||||||
|
setLoading(true)
|
||||||
|
const res = await request(`/collection/metric/_search`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: {
|
||||||
|
size: 10000,
|
||||||
|
from: 0,
|
||||||
|
query: { bool: { filter: [{ "term": { "level": type === 'index' ? 'indices' : type } }] }}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (res?.hits?.hits) {
|
||||||
|
const newMetrics = res?.hits?.hits.filter((item) => {
|
||||||
|
const { items = [] } = item._source;
|
||||||
|
if (items.length === 0) return false
|
||||||
|
return true;
|
||||||
|
}).map((item) => ({ ...item._source }))
|
||||||
|
setMetrics(newMetrics)
|
||||||
|
}
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchData = async (type, clusterID, timeRange, formData, shouldLoading = true) => {
|
||||||
|
if (!clusterID || !timeRange || !formData.sourceArea) return;
|
||||||
|
if (shouldLoading) {
|
||||||
|
setLoading(true)
|
||||||
|
}
|
||||||
|
const { top, sourceArea = {}, statisticArea, statisticColor, sourceColor = {} } = formData
|
||||||
|
const newTimeRange = formatTimeRange(timeRange);
|
||||||
|
searchParamsRef.current = { type, clusterID, formData }
|
||||||
|
const body = {
|
||||||
|
"index_pattern": ".infini_metrics*",
|
||||||
|
"time_field": "timestamp",
|
||||||
|
"bucket_size": "auto",
|
||||||
|
"filter": {
|
||||||
|
"bool": {
|
||||||
|
"must": [{
|
||||||
|
"term": {
|
||||||
|
"metadata.name": {
|
||||||
|
"value": `${type}_stats`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
"term": {
|
||||||
|
"metadata.category": {
|
||||||
|
"value": "elasticsearch"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
"range": {
|
||||||
|
"timestamp": {
|
||||||
|
"gte": newTimeRange.min,
|
||||||
|
"lte": newTimeRange.max,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}],
|
||||||
|
"must_not": type === 'index' ? [{
|
||||||
|
"term": {
|
||||||
|
"metadata.labels.index_name": {
|
||||||
|
"value": `_all`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}] : [],
|
||||||
|
"filter": [
|
||||||
|
{
|
||||||
|
"term": { "metadata.labels.cluster_id": clusterID }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"formulas": [sourceArea?.formula, sourceColor?.formula].filter((item) => !!item),
|
||||||
|
"items": [...(sourceArea?.items || [])?.map((item) => {
|
||||||
|
item.statistic = statisticArea
|
||||||
|
if (item.statistic === 'rate') {
|
||||||
|
item.statistic = 'derivative'
|
||||||
|
}
|
||||||
|
return item
|
||||||
|
}),...(sourceColor?.items || [])?.map((item) => {
|
||||||
|
item.statistic = statisticColor
|
||||||
|
if (item.statistic === 'rate') {
|
||||||
|
item.statistic = 'derivative'
|
||||||
|
}
|
||||||
|
return item
|
||||||
|
})].filter((item) => !!item),
|
||||||
|
"groups": [{
|
||||||
|
"field": type === 'shard' ? `metadata.labels.shard_id` : `metadata.labels.${type}_name`,
|
||||||
|
"limit": top
|
||||||
|
}],
|
||||||
|
"sort": [{
|
||||||
|
"direction": "desc",
|
||||||
|
"key": sourceArea?.items[0].name
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
if (statisticArea !== 'rate' && statisticColor !== 'rate') {
|
||||||
|
delete body['time_field']
|
||||||
|
delete body['bucket_size']
|
||||||
|
}
|
||||||
|
const res = await request(`/elasticsearch/infini_default_system_cluster/visualization/data`, {
|
||||||
|
method: 'POST',
|
||||||
|
body
|
||||||
|
})
|
||||||
|
if (res && !res.error) {
|
||||||
|
setResult(res)
|
||||||
|
setConfig(cloneDeep(formData))
|
||||||
|
} else {
|
||||||
|
setResult()
|
||||||
|
}
|
||||||
|
if (shouldLoading) {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onFormDataChange = (values) => {
|
||||||
|
setFormData({
|
||||||
|
...cloneDeep(formData),
|
||||||
|
...values
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const onMetricExchange = () => {
|
||||||
|
const newFormData = cloneDeep(formData);
|
||||||
|
const sourceTmp = cloneDeep(newFormData.sourceArea)
|
||||||
|
const statisticTmp = newFormData.statisticArea
|
||||||
|
newFormData.sourceArea = cloneDeep(newFormData.sourceColor)
|
||||||
|
newFormData.statisticArea = newFormData.statisticColor
|
||||||
|
newFormData.sourceColor = sourceTmp
|
||||||
|
newFormData.statisticColor = statisticTmp
|
||||||
|
setResult()
|
||||||
|
setFormData(newFormData)
|
||||||
|
fetchData(type, clusterID, timeRange, newFormData)
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchMetrics(type)
|
||||||
|
}, [type])
|
||||||
|
|
||||||
|
const isTreemap = useMemo(() => {
|
||||||
|
return currentMode === 'treemap'
|
||||||
|
}, [currentMode])
|
||||||
|
|
||||||
|
const { sourceArea, sourceColor } = config
|
||||||
|
|
||||||
|
const formatData = useMemo(() => {
|
||||||
|
|
||||||
|
const { data = [] } = result || {};
|
||||||
|
if (!data || data.length === 0 || !sourceArea) return []
|
||||||
|
return data.filter((item) => !!(item.groups && item.groups[0])).map((item) => {
|
||||||
|
const { groups = [], value } = item;
|
||||||
|
let name = groups[0];
|
||||||
|
if (type === 'shard') {
|
||||||
|
const splits = name.split(':')
|
||||||
|
if (splits.length > 1) {
|
||||||
|
name = splits.slice(1).join(':')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const object = {
|
||||||
|
name: name,
|
||||||
|
displayName: name,
|
||||||
|
value: value?.[sourceArea.formula] || 0,
|
||||||
|
metricArea: sourceArea.key,
|
||||||
|
nameArea: sourceArea.name,
|
||||||
|
}
|
||||||
|
if (sourceColor?.formula) {
|
||||||
|
object['metricColor'] = sourceColor.key
|
||||||
|
object['valueColor'] = value?.[sourceColor.formula] || 0
|
||||||
|
object['nameColor'] = sourceColor.name
|
||||||
|
}
|
||||||
|
return object
|
||||||
|
})
|
||||||
|
}, [result, sourceArea, sourceColor, type])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (searchParamsRef.current) {
|
||||||
|
const { type, clusterID, formData } = searchParamsRef.current
|
||||||
|
fetchData(type, clusterID, timeRange, formData, false)
|
||||||
|
}
|
||||||
|
}, [timeRange])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Spin spinning={loading}>
|
||||||
|
<div className={styles.topn}>
|
||||||
|
<div className={styles.header}>
|
||||||
|
<Input.Group compact style={{ width: 'auto '}}>
|
||||||
|
<Radio.Group
|
||||||
|
value={currentMode}
|
||||||
|
onChange={(e) => setCurrentMode(e.target.value)}
|
||||||
|
className={styles.mode}
|
||||||
|
style={{ marginRight: 12, marginBottom: 12 }}
|
||||||
|
>
|
||||||
|
<Radio.Button value="treemap">
|
||||||
|
<Icon
|
||||||
|
component={TreemapSvg}
|
||||||
|
style={{
|
||||||
|
fontSize: 16,
|
||||||
|
color: isTreemap ? "#1890ff" : "",
|
||||||
|
verticalAlign: '-3px'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Radio.Button>
|
||||||
|
<Radio.Button value="table">
|
||||||
|
<Icon
|
||||||
|
type="table"
|
||||||
|
style={{
|
||||||
|
color: !isTreemap ? "#1890ff" : "",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Radio.Button>
|
||||||
|
</Radio.Group>
|
||||||
|
<Input
|
||||||
|
style={{ width: "60px", marginBottom: 12 }}
|
||||||
|
className={styles.borderRadiusLeft}
|
||||||
|
disabled
|
||||||
|
defaultValue={"Top"}
|
||||||
|
/>
|
||||||
|
<InputNumber
|
||||||
|
style={{ width: "80px", marginBottom: 12, marginRight: 12 }}
|
||||||
|
className={styles.borderRadiusRight}
|
||||||
|
value={formData.top}
|
||||||
|
min={1}
|
||||||
|
step={1}
|
||||||
|
precision={0}
|
||||||
|
onChange={(value) => onFormDataChange({ top: value })}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
style={{ width: "80px", marginBottom: 12 }}
|
||||||
|
className={styles.borderRadiusLeft}
|
||||||
|
disabled
|
||||||
|
defaultValue={"面积指标"}
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
|
style={{ width: "150px", marginBottom: 12 }}
|
||||||
|
value={formData.sourceArea?.key}
|
||||||
|
onChange={(value, option) => {
|
||||||
|
const { items = [] } = option?.props?.metric || {}
|
||||||
|
onFormDataChange({
|
||||||
|
statisticArea: items[0]?.statistic === 'derivative' ? 'rate' : items[0]?.statistic,
|
||||||
|
sourceArea: option?.props?.metric
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{
|
||||||
|
metrics.map((item) => (
|
||||||
|
<Select.Option key={item.key} metric={item}>
|
||||||
|
{item.name}
|
||||||
|
</Select.Option>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</Select>
|
||||||
|
<Select
|
||||||
|
style={{ width: "88px", marginBottom: 12, marginRight: 6 }}
|
||||||
|
className={styles.borderRadiusRight}
|
||||||
|
value={formData.statisticArea}
|
||||||
|
onChange={(value) => onFormDataChange({ statisticArea: value })}
|
||||||
|
>
|
||||||
|
{
|
||||||
|
formData.sourceArea?.statistics?.filter((item) => !!item).map((item) => (
|
||||||
|
<Select.Option key={item}>
|
||||||
|
{item.toUpperCase()}
|
||||||
|
</Select.Option>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</Select>
|
||||||
|
<Button style={{ width: 32, marginBottom: 12, padding: 0, marginRight: 6, borderRadius: 4 }} onClick={() => onMetricExchange()}><Icon style={{ fontSize: 16 }} component={ConvertSvg}/></Button>
|
||||||
|
<Input
|
||||||
|
style={{ width: "80px", marginBottom: 12 }}
|
||||||
|
className={styles.borderRadiusLeft}
|
||||||
|
disabled
|
||||||
|
defaultValue={"颜色指标"}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Select
|
||||||
|
style={{ width: "150px", marginBottom: 12 }}
|
||||||
|
value={formData.sourceColor?.key}
|
||||||
|
onChange={(value, option) => {
|
||||||
|
if (value) {
|
||||||
|
const { items = [] } = option?.props?.metric || {}
|
||||||
|
onFormDataChange({
|
||||||
|
statisticColor: items[0]?.statistic === 'derivative' ? 'rate' : items[0]?.statistic,
|
||||||
|
sourceColor: option?.props?.metric
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
onFormDataChange({
|
||||||
|
statisticColor: undefined,
|
||||||
|
sourceColor: undefined
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}}
|
||||||
|
allowClear
|
||||||
|
>
|
||||||
|
{
|
||||||
|
metrics.map((item) => (
|
||||||
|
<Select.Option key={item.key} metric={item}>
|
||||||
|
{item.name}
|
||||||
|
</Select.Option>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</Select>
|
||||||
|
<Select
|
||||||
|
style={{ width: "88px", marginBottom: 12 }}
|
||||||
|
value={formData.statisticColor}
|
||||||
|
onChange={(value) => onFormDataChange({ statisticColor: value })}
|
||||||
|
>
|
||||||
|
{
|
||||||
|
formData.sourceColor?.statistics?.filter((item) => !!item).map((item) => (
|
||||||
|
<Select.Option key={item}>
|
||||||
|
{item.toUpperCase()}
|
||||||
|
</Select.Option>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</Select>
|
||||||
|
<Input
|
||||||
|
style={{ width: "60px", marginBottom: 12 }}
|
||||||
|
disabled
|
||||||
|
defaultValue={"主题"}
|
||||||
|
/>
|
||||||
|
<GradientColorPicker className={styles.borderRadiusRight} style={{ marginRight: 12, marginBottom: 12 }} value={formData.colors || []} onChange={(value) => {
|
||||||
|
onFormDataChange({ colors: value })
|
||||||
|
setConfig({
|
||||||
|
...cloneDeep(config),
|
||||||
|
colors: value
|
||||||
|
})
|
||||||
|
}}/>
|
||||||
|
<Button style={{ marginBottom: 12 }} className={styles.borderRadiusLeft} type="primary" onClick={() => fetchData(type, clusterID, timeRange, formData)}>{formatMessage({ id: "form.button.search" })}</Button>
|
||||||
|
</Input.Group>
|
||||||
|
{/* <Radio.Group
|
||||||
|
value={currentMode}
|
||||||
|
onChange={(e) => setCurrentMode(e.target.value)}
|
||||||
|
className={styles.mode}
|
||||||
|
>
|
||||||
|
<Radio.Button value="treemap">
|
||||||
|
<Icon
|
||||||
|
component={TreemapSvg}
|
||||||
|
style={{
|
||||||
|
fontSize: 16,
|
||||||
|
color: isTreemap ? "#1890ff" : "",
|
||||||
|
verticalAlign: '-3px'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Radio.Button>
|
||||||
|
<Radio.Button value="table">
|
||||||
|
<Icon
|
||||||
|
type="table"
|
||||||
|
style={{
|
||||||
|
color: !isTreemap ? "#1890ff" : "",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Radio.Button>
|
||||||
|
</Radio.Group>
|
||||||
|
<Input.Group compact style={{ width: 'auto '}}>
|
||||||
|
<Input
|
||||||
|
style={{ width: "60px" }}
|
||||||
|
disabled
|
||||||
|
defaultValue={"Top"}
|
||||||
|
/>
|
||||||
|
<InputNumber
|
||||||
|
style={{ width: "80px" }}
|
||||||
|
value={formData.top}
|
||||||
|
min={1}
|
||||||
|
step={1}
|
||||||
|
precision={0}
|
||||||
|
onChange={(value) => onFormDataChange({ top: value })}
|
||||||
|
/>
|
||||||
|
</Input.Group>
|
||||||
|
<Input.Group compact style={{ width: 'auto '}}>
|
||||||
|
<Input
|
||||||
|
style={{ width: "80px" }}
|
||||||
|
disabled
|
||||||
|
defaultValue={"面积指标"}
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
|
style={{ width: "150px" }}
|
||||||
|
value={formData.sourceArea?.key}
|
||||||
|
onChange={(value, option) => {
|
||||||
|
const { items = [] } = option?.props?.metric || {}
|
||||||
|
onFormDataChange({
|
||||||
|
statisticArea: items[0]?.statistic === 'derivative' ? 'rate' : items[0]?.statistic,
|
||||||
|
sourceArea: option?.props?.metric
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{
|
||||||
|
metrics.map((item) => (
|
||||||
|
<Select.Option key={item.key} metric={item}>
|
||||||
|
{item.name}
|
||||||
|
</Select.Option>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</Select>
|
||||||
|
<Select
|
||||||
|
style={{ width: "88px" }}
|
||||||
|
value={formData.statisticArea}
|
||||||
|
onChange={(value) => onFormDataChange({ statisticArea: value })}
|
||||||
|
>
|
||||||
|
{
|
||||||
|
formData.sourceArea?.statistics?.filter((item) => !!item).map((item) => (
|
||||||
|
<Select.Option key={item}>
|
||||||
|
{item.toUpperCase()}
|
||||||
|
</Select.Option>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</Select>
|
||||||
|
</Input.Group>
|
||||||
|
<Button style={{ width: 32, padding: 0 }} onClick={() => onMetricExchange()}><Icon style={{ fontSize: 16 }} component={ConvertSvg}/></Button>
|
||||||
|
<Input.Group compact style={{ width: 'auto'}}>
|
||||||
|
<Input
|
||||||
|
style={{ width: "80px" }}
|
||||||
|
disabled
|
||||||
|
defaultValue={"颜色指标"}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Select
|
||||||
|
style={{ width: "150px" }}
|
||||||
|
value={formData.sourceColor?.key}
|
||||||
|
onChange={(value, option) => {
|
||||||
|
if (value) {
|
||||||
|
const { items = [] } = option?.props?.metric || {}
|
||||||
|
onFormDataChange({
|
||||||
|
statisticColor: items[0]?.statistic === 'derivative' ? 'rate' : items[0]?.statistic,
|
||||||
|
sourceColor: option?.props?.metric
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
onFormDataChange({
|
||||||
|
statisticColor: undefined,
|
||||||
|
sourceColor: undefined
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}}
|
||||||
|
allowClear
|
||||||
|
>
|
||||||
|
{
|
||||||
|
metrics.map((item) => (
|
||||||
|
<Select.Option key={item.key} metric={item}>
|
||||||
|
{item.name}
|
||||||
|
</Select.Option>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</Select>
|
||||||
|
<Select
|
||||||
|
style={{ width: "88px" }}
|
||||||
|
value={formData.statisticColor}
|
||||||
|
onChange={(value) => onFormDataChange({ statisticColor: value })}
|
||||||
|
>
|
||||||
|
{
|
||||||
|
formData.sourceColor?.statistics?.filter((item) => !!item).map((item) => (
|
||||||
|
<Select.Option key={item}>
|
||||||
|
{item.toUpperCase()}
|
||||||
|
</Select.Option>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</Select>
|
||||||
|
<Input.Group compact style={{ width: 'auto '}}>
|
||||||
|
<Input
|
||||||
|
style={{ width: "60px" }}
|
||||||
|
disabled
|
||||||
|
defaultValue={"主题"}
|
||||||
|
/>
|
||||||
|
<GradientColorPicker value={formData.colors || []} onChange={(value) => {
|
||||||
|
onFormDataChange({ colors: value })
|
||||||
|
setConfig({
|
||||||
|
...cloneDeep(config),
|
||||||
|
colors: value
|
||||||
|
})
|
||||||
|
}}/>
|
||||||
|
</Input.Group>
|
||||||
|
</Input.Group>
|
||||||
|
<Button type="primary" onClick={() => fetchData(type, clusterID, timeRange, formData)}>{formatMessage({ id: "form.button.search" })}</Button> */}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.content}>
|
||||||
|
{
|
||||||
|
result?.request && (
|
||||||
|
<CopyToClipboard text={`GET .infini_metrics/_search\n${result.request}`}>
|
||||||
|
<Tooltip title={formatMessage({id: "cluster.metrics.request.copy"})}>
|
||||||
|
<div className={styles.info} onClick={() => message.success(formatMessage({id: "cluster.metrics.request.copy.success"}))}>
|
||||||
|
<Icon type="copy" />
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
</CopyToClipboard>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
{ isTreemap ? <Treemap config={config} data={formatData} /> : <Table type={type} config={config} data={formatData}/> }
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Spin>
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,78 @@
|
||||||
|
.topn {
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
|
||||||
|
:global {
|
||||||
|
.ant-input-group {
|
||||||
|
display: flex !important;
|
||||||
|
flex-wrap: wrap !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode {
|
||||||
|
width: 84px;
|
||||||
|
:global {
|
||||||
|
.ant-radio-button-wrapper {
|
||||||
|
width: 42px;
|
||||||
|
padding: 0 13px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
width: 100%;
|
||||||
|
height: calc(100vh - 500px);
|
||||||
|
min-height: 500px;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.info {
|
||||||
|
position: absolute;
|
||||||
|
display: none;
|
||||||
|
right: 2px;
|
||||||
|
top: 2px;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
border-radius: 4px;
|
||||||
|
line-height: 24px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 16px;
|
||||||
|
box-shadow: rgba(0, 0, 0, 0.16) 0px 0px 5px 0px;
|
||||||
|
background: #fff;
|
||||||
|
z-index: 11;
|
||||||
|
cursor: pointer;
|
||||||
|
color: rgba(0, 0, 0, 0.45);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #1890ff;
|
||||||
|
box-shadow: rgba(0, 0, 0, 0.3) 0px 0px 5px 0px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
.info {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.borderRadiusLeft {
|
||||||
|
border-top-left-radius: 4px !important;
|
||||||
|
border-bottom-left-radius: 4px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.borderRadiusRight {
|
||||||
|
border-top-right-radius: 4px !important;
|
||||||
|
border-bottom-right-radius: 4px !important;
|
||||||
|
:global {
|
||||||
|
.ant-select-selection {
|
||||||
|
border-top-right-radius: 4px !important;
|
||||||
|
border-bottom-right-radius: 4px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -109,15 +109,14 @@ export default {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
//handle global cluster logic
|
//handle global cluster logic
|
||||||
|
|
||||||
yield put({
|
yield put({
|
||||||
type: "global/updateCluster",
|
type: "global/updateCluster",
|
||||||
payload: {
|
payload: {
|
||||||
|
...payload,
|
||||||
id: res._id,
|
id: res._id,
|
||||||
name: payload.name,
|
|
||||||
monitored: payload.monitored,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
return res;
|
return res;
|
||||||
|
|
Loading…
Reference in New Issue