diff --git a/web/package.json b/web/package.json index 3f8e0ae5..29e811a5 100644 --- a/web/package.json +++ b/web/package.json @@ -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", diff --git a/web/src/components/Icons/Convert.jsx b/web/src/components/Icons/Convert.jsx new file mode 100644 index 00000000..ca821cc1 --- /dev/null +++ b/web/src/components/Icons/Convert.jsx @@ -0,0 +1,15 @@ +export default () => { + return ( + + + + ); + }; + \ No newline at end of file diff --git a/web/src/components/Icons/Treemap.jsx b/web/src/components/Icons/Treemap.jsx new file mode 100644 index 00000000..333d46f9 --- /dev/null +++ b/web/src/components/Icons/Treemap.jsx @@ -0,0 +1,15 @@ +export default () => { + return ( + + + + ); + }; + \ No newline at end of file diff --git a/web/src/models/global.js b/web/src/models/global.js index 6a20e040..1d455371 100644 --- a/web/src/models/global.js +++ b/web/src/models/global.js @@ -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; }, diff --git a/web/src/pages/DataManagement/Insight/Visualization/Helper/ListItem.tsx b/web/src/pages/DataManagement/Insight/Visualization/Helper/ListItem.tsx index 5020d6e2..8320dd08 100644 --- a/web/src/pages/DataManagement/Insight/Visualization/Helper/ListItem.tsx +++ b/web/src/pages/DataManagement/Insight/Visualization/Helper/ListItem.tsx @@ -31,7 +31,9 @@ export default (props: IMeta & { items, bucketSize: queries.getBucketSize(), }); - setData(res) + if (res && !res.error) { + setData(res.data) + } setLoading(false) } diff --git a/web/src/pages/DataManagement/Insight/Visualization/Widget/WidgetBody/Chart.tsx b/web/src/pages/DataManagement/Insight/Visualization/Widget/WidgetBody/Chart.tsx index bd580fbe..7f474e61 100644 --- a/web/src/pages/DataManagement/Insight/Visualization/Widget/WidgetBody/Chart.tsx +++ b/web/src/pages/DataManagement/Insight/Visualization/Widget/WidgetBody/Chart.tsx @@ -35,7 +35,9 @@ export default (props: IProps) => { ...metric, bucketSize: queries.getBucketSize(), }); - setData(res) + if (res && !res.error) { + setData(res.data) + } setLoading(false) } diff --git a/web/src/pages/DataManagement/View/Widget/WidgetBody/Chart.jsx b/web/src/pages/DataManagement/View/Widget/WidgetBody/Chart.jsx index 8333ecfa..24168856 100644 --- a/web/src/pages/DataManagement/View/Widget/WidgetBody/Chart.jsx +++ b/web/src/pages/DataManagement/View/Widget/WidgetBody/Chart.jsx @@ -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] diff --git a/web/src/pages/Platform/Overview/Cluster/Monitor/TopN.jsx b/web/src/pages/Platform/Overview/Cluster/Monitor/TopN.jsx new file mode 100644 index 00000000..7282e11a --- /dev/null +++ b/web/src/pages/Platform/Overview/Cluster/Monitor/TopN.jsx @@ -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 ( + { + setParam({ + tab: key, + }); + }} + > + + + + { + !isAgent && ( + + + + ) + } + { + isAgent && ( + + + + ) + } + + ); +} diff --git a/web/src/pages/Platform/Overview/Cluster/Monitor/index.jsx b/web/src/pages/Platform/Overview/Cluster/Monitor/index.jsx index b141370d..1ee45594 100644 --- a/web/src/pages/Platform/Overview/Cluster/Monitor/index.jsx +++ b/web/src/pages/Platform/Overview/Cluster/Monitor/index.jsx @@ -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" }, ]; diff --git a/web/src/pages/Platform/Overview/components/TopN/ColorPicker.jsx b/web/src/pages/Platform/Overview/components/TopN/ColorPicker.jsx new file mode 100644 index 00000000..65f10918 --- /dev/null +++ b/web/src/pages/Platform/Overview/components/TopN/ColorPicker.jsx @@ -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 ( + +
+ setCurrentColor(color.hex) } + /> +
+
+ + { onRemove && } + +
+ + )}> +
+ {children} +
+
+ ) +} \ No newline at end of file diff --git a/web/src/pages/Platform/Overview/components/TopN/ColorPicker.less b/web/src/pages/Platform/Overview/components/TopN/ColorPicker.less new file mode 100644 index 00000000..3163e76f --- /dev/null +++ b/web/src/pages/Platform/Overview/components/TopN/ColorPicker.less @@ -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; + } + } +} \ No newline at end of file diff --git a/web/src/pages/Platform/Overview/components/TopN/GradientColorPicker.jsx b/web/src/pages/Platform/Overview/components/TopN/GradientColorPicker.jsx new file mode 100644 index 00000000..a3119c3b --- /dev/null +++ b/web/src/pages/Platform/Overview/components/TopN/GradientColorPicker.jsx @@ -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 ( + + { + value.map((item, index) => ( + onRemove(index)} onChange={(color) => handleChange(color, index)}> + + + )) + } + + + + + )}> +
+
+
+
+ ); +}; \ No newline at end of file diff --git a/web/src/pages/Platform/Overview/components/TopN/GradientColorPicker.less b/web/src/pages/Platform/Overview/components/TopN/GradientColorPicker.less new file mode 100644 index 00000000..ee1670d0 --- /dev/null +++ b/web/src/pages/Platform/Overview/components/TopN/GradientColorPicker.less @@ -0,0 +1,11 @@ +.colors { + :global { + .ant-popover-inner-content { + padding: 8px; + max-width: 136px; + } + .ant-popover-arrow { + display: none; + } + } +} \ No newline at end of file diff --git a/web/src/pages/Platform/Overview/components/TopN/Table.jsx b/web/src/pages/Platform/Overview/components/TopN/Table.jsx new file mode 100644 index 00000000..97a05d75 --- /dev/null +++ b/web/src/pages/Platform/Overview/components/TopN/Table.jsx @@ -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 ( + + ) +} \ No newline at end of file diff --git a/web/src/pages/Platform/Overview/components/TopN/Treemap.jsx b/web/src/pages/Platform/Overview/components/TopN/Treemap.jsx new file mode 100644 index 00000000..0bd51043 --- /dev/null +++ b/web/src/pages/Platform/Overview/components/TopN/Treemap.jsx @@ -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 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 ( +
+ { data.length === 0 || data.some((item) => !Number.isFinite(item.value)) ? ( +
+ +
+ ) : ( + { + 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: + } + ] + + if (metricColor) { + const { format: formatColor, unit: unitColor } = sourceColor || {} + const formatterColor = getFormatter(formatColor) + markers.push({ + name: nameColor, + value: formatterColor ? formatterColor(valueColor) : valueColor, + unit: unitColor, + marker: + }) + } + + return ( +
+ { +
+ {displayName} +
+ } +
+ { + markers.map((item, index) => ( +
+ {item.marker} + + {item.name}: + + {item.unit ? `${item.value}${item.unit}` : item.value} + + +
+ )) + } +
+
+ ); + }, + } + }} /> + )} +
+ ) +} \ No newline at end of file diff --git a/web/src/pages/Platform/Overview/components/TopN/index.jsx b/web/src/pages/Platform/Overview/components/TopN/index.jsx new file mode 100644 index 00000000..88eb5d75 --- /dev/null +++ b/web/src/pages/Platform/Overview/components/TopN/index.jsx @@ -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 ( + +
+
+ + setCurrentMode(e.target.value)} + className={styles.mode} + style={{ marginRight: 12, marginBottom: 12 }} + > + + + + + + + + + onFormDataChange({ top: value })} + /> + + + + + + + + + + { + onFormDataChange({ colors: value }) + setConfig({ + ...cloneDeep(config), + colors: value + }) + }}/> + + + {/* setCurrentMode(e.target.value)} + className={styles.mode} + > + + + + + + + + + + onFormDataChange({ top: value })} + /> + + + + + + + + + + + + + + + { + onFormDataChange({ colors: value }) + setConfig({ + ...cloneDeep(config), + colors: value + }) + }}/> + + + */} +
+ +
+ { + result?.request && ( + + +
message.success(formatMessage({id: "cluster.metrics.request.copy.success"}))}> + +
+
+
+ ) + } + { isTreemap ? :
} + + + + ) +} \ No newline at end of file diff --git a/web/src/pages/Platform/Overview/components/TopN/index.less b/web/src/pages/Platform/Overview/components/TopN/index.less new file mode 100644 index 00000000..f00b1aa7 --- /dev/null +++ b/web/src/pages/Platform/Overview/components/TopN/index.less @@ -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; + } + } +} \ No newline at end of file diff --git a/web/src/pages/System/Cluster/models/cluster.js b/web/src/pages/System/Cluster/models/cluster.js index 1ed2ec8b..c31bbdd9 100644 --- a/web/src/pages/System/Cluster/models/cluster.js +++ b/web/src/pages/System/Cluster/models/cluster.js @@ -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;