diff --git a/web/app/components/base/line-chart/index.tsx b/web/app/components/base/line-chart/index.tsx index fd3a7b7d75..35bd0e1770 100644 --- a/web/app/components/base/line-chart/index.tsx +++ b/web/app/components/base/line-chart/index.tsx @@ -2,62 +2,99 @@ import React, { memo, useEffect, useRef, useState } from 'react' import ReactECharts from 'echarts-for-react' import { useTranslation } from 'react-i18next' import dayjs from 'dayjs' +const formatMGT = (y) => { + const yy = Math.abs(y) + if (yy >= 1024 * 1024) { + return y < 0 + ? `${-1 * +(yy / (1024 * 1024)).toFixed(2)}T` + : `${(yy / (1024 * 1024)).toFixed(2)}T` + } + else if (yy >= 1024) { + return y < 0 ? `${-1 * +(yy / 1024).toFixed(2)}G` : `${(yy / 1024).toFixed(2)}G` + } + else if (yy < 1024 && yy >= 1) { + return y < 0 ? `${-1 * +yy.toFixed(2)}M` : `${yy.toFixed(2)}M` + } + else if (yy < 1 && yy > 0) { + return y < 0 ? `${-1 * yy}M` : `${yy}M` + } + else if (yy === 0) { + return 0 + } + else { + return `${yy}M` + } +} export const adjustAlpha = (color: string, alpha: number) => { const rgba = color.match(/\d+/g) return `rgba(${rgba[0]}, ${rgba[1]}, ${rgba[2]}, ${alpha})` } const formatTimeUTC = (value: number) => { - return dayjs(value * 1000).format('HH:mm:ss') + return dayjs(value).format('HH:mm:ss') } const formatDay = (value: number) => { return dayjs(value).format('YYYY-MM-DD HH:mm:ss') } -type LineChartData = { +type ChartData = { [key: string]: number; } -type LineChartType = 'cpu' | 'network' | 'memory' -type LineChartProps = { - data: LineChartData - type: LineChartType -} -const DelayLineChartTitleMap: Record = { - cpu: 'cpu指标', - network: '网络时延', - memory: '内存占用', +type LineChartData = { + legend: string + legendFormat: string + labels: { + [key: string]: string; + } + chart: { + chartData: ChartData + } } -const MetricsLineChartColor = { - network: 'rgba(212, 164, 235, 1)', - memory: 'rgba(212, 164, 235, 1)', - cpu: 'rgba(55, 162,235, 1)', +type LineChartProps = { + data: LineChartData[] + unit: string } +// const DelayLineChartTitleMap: Record = { +// cpu: 'cpu指标', +// network: '网络时延', +// memory: '内存占用', +// } +// const MetricsLineChartColor = { +// network: 'rgba(212, 164, 235, 1)', +// memory: 'rgba(212, 164, 235, 1)', +// cpu: 'rgba(55, 162,235, 1)', +// } export const YValueMinInterval = { network: 0.01, memory: 0.01, cpu: 0.01, } -const LineChart = ({ type, data }: LineChartProps) => { +const LineChart = (props: LineChartProps) => { + const { data, unit } = props const { t } = useTranslation() const chartRef = useRef(null) const convertYValue = (value: number) => { - switch (type) { - case 'network': - if (value > 0 && value < 0.01) - return '< 0.01 s' - return `${value}s` - case 'memory': + switch (unit) { + // case 'network': + // if (value > 0 && value < 0.01) + // return '< 0.01 s' + // return `${value}s` + // case 'memory': + // if (value > 0 && value < 0.01) + // return '< 0.01 bytes' + // return `${value}bytes` + case 'percent': if (value > 0 && value < 0.01) - return '< 0.01 bytes' - return `${value}bytes` - case 'cpu': - if (value > 0 && value < 0.0001) return '< 0.01%' - return `${Number.parseFloat((value * 100).toFixed(2))}%` - // case 'tps': - // if (value > 0 && value < 0.01) - // return `< 0.01${t('units.timesPerMinute')}` - // return Number.parseFloat((Math.floor(value * 100) / 100).toString()) + t('units.timesPerMinute') + return `${Number.parseFloat((value).toFixed(2))}%` + case 'bytes': + if (value > 0 && value < 0.01) + return `< 0.01 ${unit}` + return `${formatMGT(Number.parseFloat((value).toFixed(2)))} ${unit}` + default: + if (value > 0 && value < 0.01) + return `< 0.01 ${unit}` + return `${Number.parseFloat((value).toFixed(2))} ${unit}` } } const [option, setOption] = useState({ @@ -76,14 +113,14 @@ const LineChart = ({ type, data }: LineChartProps) => { if (axisDimension === 'y') return convertYValue(value) else - return formatDay(value * 1000) + return formatDay(value) // return `自定义格式: ${params.value}`; }, }, }, formatter: (params) => { - let result = `
${formatDay(params.data[0] * 1000)}
+ let result = `
${formatDay(params.data[0])}
` result += `
@@ -114,14 +151,9 @@ const LineChart = ({ type, data }: LineChartProps) => { left: '3%', right: '4%', bottom: '3%', - top: '4%', + top: '15%', containLabel: true, }, - // toolbox: { - // feature: { - // saveAsImage: {}, - // }, - // }, xAxis: { type: 'time', boundaryGap: false, @@ -136,27 +168,16 @@ const LineChart = ({ type, data }: LineChartProps) => { return formatTimeUTC(value) }, }, - // axisLine: { - // lineStyle: { - // color: '#000000', // 设置 x 轴刻度线颜色 - // }, - // }, - // axisTick: { - // lineStyle: { - // color: '#000000', // 设置 x 轴刻度线颜色 - // }, - // }, }, yAxis: { type: 'value', - minInterval: YValueMinInterval[type], + minInterval: 0.01, min: 0, axisLabel: { formatter(value: number) { - if(type === 'cpu') - return Number.parseFloat((value * 100).toFixed(2)) - - return value + if(unit === 'bytes') + return formatMGT(Number.parseFloat((value).toFixed(2))) + return Number.parseFloat((value).toFixed(2)) }, }, }, @@ -174,25 +195,9 @@ const LineChart = ({ type, data }: LineChartProps) => { }, }) - // // 处理缺少数据的时间点并补0 - // const fillMissingData = () => { - // const filledData = [] - // const { startTime, endTime } = timeRange - // const step = getStep(startTime, endTime) - - // for (let time = startTime; time <= endTime; time += step) { - // filledData.push({ - // timestamp: time, // 用于显示在图例中 - // value: data[time] || 0, - // }) - // } - // return filledData - // } - useEffect(() => { if (data) { // const filledData = fillMissingData() - setOption({ ...option, xAxis: { @@ -211,18 +216,22 @@ const LineChart = ({ type, data }: LineChartProps) => { // min: timeRange.startTime / 1000, // max: timeRange.endTime / 1000, }, - series: [ - { - data: Object.entries(data).map(([key, value]) => [Number(key) / 1000, value]), - type: 'line', - smooth: true, - name: DelayLineChartTitleMap[type], - color: MetricsLineChartColor[type], - areaStyle: { - color: adjustAlpha(MetricsLineChartColor[type], 0.3), // 设置区域填充颜色 - }, - }, - ], + legend: { + type: 'scroll', + data: data.map(item => item.legend), + }, + series: data.map(item => ({ + data: Object.entries(item.chart.chartData) + .map(([key, value]) => [Number(key) / 1000, value]) // 处理微秒级时间戳 + .sort((a, b) => a[0] - b[0]), + type: 'line', + smooth: true, + name: item.legend, + showSymbol: false, + // areaStyle: { + // color: adjustAlpha(MetricsLineChartColor[type], 0.3), // 设置区域填充颜色 + // }, + })), }) } }, [data]) diff --git a/web/app/components/workflow/apo/tool-preview/apo-tools-preview.tsx b/web/app/components/workflow/apo/tool-preview/apo-tools-preview.tsx index 66663abacd..c373293e07 100644 --- a/web/app/components/workflow/apo/tool-preview/apo-tools-preview.tsx +++ b/web/app/components/workflow/apo/tool-preview/apo-tools-preview.tsx @@ -10,6 +10,7 @@ import ParametersInfo from './parameters-info' import cn from '@/utils/classnames' import { debounce } from 'lodash-es' import { useGetLanguage } from '@/context/i18n' +import Button from '@/app/components/base/button' type ApoToolsPreviewProps = { onSelect: (type: BlockEnum, tool?: ToolDefaultValue) => void; @@ -33,7 +34,27 @@ const ApoToolsPreview = ({ onSelect, apoToolType, hidePopover }: ApoToolsPreview const handlePopoverOpen = useCallback((item) => { setToolDetail(item) }, []) - + const handleSelect = (item) => { + const params: Record = {} + if (item.parameters) { + item.parameters.forEach((param) => { + params[param.name] = '' + }) + } + onSelect(BlockEnum.Tool, { + provider_id: provider.id, + provider_type: provider.type, + provider_name: provider.name, + tool_name: item.name, + tool_label: item.label[language], + title: item.label[language], + is_team_authorization: provider.is_team_authorization, + output_schema: item.output_schema, + paramSchemas: item.parameters, + params, + }) + hidePopover() + } const convertMetricsListToMenuItems = useCallback(() => { return tools.map((item, index) => (
handlePopoverOpen(item)} - onClick={() => { - const params: Record = {} - if (item.parameters) { - item.parameters.forEach((param) => { - params[param.name] = '' - }) - } - onSelect(BlockEnum.Tool, { - provider_id: provider.id, - provider_type: provider.type, - provider_name: provider.name, - tool_name: item.name, - tool_label: item.label[language], - title: item.label[language], - is_team_authorization: provider.is_team_authorization, - output_schema: item.output_schema, - paramSchemas: item.parameters, - params, - }) - hidePopover() - }} >
{/* -
- {/* left */} -
-
- setSearchText(e.target.value)} - /> -
-
- {tools?.length > 0 && convertMetricsListToMenuItems()} -
+
+ {/* left */} +
+
+ setSearchText(e.target.value)} + />
-
- {toolDetail && ( -
-
{toolDetail.label[language]}
-
-
- 描述:<>{toolDetail.description[language]} -
-
- {apoToolType === 'select' ? ( - <> - {['cpu', 'network', 'memory'].includes(toolDetail?.display.type) &&
单位:{toolDetail.display.unit}
} - {toolDetail?.display?.type &&
输出:{t(`apo.displayType.${toolDetail?.display?.type}`)}
} -
-
-
-
- -
+
+ {tools?.length > 0 && convertMetricsListToMenuItems()} +
+
+ {toolDetail && (
+ +
+
+
{toolDetail.label[language]}
+ +
+
+
+ 描述:<>{toolDetail.description[language]} +
+
+ {apoToolType === 'apo_select' ? ( + <> + {toolDetail?.display.type === 'metric' &&
单位:{toolDetail.display.unit}
} + {toolDetail?.display?.type &&
输出:{t(`apo.displayType.${toolDetail?.display?.type}`)}
} +
+
+
+
+ +
- - ) : ( -
- 输入参数: -
- {toolDetail?.parameters.map(parameter => ( - - ))} -
-
- )} + + ) : ( +
+ 输入参数: +
+ {toolDetail?.parameters.map(parameter => ( + + ))} +
-
+ )}
- )} +
+ )}
) } diff --git a/web/app/components/workflow/apo/tool-preview/parameters-info.tsx b/web/app/components/workflow/apo/tool-preview/parameters-info.tsx index 42ac2c65fc..6b425ef7e3 100644 --- a/web/app/components/workflow/apo/tool-preview/parameters-info.tsx +++ b/web/app/components/workflow/apo/tool-preview/parameters-info.tsx @@ -1,10 +1,8 @@ -import { memo, useContext } from 'react' -import I18n from '@/context/i18n' -import { getLanguage } from '@/i18n/language' +import { useGetLanguage } from '@/context/i18n' +import { memo } from 'react' import { useTranslation } from 'react-i18next' const ParametersInfo = ({ parameter }) => { - const { locale } = useContext(I18n) - const language = getLanguage(locale) + const language = useGetLanguage() const { t } = useTranslation() const getType = (type: string) => { if (type === 'number-input') diff --git a/web/app/components/workflow/apo/tool-preview/tool-trial-run.tsx b/web/app/components/workflow/apo/tool-preview/tool-trial-run.tsx index 00faa8bfa7..3a9f75a797 100644 --- a/web/app/components/workflow/apo/tool-preview/tool-trial-run.tsx +++ b/web/app/components/workflow/apo/tool-preview/tool-trial-run.tsx @@ -5,6 +5,11 @@ import { InputVarType } from '../../types' import ParametersInfo from './parameters-info' import { useTranslation } from 'react-i18next' import { useGetLanguage } from '@/context/i18n' +import Button from '@/app/components/base/button' +import { RiLoader2Line } from '@remixicon/react' +import { testApoTools } from '@/service/tools' +import DataDisplay from '../../run/data-display' +const i18nPrefix = 'workflow.singleRun' const varTypeToInputVarType = (type: string) => { if(type === 'string') return InputVarType.textInput @@ -12,11 +17,14 @@ const varTypeToInputVarType = (type: string) => { return InputVarType.singleFile return type } -const ToolTrialRun = ({ infoSchemas }) => { +const ToolTrialRun = ({ infoSchemas, type, title }) => { const [formValues, setFormValues] = useState(null) const [formInputs, setFormInputs] = useState([]) const language = useGetLanguage() const { t } = useTranslation() + + const [isRunning, setIsRunning] = useState(false) + const [displayData, setDisplayData] = useState(null) useEffect(() => { const formValues = infoSchemas?.reduce((acc, item) => { acc[item.name] = null @@ -32,11 +40,39 @@ const ToolTrialRun = ({ infoSchemas }) => { })) setFormValues(formValues) setFormInputs(formInputs) + setDisplayData(null) }, [infoSchemas]) + const handleRun = async () => { + const data = await testApoTools({ + type, + title, + params: formValues, + startTime: formValues?.startTime, + endTime: formValues?.endTime, + }) + if(data?.data?.code) + setDisplayData({ type: 'error', data: data.data?.message }) + + else + setDisplayData(data) + } return <> -
{t('tools.setBuiltInTools.parameters')}
+
+ {/* {t('tools.setBuiltInTools.parameters')} */} + 数据测试 +
setFormValues(newValues) } >
+
+ +
+ { + displayData && + } + } export default memo(ToolTrialRun) diff --git a/web/app/components/workflow/block-selector/apoTools.tsx b/web/app/components/workflow/block-selector/apoTools.tsx index ab5ec32bc5..5f5c7748e3 100644 --- a/web/app/components/workflow/block-selector/apoTools.tsx +++ b/web/app/components/workflow/block-selector/apoTools.tsx @@ -1,20 +1,25 @@ -import type { BlockEnum } from '../types' +import { BlockEnum } from '../types' import cn from '@/utils/classnames' -import { initApoToolsEntry } from '../apo/constant' -import { useState } from 'react' +import { useEffect, useState } from 'react' import type { ToolDefaultValue } from './types' import { Popover } from 'antd' import ApoToolsPreview from '../apo/tool-preview/apo-tools-preview' import type { ApoToolTypeInfo } from '../apo/types' +import { fetchApoNode } from '@/service/tools' +import { useGetLanguage } from '@/context/i18n' +import BlockIcon from '../block-icon' type APOToolsProps = { searchText: string; onSelect: (type: BlockEnum, tool?: ToolDefaultValue) => void } const APOTools = ({ searchText, onSelect }: APOToolsProps) => { + const language = useGetLanguage() const [openKey, setOpenKey] = useState({ - select: false, - analysis: false, + apo_select: false, + apo_analysis: false, + apo_rule: false, }) + const [apoNodes, setApoNodes] = useState([]) const hide = (key: ApoToolTypeInfo) => { setOpenKey(prev => ({ ...prev, @@ -28,31 +33,39 @@ const APOTools = ({ searchText, onSelect }: APOToolsProps) => { [key]: newOpen, })) } + + const getApoNodes = async () => { + const data = await fetchApoNode() + setApoNodes(data) + } + useEffect(() => { + getApoNodes() + }, []) return ( <>
- {Object.entries(initApoToolsEntry).map(([key, tool]) => ( + {apoNodes?.map(node => ( handleOpenChange(key, newOpen)} + open={openKey[node.id]} + onOpenChange={newOpen => handleOpenChange(node.id, newOpen)} content={ - hide(key)}/> + hide(node.name)}/> } >
- {/* */} + toolIcon={node.icon} + />
- {tool.label} + {node?.label[language]}
diff --git a/web/app/components/workflow/block-selector/index.tsx b/web/app/components/workflow/block-selector/index.tsx index 532d4c154c..c359f4b479 100644 --- a/web/app/components/workflow/block-selector/index.tsx +++ b/web/app/components/workflow/block-selector/index.tsx @@ -99,41 +99,45 @@ const NodeSelector: FC = ({ return t('workflow.tabs.searchTool') return '' }, [activeTab, t]) - const TabContent = () =>
-
e.stopPropagation()}> - {activeTab === TabsEnum.Blocks && ( - setSearchText(e.target.value)} - onClear={() => setSearchText('')} - /> - )} - {activeTab === TabsEnum.Tools && ( - - )} + const TabContent = () =>
+
e.stopPropagation()}> + {activeTab === TabsEnum.Blocks && ( + setSearchText(e.target.value)} + onClear={() => setSearchText('')} + /> + )} + {activeTab === TabsEnum.Tools && ( + + )} -
- -
+
+
+ + +
+ +
return absolute ? = ({ const { data: workflowTools } = useAllWorkflowTools() return ( -
e.stopPropagation()}> +
e.stopPropagation()} className='h-full flex flex-col'> { !noBlocks && (
@@ -59,12 +59,12 @@ const Tabs: FC = ({ } { activeTab === TabsEnum.Blocks && !noBlocks && ( - <> +
+ />
) } { diff --git a/web/app/components/workflow/run/data-display.tsx b/web/app/components/workflow/run/data-display.tsx index b9554c030c..c63c27d532 100644 --- a/web/app/components/workflow/run/data-display.tsx +++ b/web/app/components/workflow/run/data-display.tsx @@ -7,26 +7,32 @@ import AlertList from '../../base/alert' import LogContent from '../../base/log/LogContent' type DataDisplayProps = { - data: string; + data?: string; + dataObj?: any } const TopologyCom = ({ data }) => { const topologyData = usePrepareTopologyData(data) return
} -const DataDisplay = ({ data }: DataDisplayProps) => { +const DataDisplay = ({ data, dataObj }: DataDisplayProps) => { // const [chartData,setChartData] = useState(null) const { t } = useTranslation() let chartData: any = null - try { - chartData = JSON.parse(data) - if(chartData.data?.message) chartData = { type: 'error', data: chartData.data?.message } + if(data) { + try { + chartData = JSON.parse(data) + if(chartData.data?.message) chartData = { type: 'error', data: chartData.data?.message } + } + catch (error) { + console.error('JSON 解析失败:', error) + chartData = {} // 返回一个空对象或其他默认值 + } } - catch (error) { - console.error('JSON 解析失败:', error) - chartData = {} // 返回一个空对象或其他默认值 + else{ + chartData = dataObj } - console.log(chartData) + return (
{ @@ -36,8 +42,8 @@ const DataDisplay = ({ data }: DataDisplayProps) => { {t('apo.chart.chartTitle')}
- {['cpu', 'network', 'memory'].includes(chartData?.type) && ( - + {chartData?.type === 'metric' && ( + )} {chartData?.type === 'topology' && ( diff --git a/web/config/index.ts b/web/config/index.ts index e7e66e409e..0f882267e6 100644 --- a/web/config/index.ts +++ b/web/config/index.ts @@ -43,7 +43,7 @@ if(process.env.NEXT_PUBLIC_V1_API_PREFIX) v1ApiPrefix = process.env.NEXT_PUBLIC_V1_API_PREFIX else if(globalThis.document?.body?.getAttribute('data-v1-api-prefix')) - v1ApiPrefix = globalThis.document?.body?.getAttribute('data-v1-api-prefix') + v1ApiPrefix = globalThis.document?.body?.getAttribute('data-v1-api-prefix') else v1ApiPrefix = 'http://localhost:5001/v1' diff --git a/web/i18n/en-US/apo.ts b/web/i18n/en-US/apo.ts index ed5a720b78..66f0b2bc6e 100644 --- a/web/i18n/en-US/apo.ts +++ b/web/i18n/en-US/apo.ts @@ -14,6 +14,7 @@ const translation = { alert: 'Alert events', log: 'Log data', custom: 'Custom query return data', + metric: 'Metric Line Chart', }, } diff --git a/web/i18n/zh-Hans/apo.ts b/web/i18n/zh-Hans/apo.ts index 6cb3c99e77..a01190f8d8 100644 --- a/web/i18n/zh-Hans/apo.ts +++ b/web/i18n/zh-Hans/apo.ts @@ -14,6 +14,7 @@ const translation = { alert: '告警事件', log: '日志数据', custom: '自定义查询返回数据', + metric: '指标折线图', }, } diff --git a/web/service/tools.ts b/web/service/tools.ts index b5ced86b0f..3fa675dca1 100644 --- a/web/service/tools.ts +++ b/web/service/tools.ts @@ -167,3 +167,10 @@ export const fetchApoTools = (apoToolType: string, queryText: string) => { }, }) } +export const testApoTools = (params: any) => { + return post('/workspaces/current/tools/apo/preview', { body: params }) +} + +export const fetchApoNode = () => { + return get('/workspaces/current/tools/apo/list') +}