feat: apo tool test run shpw data display

pull/17608/head
keting lu 1 year ago
parent 7dd1ee3691
commit 49680f909e

@ -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<LineChartType, string> = {
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<LineChartType, string> = {
// 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<any>({
@ -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 = `<div class="rgb(102, 102, 102)">${formatDay(params.data[0] * 1000)}<br/></div>
let result = `<div class="rgb(102, 102, 102)">${formatDay(params.data[0])}<br/></div>
<div class="overflow-hidden w-full " >`
result += `<div class="flex flex-row items-center justify-between">
<div class="flex flex-row items-center flex-nowrap flex-shrink flex-1 whitespace-normal break-words">
@ -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])

@ -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<string, string> = {}
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) => (
<div
@ -43,27 +64,6 @@ const ApoToolsPreview = ({ onSelect, apoToolType, hidePopover }: ApoToolsPreview
item.name === toolDetail?.name && 'bg-state-base-active text-red-500',
)}
onMouseEnter={() => handlePopoverOpen(item)}
onClick={() => {
const params: Record<string, string> = {}
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()
}}
>
<div className="flex grow items-center h-8">
{/* <BlockIcon
@ -91,60 +91,63 @@ const ApoToolsPreview = ({ onSelect, apoToolType, hidePopover }: ApoToolsPreview
}
}, [apoToolType, searchText])
return (
<div>
<div className="flex">
{/* left */}
<div className="w-[300px]">
<div className="mb-3">
<Input
value={searchText}
onChange={e => setSearchText(e.target.value)}
/>
</div>
<div className="p-0.5 space-y-0.5 rounded-[10px]">
{tools?.length > 0 && convertMetricsListToMenuItems()}
</div>
<div className="flex max-h-[70vh] overflow-hidden">
{/* left */}
<div className="w-[300px] h-full flex flex-col shrink-0">
<div className="mb-3">
<Input
value={searchText}
onChange={e => setSearchText(e.target.value)}
/>
</div>
<div className="flex-1">
{toolDetail && (
<div className="px-4">
<div>{toolDetail.label[language]}</div>
<div className="text-xs text-text-tertiary">
<div className="pt-2">
<>{toolDetail.description[language]}</>
</div>
<div className="pt-2">
{apoToolType === 'select' ? (
<>
{['cpu', 'network', 'memory'].includes(toolDetail?.display.type) && <div className="pb-2">{toolDetail.display.unit}</div>}
{toolDetail?.display?.type && <div>{t(`apo.displayType.${toolDetail?.display?.type}`)}</div>}
<div className="px-4 py-2">
<div className="h-[0.5px] divider-subtle" />
</div>
<div className="pb-2">
<ToolTrialRun infoSchemas={toolDetail.parameters} />
</div>
<div className="p-0.5 space-y-0.5 rounded-[10px] shrink grow overflow-y-auto">
{tools?.length > 0 && convertMetricsListToMenuItems()}
</div>
</div>
{toolDetail && (<div className="flex-1 min-w-[500px] overflow-auto max-w-[800px]">
<div className="px-4">
<div className='flex'>
<div className='flex-1'>{toolDetail.label[language]}</div>
<Button variant='primary' className=' space-x-2' onClick={() => handleSelect(toolDetail)}>
<div>使</div>
</Button>
</div>
<div className="text-xs text-text-tertiary">
<div className="pt-2">
<>{toolDetail.description[language]}</>
</div>
<div className="pt-2">
{apoToolType === 'apo_select' ? (
<>
{toolDetail?.display.type === 'metric' && <div className="pb-2">{toolDetail.display.unit}</div>}
{toolDetail?.display?.type && <div>{t(`apo.displayType.${toolDetail?.display?.type}`)}</div>}
<div className="px-4 py-2">
<div className="h-[0.5px] divider-subtle" />
</div>
<div className="pb-2">
<ToolTrialRun infoSchemas={toolDetail.parameters} type={toolDetail.display?.type} title={toolDetail?.display?.title}/>
</div>
</>
) : (
<div className="flex">
<span></span>
<div>
{toolDetail?.parameters.map(parameter => (
<ParametersInfo
key={parameter.name}
parameter={parameter}
/>
))}
</div>
</div>
)}
</>
) : (
<div className="flex">
<span></span>
<div>
{toolDetail?.parameters.map(parameter => (
<ParametersInfo
key={parameter.name}
parameter={parameter}
/>
))}
</div>
</div>
</div>
)}
</div>
)}
</div>
</div>
</div>
)}
</div>
)
}

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

@ -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 <>
<div className='py-2 text-text-primary system-sm-semibold-uppercase'>{t('tools.setBuiltInTools.parameters')}</div>
<div className='py-2 text-text-primary system-sm-semibold-uppercase'>
{/* {t('tools.setBuiltInTools.parameters')} */}
</div>
<Form inputs={formInputs} values={formValues} onChange={newValues => setFormValues(newValues) }
></Form>
<div className='w-full flex pt-2'>
<Button disabled={isRunning} variant='primary' className='w-0 grow space-x-2' onClick={handleRun}>
{isRunning && <RiLoader2Line className='animate-spin w-4 h-4 text-white' />}
<div>{t(`${i18nPrefix}.${isRunning ? 'running' : 'startRun'}`)}</div>
</Button>
</div>
{
displayData && <DataDisplay dataObj={displayData} />
}
</>
}
export default memo(ToolTrialRun)

