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",
|
||||
"react": "^16.5.1",
|
||||
"react-calendar-heatmap": "^1.8.1",
|
||||
"react-color": "^2.19.3",
|
||||
"react-container-query": "^0.11.0",
|
||||
"react-copy-to-clipboard": "^5.0.1",
|
||||
"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 }) {
|
||||
let idx = state.clusterList.findIndex((item) => item.id === payload.id);
|
||||
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;
|
||||
return state;
|
||||
},
|
||||
|
|
|
@ -31,7 +31,9 @@ export default (props: IMeta & {
|
|||
items,
|
||||
bucketSize: queries.getBucketSize(),
|
||||
});
|
||||
setData(res)
|
||||
if (res && !res.error) {
|
||||
setData(res.data)
|
||||
}
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
|
|
|
@ -35,7 +35,9 @@ export default (props: IProps) => {
|
|||
...metric,
|
||||
bucketSize: queries.getBucketSize(),
|
||||
});
|
||||
setData(res)
|
||||
if (res && !res.error) {
|
||||
setData(res.data)
|
||||
}
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
|
|
|
@ -222,7 +222,7 @@ export default (props) => {
|
|||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
const newData = res.map((item) => Array.isArray(item) ? item : []);
|
||||
const newData = res.map((item) => Array.isArray(item.data) ? item.data : []);
|
||||
let group_mapping
|
||||
if (is_layered) {
|
||||
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 StatisticBar from "./statistic_bar";
|
||||
import { Empty } from "antd";
|
||||
import TopN from "./TopN";
|
||||
|
||||
const panes = [
|
||||
{ title: "Overview", component: Overview, key: "overview" },
|
||||
{ title: "Advanced", component: Advanced, key: "advanced" },
|
||||
{ title: "TopN", component: TopN, key: "topn" },
|
||||
{ title: "Nodes", component: Nodes, key: "nodes" },
|
||||
{ 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
|
||||
|
||||
yield put({
|
||||
type: "global/updateCluster",
|
||||
payload: {
|
||||
...payload,
|
||||
id: res._id,
|
||||
name: payload.name,
|
||||
monitored: payload.monitored,
|
||||
},
|
||||
});
|
||||
return res;
|
||||
|
|
Loading…
Reference in New Issue