diff --git a/src/components/DashboardCard/index.tsx b/src/components/DashboardCard/index.tsx index 16e57fde..79a8dae6 100644 --- a/src/components/DashboardCard/index.tsx +++ b/src/components/DashboardCard/index.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { RouteComponentProps, withRouter } from 'react-router-dom'; +import { RouteComponentProps, useHistory, withRouter } from 'react-router-dom'; import Icon from '../Icon'; import { trackPageView } from '@/utils/stat'; import './index.less'; @@ -8,50 +8,45 @@ interface IProps extends RouteComponentProps { title: React.ElementRef; children: any; viewPath?: string; - type?: string; onConfigPanel?: () => void; } -class DashboardCard extends React.PureComponent { - handleViewDetail = () => { - const { viewPath, type } = this.props; +function DashboardCard(props: IProps) { + const { viewPath, title, children, onConfigPanel } = props; + const history = useHistory(); + + const handleViewDetail = () => { if (!viewPath) return; - if (type) { - localStorage.setItem('detailType', type); - } trackPageView(viewPath); - this.props.history.push(viewPath); + history.push(viewPath); }; - render() { - const { title, children, onConfigPanel, viewPath } = this.props; - return ( -
-
-
-

{title}

- { - viewPath && ( - - ) - } - {onConfigPanel && ( + return ( +
+
+
+

{title}

+ { + viewPath && ( - )} -
-
{children}
+ ) + } + {onConfigPanel && ( + + )}
+
{children}
- ); - } +
+ ); } export default withRouter(DashboardCard); diff --git a/src/config/locale/en-US/common.json b/src/config/locale/en-US/common.json index 880b646c..37a4669b 100644 --- a/src/config/locale/en-US/common.json +++ b/src/config/locale/en-US/common.json @@ -49,5 +49,17 @@ }, "addhostSuccess": "{host} add host success", "successDelay": "Success, Please refresh the page later to view", - "running": "Running" + "running": "Running", + "panelName": "Panel Name", + "panelNamePlaceholder": "Please enter a panel name", + "panelMetricType": "Metric Type", + "panelMetricModal": { + "PERCENTAGE": "percentage", + "BYTE": "byte", + "BYTE_SECOND": "byte/s", + "DISK_IO_NET": "io/s", + "NUMBER": "number", + "numberSecond": "number/s", + "latency": "us/ms/s" + } } \ No newline at end of file diff --git a/src/config/locale/zh-CN/common.json b/src/config/locale/zh-CN/common.json index 3c9d13a4..961d13f7 100644 --- a/src/config/locale/zh-CN/common.json +++ b/src/config/locale/zh-CN/common.json @@ -49,5 +49,17 @@ }, "addhostSuccess": "{host} add host 成功", "successDelay": "操作成功,请稍后刷新页面查看", - "running": "运行中" + "running": "运行中", + "panelName": "面板名称", + "panelNamePlaceholder": "请输入监控面板名称", + "panelMetricType": "监控指标类型", + "panelMetricModal": { + "PERCENTAGE": "百分比", + "BYTE": "字节", + "BYTE_SECOND": "字节/秒", + "DISK_IO_NET": "io/秒", + "NUMBER": "数值", + "numberSecond": "数值/秒", + "latency": "秒/微妙/毫秒" + } } \ No newline at end of file diff --git a/src/pages/ServiceDashboard/OverviewTable.tsx b/src/pages/ServiceDashboard/OverviewTable.tsx index 0e82e3b4..0ada4a9d 100644 --- a/src/pages/ServiceDashboard/OverviewTable.tsx +++ b/src/pages/ServiceDashboard/OverviewTable.tsx @@ -5,12 +5,13 @@ import intl from 'react-intl-universal'; import { ServicePanelType } from './index'; import styles from './index.module.less'; -import { BatchQueryItem, ServiceName } from '@/utils/interface'; +import { BatchQueryItem, CellHealtyLevel, ServiceName } from '@/utils/interface'; import { connect } from 'react-redux'; import { getClusterPrefix, VALUE_TYPE } from '@/utils/promQL'; import { asyncBatchQueries } from '@/requests'; import { getAutoLatency, getProperByteDesc } from '@/utils/dashboard'; import EventBus from '@/utils/EventBus'; +import { calcNodeHealty } from '@/utils'; interface OverviewTableData { serviceName: string; @@ -30,7 +31,6 @@ const mapDispatch: any = (_dispatch: any) => ({ }); const mapState = (state: any) => ({ - serviceMetric: state.serviceMetric, ready: state.serviceMetric.ready, cluster: state.cluster.cluster }); @@ -45,11 +45,17 @@ interface IProps const CellWidth = 150; +const Percent_Range = { + [CellHealtyLevel.normal]: [0, 40], + [CellHealtyLevel.warning]: [40, 80], + [CellHealtyLevel.danger]: [80, 100], +} + const ShowedServices: string[] = [ServiceName.GRAPHD, ServiceName.METAD, ServiceName.STORAGED]; function OverviewTable(props: IProps) { - const { serviceMap, serviceMetric, cluster } = props; + const { serviceMap, cluster } = props; const [loading, setLoading] = useState(false); const [frequencyValue, setFrequencyValue] = useState(0); const [dataSource, setDataSource] = useState([]); @@ -99,7 +105,7 @@ function OverviewTable(props: IProps) { } } - const renderCell = (text: string, type: VALUE_TYPE = VALUE_TYPE.byte) => { + const renderCell = (text: string, type: VALUE_TYPE = VALUE_TYPE.byte, shouldCalcNodeHealty: boolean = false) => { if (!text) return
-
let showText: string = ''; switch (type) { @@ -118,7 +124,12 @@ function OverviewTable(props: IProps) { default: break; } - return
{showText}
+ let level: CellHealtyLevel = CellHealtyLevel.normal ; + if (showText?.includes('%')) { + const num = parseFloat(text.slice(0, text.length - 1)); + level = calcNodeHealty(num); + } + return
{showText}
} useEffect(() => { @@ -137,19 +148,7 @@ function OverviewTable(props: IProps) { title: intl.get('device.serviceResource.context_switches_total'), dataIndex: "context_switches_total", render: (text, _) => renderCell(text, VALUE_TYPE.number), - width: CellWidth - }, - { - title: intl.get('device.serviceResource.cpu_seconds_total'), - dataIndex: "cpu_seconds_total", - render: (text, _) => renderCell(text, VALUE_TYPE.percentage), - width: CellWidth - }, - { - title: intl.get('device.serviceResource.memory_bytes_gauge'), - dataIndex: "memory_bytes_gauge", - render: (text, _) => renderCell(text), - width: CellWidth + width: 110 }, { title: intl.get('device.serviceResource.read_bytes_total'), @@ -170,6 +169,25 @@ function OverviewTable(props: IProps) { dataIndex: "open_filedesc_gauge", ellipsis: true, render: (text, _) => renderCell(text, VALUE_TYPE.number), + width: 110 + }, + { + title: intl.get('device.serviceResource.cpu_seconds_total'), + dataIndex: "cpu_seconds_total", + render: (text, _) => renderCell(text, VALUE_TYPE.percentage, true), + width: CellWidth + }, + { + title: intl.get('device.serviceResource.memory_bytes_gauge'), + dataIndex: "memory_bytes_gauge", + render: (text, record) => { + const value = getProperByteDesc(parseInt(text)).desc; + const percent = (parseInt(text) / parseInt(record['memory_total']) as any).toFixed(3) * 100; + const level: CellHealtyLevel = calcNodeHealty(percent, Percent_Range); + return ( +
{value} ({percent}%)
+ ); + }, width: CellWidth }, { @@ -180,7 +198,7 @@ function OverviewTable(props: IProps) { const isRunning = text === '1'; return (
- {isRunning ? intl.get('device.serviceResource.running') : intl.get('device.serviceResource.exit')} + {isRunning ? intl.get('device.serviceResource.running') : intl.get('device.serviceResource.exit')}
) }, @@ -207,31 +225,67 @@ function OverviewTable(props: IProps) { return queries; } + const getMachineQueries = (servicNames: string[]) => { + const hosts: string[] = [...new Set(servicNames + .map((service) => service.split('-')[0]))]; + const clusterSuffix1 = cluster ? `,${getClusterPrefix()}="${cluster.id}"` : ''; + const queries: BatchQueryItem[] = hosts.map(host => { + const instanceSuffix = `instance=~"^${host.replaceAll(".", "\.")}.*"`; + return ( + { + refId: `node$${host}$memory_total`, + query: `node_memory_MemTotal_bytes{${instanceSuffix}${clusterSuffix1}}` + } + ) + }) + return queries; + } + const asyncGetServiceOverviewData = async (shouldLoading?: boolean) => { if (!serviceMap[ServiceName.GRAPHD]) return; shouldLoading && setLoading(true); - const queries = getQueries(); - const data: any = await asyncBatchQueries(queries); - const { results } = data; const curDataSources: OverviewTableData[] = serviceMap[ServiceName.GRAPHD] .concat(serviceMap[ServiceName.METAD]) .concat(serviceMap[ServiceName.STORAGED]) .map(item => ({ serviceName: item })); + const machineInfoQueries: BatchQueryItem[] = getMachineQueries(curDataSources.map(item => item.serviceName)) + const queries = getQueries(); + const data: any = await asyncBatchQueries(queries.concat(machineInfoQueries)); + const { results } = data; Object.keys(results).forEach(refId => { - const [_serviceType, metricName] = refId.split('$'); - const metricItems = results[refId].result; - metricItems.forEach(({ metric, value }) => { - const curItem = curDataSources.find(item => item.serviceName === metric.instanceName); - if (curItem) { - curItem[metricName] = value[1]; - } else { - curDataSources.push({ - serviceName: metric.instanceName, - [metricName]: value[1] - }) - } - }) + if (refId.startsWith('node$')) { + const [_, machineHost, metricName] = refId.split('$'); + const metricItems = results[refId].result; + metricItems.forEach(({ metric, value }) => { + const curItems = curDataSources.filter(item => item.serviceName.includes(machineHost)); + if (curItems.length) { + curItems.forEach(item => { + item[metricName] = value[1]; + }) + } else { + curDataSources.push({ + serviceName: metric.instanceName, + [metricName]: value[1] + }) + } + }) + } else { + const [_serviceType, metricName] = refId.split('$'); + const metricItems = results[refId].result; + metricItems.forEach(({ metric, value }) => { + const curItem = curDataSources.find(item => item.serviceName === metric.instanceName); + if (curItem) { + curItem[metricName] = value[1]; + } else { + curDataSources.push({ + serviceName: metric.instanceName, + [metricName]: value[1] + }) + } + }) + } }); + setLoading(false); setDataSource(curDataSources); } diff --git a/src/pages/ServiceDashboard/ServiceOverview/index.tsx b/src/pages/ServiceDashboard/ServiceOverview/index.tsx index 38af68bc..e9f40f70 100644 --- a/src/pages/ServiceDashboard/ServiceOverview/index.tsx +++ b/src/pages/ServiceDashboard/ServiceOverview/index.tsx @@ -1,5 +1,6 @@ import React, { useEffect, useMemo, useRef, useState } from 'react'; import intl from 'react-intl-universal'; +import { connect } from 'react-redux'; import { BatchQueryItem, IServiceMetricItem, ServiceName } from '@/utils/interface'; import ServiceHeader from '@/components/Service/ServiceHeader'; @@ -12,11 +13,12 @@ import styles from './index.module.less'; import { getClusterPrefix } from '@/utils/promQL'; import { getQueryByMetricType } from '@/utils/metric'; import DashboardCard from '@/components/DashboardCard'; -import { connect } from 'react-redux'; + import { Spin } from 'antd'; import { DashboardSelect, Option } from '@/components/DashboardSelect'; import EventBus from '@/utils/EventBus'; + const mapDispatch: any = (_dispatch: any) => ({ }); @@ -33,10 +35,11 @@ interface IProps extends ReturnType, panelConfigData: ServicePanelConfig[]; timeRange: TIME_OPTION_TYPE | [number, number]; panelVisible?: boolean; + onEditPanel: (panelItem: ServicePanelConfig) => void; } function ServiceOverview(props: IProps) { - const { serviceType, panelConfigData, serviceNames, timeRange, cluster, ready, serviceMetric, panelVisible } = props; + const { serviceType, panelConfigData, serviceNames, timeRange, cluster, ready, serviceMetric, panelVisible, onEditPanel } = props; const [loading, setLoading] = useState(false); const [frequencyValue, setFrequencyValue] = useState(0); const [curPanelVisile, setCurPanelVisible] = useState(panelVisible || false); @@ -164,6 +167,10 @@ function ServiceOverview(props: IProps) { }); } + const getViewPath = (serviceType: string, serviceName: string, panelName: string) => { + return `/clusters/${cluster.id}/service-metric/${serviceType}/${serviceName}/${encodeURIComponent(panelName)}`; + } + return (
onEditPanel(configItem)} > metricRefs[index + 1] = ref} diff --git a/src/pages/ServiceDashboard/index.tsx b/src/pages/ServiceDashboard/index.tsx index 291a2749..bc0c69e0 100644 --- a/src/pages/ServiceDashboard/index.tsx +++ b/src/pages/ServiceDashboard/index.tsx @@ -9,7 +9,7 @@ import Icon from '@/components/Icon'; import { ServiceName } from '@/utils/interface'; import { ClusterServiceNameMap } from '@/utils/metric'; -import { defaultServicePanelConfigData } from './defaultPanelConfig'; +import { defaultServicePanelConfigData, ServicePanelConfig, ServicePanelConfigItem } from './defaultPanelConfig'; import ServiceOverview from './ServiceOverview'; import styles from './index.module.less'; @@ -20,21 +20,25 @@ const mapDispatch: any = (_dispatch: any) => ({ const mapState = (state: any) => ({ loading: state.loading.effects.nebula.asyncBatchQueries, + cluster: state.cluster.cluster, }); interface IProps extends ReturnType, ReturnType { - cluster?: any; + enableAddPanel?: boolean; + onAddPanel?: () => void; + onEditPanel?: (panelItem: ServicePanelConfig, serviceName: ServiceName) => void; + panelConfigs?: ServicePanelConfigItem[]; } -const ServicePanels = [ +export const ServicePanels = [ ServiceName.GRAPHD, ServiceName.STORAGED, ServiceName.METAD, - ClusterServiceNameMap[ServiceName.MetadListener], - ClusterServiceNameMap[ServiceName.StoragedListener], - ClusterServiceNameMap[ServiceName.Drainer], + // ClusterServiceNameMap[ServiceName.MetadListener], + // ClusterServiceNameMap[ServiceName.StoragedListener], + // ClusterServiceNameMap[ServiceName.Drainer], ] export type ServicePanelType = { @@ -42,7 +46,7 @@ export type ServicePanelType = { }; function ServiceDashboard(props: IProps) { - const { cluster } = props; + const { cluster, enableAddPanel, onAddPanel, panelConfigs, onEditPanel } = props; const [timeRange, setTimeRange] = useState(TIME_OPTION_TYPE.HOUR1); @@ -56,7 +60,15 @@ function ServiceDashboard(props: IProps) { if (cluster.id) { getServiceNames(); } - }, [cluster]) + }, [cluster]); + + const [curPanelConfigs, setCurPanelConfigs] = useState(panelConfigs || defaultServicePanelConfigData); + + useEffect(() => { + if (panelConfigs) { + setCurPanelConfigs(panelConfigs); + } + }, [panelConfigs]) const getServiceNames = () => { const serviceTypeMap: ServicePanelType = {}; @@ -67,6 +79,10 @@ function ServiceDashboard(props: IProps) { setCurServiceMap(serviceTypeMap); } + const handleEditServicePanel = (serviceName: ServiceName) => (configItem: ServicePanelConfig) => { + onEditPanel && onEditPanel(configItem, serviceName); + } + return (
{/* @ts-ignore */} @@ -76,45 +92,36 @@ function ServiceDashboard(props: IProps) {
{intl.get('device.serviceResource.singleServiceTitle')}
- + { + enableAddPanel && ( + + ) + }
-
- item.type === ServiceName.GRAPHD)?.panels || []} - /> -
-
- item.type === ServiceName.METAD)?.panels || []} - /> -
-
- item.type === ServiceName.STORAGED)?.panels || []} - /> -
+ { + curPanelConfigs.map((panelConfig, index) => ( +
+ +
+ )) + }
); } diff --git a/src/utils/index.ts b/src/utils/index.ts index e8b740b3..f9a58558 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -106,8 +106,8 @@ export const getMenuPathByKey = (menuList: any[], activeKey: string): string[] = return path; } -export const calcNodeHealty = (percent: number) => { - const levels = Object.keys(Percent_Range); +export const calcNodeHealty = (percent: number, rangeMap = Percent_Range) => { + const levels = Object.keys(rangeMap); let level: CellHealtyLevel = CellHealtyLevel.normal ; for (let index = 0; index < levels.length; index++) { const key = levels[index]; diff --git a/src/utils/promQL.ts b/src/utils/promQL.ts index 1eb36384..6991b152 100644 --- a/src/utils/promQL.ts +++ b/src/utils/promQL.ts @@ -3,7 +3,7 @@ */ import intl from 'react-intl-universal'; -export const enum VALUE_TYPE { +export enum VALUE_TYPE { percentage = 'PERCENTAGE', byte = 'BYTE', byteSecond = 'BYTE_SECOND',