@ -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 (
<>
<div className="mb-1 last-of-type:mb-0">
{Object.entries(initApoToolsEntry).map(([key, tool]) => (
{apoNodes?.map(node => (
<Popover
key={key}
key={node.id}
placement='rightTop'
trigger={['click']}
open={openKey[key]}
onOpenChange={newOpen => handleOpenChange(key, newOpen)}
open={openKey[node.id]}
onOpenChange={newOpen => handleOpenChange(node.id, newOpen)}
content={
<ApoToolsPreview onSelect={onSelect} apoToolType={key} hidePopover={() => hide(key)}/>
<ApoToolsPreview onSelect={onSelect} apoToolType={node.name} hidePopover={() => hide(node.name)}/>
}
>
<div
key={tool.type}
key={node.id}
className="flex items-center px-3 w-full h-8 rounded-lg hover:bg-state-base-hover cursor-pointer"
>
{/* <BlockIcon
<BlockIcon
className="mr-2 shrink-0"
type={BlockEnum.Tool}
toolIcon={tool.icon}
/> */}
toolIcon={node.icon}
/>
<div className={cn('grow text-sm text-gray-900 truncate')}>
{tool.label}
{node?.label[language]}
</div>
</div>
</Popover>

@ -99,41 +99,45 @@ const NodeSelector: FC<NodeSelectorProps> = ({
return t('workflow.tabs.searchTool')
return ''
}, [activeTab, t])
const TabContent = () => <div className={`rounded-lg border-[0.5px] border-gray-200 bg-white shadow-lg ${popupClassName}`}>
<div className='px-2 pt-2' onClick={e => e.stopPropagation()}>
{activeTab === TabsEnum.Blocks && (
<Input
showLeftIcon
showClearIcon
autoFocus
value={searchText}
placeholder={searchPlaceholder}
onChange={e => setSearchText(e.target.value)}
onClear={() => setSearchText('')}
/>
)}
{activeTab === TabsEnum.Tools && (
<SearchBox
search={searchText}
onSearchChange={setSearchText}
tags={tags}
onTagsChange={setTags}
size='small'
placeholder={t('plugin.searchTools')!}
/>
)}
const TabContent = () => <div className={`flex flex-col overflow-hidden rounded-lg border-[0.5px] border-gray-200 bg-white shadow-lg ${popupClassName}`}>
<div className='px-2 pt-2 ' onClick={e => e.stopPropagation()}>
{activeTab === TabsEnum.Blocks && (
<Input
showLeftIcon
showClearIcon
autoFocus
value={searchText}
placeholder={searchPlaceholder}
onChange={e => setSearchText(e.target.value)}
onClear={() => setSearchText('')}
/>
)}
{activeTab === TabsEnum.Tools && (
<SearchBox
search={searchText}
onSearchChange={setSearchText}
tags={tags}
onTagsChange={setTags}
size='small'
placeholder={t('plugin.searchTools')!}
/>
)}
</div>
<Tabs
activeTab={activeTab}
onActiveTabChange={handleActiveTabChange}
onSelect={handleSelect}
searchText={searchText}
tags={tags}
availableBlocksTypes={availableBlocksTypes}
noBlocks={noBlocks}
/>
</div>
</div>
<div className='h-0 grow'>
<Tabs
activeTab={activeTab}
onActiveTabChange={handleActiveTabChange}
onSelect={handleSelect}
searchText={searchText}
tags={tags}
availableBlocksTypes={availableBlocksTypes}
noBlocks={noBlocks}
/>
</div>
</div>
return absolute
? <PortalToFollowElem
placement={placement}

@ -34,7 +34,7 @@ const Tabs: FC<TabsProps> = ({
const { data: workflowTools } = useAllWorkflowTools()
return (
<div onClick={e => e.stopPropagation()}>
<div onClick={e => e.stopPropagation()} className='h-full flex flex-col'>
{
!noBlocks && (
<div className='flex items-center px-3 border-b-[0.5px] border-divider-subtle'>
@ -59,12 +59,12 @@ const Tabs: FC<TabsProps> = ({
}
{
activeTab === TabsEnum.Blocks && !noBlocks && (
<><APOTools onSelect={onSelect} searchText={searchText}/>
<div className='h-0 grow overflow-y-auto'><APOTools onSelect={onSelect} searchText={searchText}/>
<Blocks
searchText={searchText}
onSelect={onSelect}
availableBlocksTypes={availableBlocksTypes}
/></>
/></div>
)
}
{

@ -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 <div className=' w-full h-[400px] relative'><Topology data={topologyData} /></div>
}
const DataDisplay = ({ data }: DataDisplayProps) => {
const DataDisplay = ({ data, dataObj }: DataDisplayProps) => {
// const [chartData,setChartData] = useState<any>(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 (
<div className="relative w-full">
{
@ -36,8 +42,8 @@ const DataDisplay = ({ data }: DataDisplayProps) => {
{t('apo.chart.chartTitle')}
</div>
<div className='px-1'>
{['cpu', 'network', 'memory'].includes(chartData?.type) && (
<LineChart type={chartData?.type} data={chartData?.data} />
{chartData?.type === 'metric' && (
<LineChart data={chartData?.data?.timeseries || []} unit={chartData?.unit} />
)}
{chartData?.type === 'topology' && (
<TopologyCom data={chartData?.data} />

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

@ -14,6 +14,7 @@ const translation = {
alert: 'Alert events',
log: 'Log data',
custom: 'Custom query return data',
metric: 'Metric Line Chart',
},
}

@ -14,6 +14,7 @@ const translation = {
alert: '告警事件',
log: '日志数据',
custom: '自定义查询返回数据',
metric: '指标折线图',
},
}

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

Loading…
Cancel
Save