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:
yaojp123 2025-01-10 15:17:08 +08:00 committed by GitHub
parent 718b029b85
commit f8eb0c2fc7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 1072 additions and 6 deletions

View File

@ -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",

View File

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

View File

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

View File

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

View File

@ -31,7 +31,9 @@ export default (props: IMeta & {
items,
bucketSize: queries.getBucketSize(),
});
setData(res)
if (res && !res.error) {
setData(res.data)
}
setLoading(false)
}

View File

@ -35,7 +35,9 @@ export default (props: IProps) => {
...metric,
bucketSize: queries.getBucketSize(),
});
setData(res)
if (res && !res.error) {
setData(res.data)
}
setLoading(false)
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,11 @@
.colors {
:global {
.ant-popover-inner-content {
padding: 8px;
max-width: 136px;
}
.ant-popover-arrow {
display: none;
}
}
}

View File

@ -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
}}
/>
)
}

View File

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

View File

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

View File

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

View File

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