From 8f2eea70f68a5b5109197a8201514711cddd9c0d Mon Sep 17 00:00:00 2001 From: jialin Date: Mon, 26 Jan 2026 13:34:58 +0800 Subject: [PATCH] chore: detail api --- config/routes.ts | 11 + src/hooks/use-query-data-list.ts | 142 ++++++++++ src/layouts/index.tsx | 1 + .../_components/page-breadcrumb/index.tsx | 3 + src/pages/benchmark/apis/index.ts | 35 ++- .../components/add-benchmark-modal.tsx | 1 + .../components/compare-conditions.tsx | 2 +- .../benchmark/components/detail-content.tsx | 89 +++++++ .../benchmark/components/detail-modal.tsx | 29 +++ .../components/environment/gpu-data.tsx | 57 ++++ .../components/environment/index.tsx | 77 +++++- .../components/environment/metadata.tsx | 43 +++ .../components/environment/worker-data.tsx | 57 ++++ .../benchmark/components/left-actions.tsx | 3 +- src/pages/benchmark/components/logs/index.tsx | 21 +- .../benchmark/components/right-actions.tsx | 48 +++- .../benchmark/components/summary/basic.tsx | 244 ++++++++++++++++++ .../benchmark/components/summary/index.tsx | 18 +- .../benchmark/components/summary/metrics.tsx | 40 +++ .../components/summary/percentile-result.tsx | 119 +++++++++ .../benchmark/components/summary/section.tsx | 42 +++ src/pages/benchmark/config/detail-context.ts | 4 +- src/pages/benchmark/config/detail-types.ts | 117 +++++++++ src/pages/benchmark/config/form-context.ts | 6 +- src/pages/benchmark/config/types.ts | 1 + src/pages/benchmark/details.tsx | 61 ++--- src/pages/benchmark/forms/basic.tsx | 97 ++++++- src/pages/benchmark/forms/dataset.tsx | 39 ++- src/pages/benchmark/forms/index.tsx | 11 +- src/pages/benchmark/forms/random-settings.tsx | 26 +- .../benchmark/hooks/use-benchmark-columns.tsx | 41 +-- .../benchmark/hooks/use-column-settings.tsx | 201 +++++++++++++++ src/pages/benchmark/hooks/use-view-detail.ts | 35 +++ src/pages/benchmark/index.tsx | 34 ++- .../benchmark/services/use-query-dataset.ts | 22 ++ .../benchmark/services/use-query-detail.ts | 17 ++ .../benchmark/services/use-query-metrics.ts | 22 ++ src/pages/cluster-management/apis/index.ts | 8 +- .../services/use-query-cluster-list.tsx | 67 +++++ .../llmodels/components/view-logs-modal.tsx | 2 +- .../services/use-query-model-instances.ts | 71 +++++ .../llmodels/services/use-query-model-list.ts | 67 +++++ src/request-config.ts | 4 + 43 files changed, 1938 insertions(+), 97 deletions(-) create mode 100644 src/hooks/use-query-data-list.ts create mode 100644 src/pages/benchmark/components/detail-content.tsx create mode 100644 src/pages/benchmark/components/detail-modal.tsx create mode 100644 src/pages/benchmark/components/environment/gpu-data.tsx create mode 100644 src/pages/benchmark/components/environment/metadata.tsx create mode 100644 src/pages/benchmark/components/environment/worker-data.tsx create mode 100644 src/pages/benchmark/components/summary/basic.tsx create mode 100644 src/pages/benchmark/components/summary/metrics.tsx create mode 100644 src/pages/benchmark/components/summary/percentile-result.tsx create mode 100644 src/pages/benchmark/components/summary/section.tsx create mode 100644 src/pages/benchmark/config/detail-types.ts create mode 100644 src/pages/benchmark/hooks/use-column-settings.tsx create mode 100644 src/pages/benchmark/hooks/use-view-detail.ts create mode 100644 src/pages/benchmark/services/use-query-dataset.ts create mode 100644 src/pages/benchmark/services/use-query-detail.ts create mode 100644 src/pages/benchmark/services/use-query-metrics.ts create mode 100644 src/pages/cluster-management/services/use-query-cluster-list.tsx create mode 100644 src/pages/llmodels/services/use-query-model-instances.ts create mode 100644 src/pages/llmodels/services/use-query-model-list.ts diff --git a/config/routes.ts b/config/routes.ts index 6d3f7937..af629e3c 100644 --- a/config/routes.ts +++ b/config/routes.ts @@ -124,6 +124,17 @@ export default [ defaultIcon: 'icon-speed', access: 'canSeeAdmin', component: './benchmark/index' + }, + { + name: 'benchmarkDetail', + path: '/models/benchmark/detail', + key: 'benchmarkDetail', + icon: 'icon-speed', + selectedIcon: 'icon-speed-filled', + defaultIcon: 'icon-speed', + access: 'canSeeAdmin', + hideInMenu: true, + component: './benchmark/details' } ] }, diff --git a/src/hooks/use-query-data-list.ts b/src/hooks/use-query-data-list.ts new file mode 100644 index 00000000..799d7215 --- /dev/null +++ b/src/hooks/use-query-data-list.ts @@ -0,0 +1,142 @@ +import { createAxiosToken } from '@/hooks/use-chunk-request'; +import { useRequest } from 'ahooks'; +import { message } from 'antd'; +import { CancelTokenSource } from 'axios'; +import { useEffect, useRef, useState } from 'react'; + +/** + + * generic hook to query data list + * @template ListItem + * @param option.fetchList: (params, extra) => Promise<{ items: ListItem[] }> + * @returns loading, dataList, fetchData, cancelRequest + */ +export function useQueryDataList(option: { + key: string; + fetchList: ( + params: Params, + options?: any + ) => Promise>; + getLabel?: (item: ListItem) => string; + getValue?: (item: ListItem) => any; + errorMsg?: string; +}) { + const { key, fetchList, getLabel, getValue, errorMsg } = option; + const axiosTokenRef = useRef(null); + const [dataList, setDataList] = useState< + Array + >([]); + + const { + runAsync: fetchData, + loading, + cancel + } = useRequest( + async (params: Params, extra?: any) => { + axiosTokenRef.current?.cancel(); + axiosTokenRef.current = createAxiosToken(); + const res = await fetchList(params, { + token: axiosTokenRef.current?.token, + ...(extra || {}) + }); + + setDataList( + res.items?.map((item: ListItem) => ({ + ...item, + label: getLabel ? getLabel(item) : (item as any).name, + value: getValue ? getValue(item) : (item as any).id + })) || [] + ); + + return res.items || []; + }, + { + manual: true, + onSuccess: () => {}, + onError: (error) => { + message.error( + error?.message || errorMsg || `Failed to fetch ${key} list` + ); + setDataList([]); + } + } + ); + + const cancelRequest = () => { + cancel(); + axiosTokenRef.current?.cancel(); + }; + + useEffect(() => { + return () => { + cancel(); + axiosTokenRef.current?.cancel(); + }; + }, []); + + return { + loading, + dataList, + cancelRequest, + fetchData + }; +} + +export function useQueryData(option: { + key: string; + fetchDetail: (params: Params, options?: any) => Promise; + getData?: (response: Detail) => any; + errorMsg?: string; +}) { + const { key, fetchDetail, getData, errorMsg } = option; + const axiosTokenRef = useRef(null); + const [detailData, setDetailData] = useState({} as Detail); + + const { + runAsync: fetchData, + loading, + cancel + } = useRequest( + async (params: Params, extra?: any) => { + axiosTokenRef.current?.cancel(); + axiosTokenRef.current = createAxiosToken(); + const res = await fetchDetail(params, { + token: axiosTokenRef.current?.token, + ...(extra || {}) + }); + + setDetailData(getData ? getData(res) : res); + + return res; + }, + { + manual: true, + onSuccess: () => {}, + onError: (error) => { + message.error( + error?.message || errorMsg || `Failed to fetch ${key} list` + ); + setDetailData({} as Detail); + } + } + ); + + const cancelRequest = () => { + cancel(); + axiosTokenRef.current?.cancel(); + }; + + useEffect(() => { + return () => { + cancel(); + axiosTokenRef.current?.cancel(); + }; + }, []); + + return { + loading, + detailData, + cancelRequest, + fetchData + }; +} diff --git a/src/layouts/index.tsx b/src/layouts/index.tsx index 7b545c73..64cdb609 100644 --- a/src/layouts/index.tsx +++ b/src/layouts/index.tsx @@ -46,6 +46,7 @@ const NO_CONTAINER_PAGES = [ 'text2images', 'clusterDetail', 'clusterCreate', + 'benchmarkDetail', 'video' ]; diff --git a/src/pages/_components/page-breadcrumb/index.tsx b/src/pages/_components/page-breadcrumb/index.tsx index 653f4b0b..0179eb02 100644 --- a/src/pages/_components/page-breadcrumb/index.tsx +++ b/src/pages/_components/page-breadcrumb/index.tsx @@ -10,6 +10,9 @@ const StyledBreadcrumb = styled(Breadcrumb)` a { background: unset; } + a:hover { + background: unset; + } } .ant-breadcrumb-separator { display: flex; diff --git a/src/pages/benchmark/apis/index.ts b/src/pages/benchmark/apis/index.ts index 9d536458..2f15fcb4 100644 --- a/src/pages/benchmark/apis/index.ts +++ b/src/pages/benchmark/apis/index.ts @@ -1,8 +1,9 @@ import { request } from '@umijs/max'; import { CancelToken } from 'axios'; -import { BenchmarkListItem, FormData } from '../config/types'; +import { BenchmarkMetricsFormData } from '../config/detail-types'; +import { BenchmarkListItem, DatasetListItem, FormData } from '../config/types'; -export const BENCHMARKS_API = '/benchmark'; +export const BENCHMARKS_API = '/benchmarks'; export const DATASETS_API = '/datasets'; export async function queryBenchmarkList( @@ -50,6 +51,18 @@ export async function queryBenchmarkLogs( }); } +export async function queryBenchmarkDetail( + id: number, + options?: { + token?: CancelToken; + } +) { + return request(`${BENCHMARKS_API}/${id}`, { + method: 'GET', + cancelToken: options?.token + }); +} + export async function createBenchmarkResult(params: { id: number; data: any }) { return request(`${BENCHMARKS_API}/${params.id}/result`, { method: 'POST', @@ -63,9 +76,25 @@ export async function queryDatasetList( token?: CancelToken; } ) { - return request>(`${DATASETS_API}`, { + return request>(`${DATASETS_API}`, { method: 'GET', params, cancelToken: options?.token }); } + +export async function queryBenchmarkMetrics( + params: { + id: number; + data: BenchmarkMetricsFormData; + }, + options?: { + token?: CancelToken; + } +) { + return request(`${BENCHMARKS_API}/${params.id}/metrics`, { + method: 'POST', + data: params.data, + cancelToken: options?.token + }); +} diff --git a/src/pages/benchmark/components/add-benchmark-modal.tsx b/src/pages/benchmark/components/add-benchmark-modal.tsx index c1361e33..fb4c16db 100644 --- a/src/pages/benchmark/components/add-benchmark-modal.tsx +++ b/src/pages/benchmark/components/add-benchmark-modal.tsx @@ -49,6 +49,7 @@ const AddBenchmark: React.FC = ({ diff --git a/src/pages/benchmark/components/compare-conditions.tsx b/src/pages/benchmark/components/compare-conditions.tsx index 942f8ed7..cf7e98c0 100644 --- a/src/pages/benchmark/components/compare-conditions.tsx +++ b/src/pages/benchmark/components/compare-conditions.tsx @@ -184,7 +184,7 @@ const CompareConditions: React.FC = () => { }} > - Compare Filter + Export filter ); diff --git a/src/pages/benchmark/components/detail-content.tsx b/src/pages/benchmark/components/detail-content.tsx new file mode 100644 index 00000000..4c050980 --- /dev/null +++ b/src/pages/benchmark/components/detail-content.tsx @@ -0,0 +1,89 @@ +import IconFont from '@/components/icon-font'; +import { useQueryClusterList } from '@/pages/cluster-management/services/use-query-cluster-list'; +import { useIntl, useSearchParams } from '@umijs/max'; +import { Tabs, TabsProps } from 'antd'; +import React, { useEffect, useState } from 'react'; +import DetailContext from '../config/detail-context'; +import { BenchmarkListItem } from '../config/types'; +import useQueryDetail from '../services/use-query-detail'; +import Configure from './configure'; +import Environment from './environment'; +import Logs from './logs'; +import Summary from './summary'; + +const Details: React.FC<{ currentData?: BenchmarkListItem }> = ({ + currentData +}) => { + const intl = useIntl(); + const [searchParams] = useSearchParams(); + const id = searchParams.get('id'); + const { loading, detailData, cancelRequest, fetchData } = useQueryDetail(); + const { + clusterList, + fetchClusterList, + cancelRequest: cancelClusterRequest + } = useQueryClusterList(); + const [activeKey, setActiveKey] = useState('summary'); + + const items: TabsProps['items'] = [ + { + key: 'summary', + label: intl.formatMessage({ id: 'benchmark.detail.summary.title' }), + children: , + icon: + }, + { + key: 'configure', + label: intl.formatMessage({ id: 'benchmark.detail.configure.title' }), + children: , + icon: + }, + { + key: 'environment', + label: intl.formatMessage({ id: 'benchmark.detail.environment.title' }), + children: , + icon: + }, + { + key: 'logs', + label: intl.formatMessage({ id: 'benchmark.detail.logs.title' }), + children: , + icon: + } + ]; + + const handleChangeTab = (key: string) => { + setActiveKey(key); + }; + + useEffect(() => { + if (id) { + fetchData(id); + fetchClusterList({ page: -1 }); + } else { + cancelRequest(); + cancelClusterRequest(); + } + }, [id]); + + return ( + + + + ); +}; + +export default Details; diff --git a/src/pages/benchmark/components/detail-modal.tsx b/src/pages/benchmark/components/detail-modal.tsx new file mode 100644 index 00000000..036d9825 --- /dev/null +++ b/src/pages/benchmark/components/detail-modal.tsx @@ -0,0 +1,29 @@ +import FormDrawer from '@/pages/_components/form-drawer'; +import { BenchmarkListItem } from '../config/types'; +import DetailContent from './detail-content'; + +interface DetailModalProps { + open: boolean; + currentData?: BenchmarkListItem; + onClose: () => void; +} + +const DetailModal: React.FC = ({ + open, + currentData, + onClose +}) => { + return ( + + + + ); +}; + +export default DetailModal; diff --git a/src/pages/benchmark/components/environment/gpu-data.tsx b/src/pages/benchmark/components/environment/gpu-data.tsx new file mode 100644 index 00000000..f40e5865 --- /dev/null +++ b/src/pages/benchmark/components/environment/gpu-data.tsx @@ -0,0 +1,57 @@ +import { convertFileSize } from '@/utils'; +import { Descriptions, DescriptionsProps } from 'antd'; +import React from 'react'; +import { GPUData } from '../../config/detail-types'; +import Section from '../summary/section'; + +const Environment: React.FC = (props) => { + const items: DescriptionsProps['items'] = [ + { + key: '4', + label: 'Name', + children: props.name + }, + { + key: '1', + label: 'VRAM', + children: convertFileSize(props.memory_total) + }, + { + key: '3', + label: 'Driver Version', + children: props.driver_version + }, + { + key: '2', + label: 'Runtime Version', + children: props.runtime_version + }, + { + key: '6', + label: 'Core', + children: props.core_total + }, + { + key: '5', + label: 'Vendor', + children: props.vendor + } + ]; + return ( +
+ +
+ ); +}; + +export default Environment; diff --git a/src/pages/benchmark/components/environment/index.tsx b/src/pages/benchmark/components/environment/index.tsx index 2f41b7f7..7b574c41 100644 --- a/src/pages/benchmark/components/environment/index.tsx +++ b/src/pages/benchmark/components/environment/index.tsx @@ -1,9 +1,82 @@ -import React from 'react'; +import { Tag } from 'antd'; +import React, { useMemo } from 'react'; +import styled from 'styled-components'; import { useDetailContext } from '../../config/detail-context'; +import WorkerData from './worker-data'; + +const Container = styled.div` + display: flex; + flex-direction: column; + gap: 16px; +`; const Environment: React.FC = () => { const { detailData } = useDetailContext(); - return
Environment Content
; + const { snapshot } = detailData; + + const mainWorker = useMemo(() => { + // get workers data + const [[workerName, workerInfo]] = Object.entries(snapshot.workers); + const gpuData = Object.values(snapshot.gpus).filter( + (gpu) => gpu.worker_name === workerName + ); + + return { + workerData: workerInfo, + gpuData: [...gpuData, ...gpuData] + }; + }, [snapshot]); + + const subWorkers = useMemo(() => { + const [[mainWorkerName, mainWorkerInfo]] = Object.entries(snapshot.workers); + + const subOrdinaryWorkers = Object.values(snapshot.instances).filter( + (instance) => instance.worker_name === mainWorkerName + ); + + return subOrdinaryWorkers.map((worker) => { + const gpuData = Object.values(snapshot.gpus).filter( + (gpu) => gpu.worker_name === worker.worker_name + ); + + return { + workerData: worker, + gpuData: gpuData + }; + }); + }, [snapshot, mainWorker]); + + return ( + + + Main + {mainWorker?.workerData?.name} + + } + > + + Sub + {mainWorker?.workerData?.name} + + } + > + {/* {subWorkers?.map?.((worker, index) => ( + + ))} */} + + ); }; export default Environment; diff --git a/src/pages/benchmark/components/environment/metadata.tsx b/src/pages/benchmark/components/environment/metadata.tsx new file mode 100644 index 00000000..78008626 --- /dev/null +++ b/src/pages/benchmark/components/environment/metadata.tsx @@ -0,0 +1,43 @@ +import { convertFileSize } from '@/utils'; +import { Descriptions, DescriptionsProps } from 'antd'; +import React from 'react'; +import { WorkerData } from '../../config/detail-types'; + +const Environment: React.FC = (props) => { + const { os, cpu_total, memory_total } = props; + + const items: DescriptionsProps['items'] = [ + { + key: '4', + label: 'System', + children: os.name + }, + { + key: '1', + label: 'CPU Count', + children: cpu_total + }, + { + key: '5', + label: 'Memory Total', + children: convertFileSize(memory_total) + } + ]; + return ( +
+ +
+ ); +}; + +export default Environment; diff --git a/src/pages/benchmark/components/environment/worker-data.tsx b/src/pages/benchmark/components/environment/worker-data.tsx new file mode 100644 index 00000000..84dec986 --- /dev/null +++ b/src/pages/benchmark/components/environment/worker-data.tsx @@ -0,0 +1,57 @@ +import { Col, Row } from 'antd'; +import React from 'react'; +import styled from 'styled-components'; +import { GPUData as GPUDataType, WorkerData } from '../../config/detail-types'; +import Section from '../summary/section'; +import GPUData from './gpu-data'; +import Metadata from './metadata'; + +const Container = styled.div` + display: flex; + flex-direction: column; + border-radius: var(--ant-border-radius); + border: 1px solid var(--ant-color-border); +`; + +const Content = styled.div` + display: flex; + flex-direction: column; + gap: 16px; + padding: 16px 16px; +`; + +const Environment: React.FC<{ + workerData: WorkerData; + gpuData: GPUDataType[]; + title?: React.ReactNode; +}> = (props) => { + const { title } = props; + return ( + +
+ +
+ + + {props.gpuData.map((gpu, index) => ( + + + + ))} + + +
+ ); +}; + +export default Environment; diff --git a/src/pages/benchmark/components/left-actions.tsx b/src/pages/benchmark/components/left-actions.tsx index de388a44..26a3d0e5 100644 --- a/src/pages/benchmark/components/left-actions.tsx +++ b/src/pages/benchmark/components/left-actions.tsx @@ -2,7 +2,6 @@ import { SearchOutlined, SyncOutlined } from '@ant-design/icons'; import { useIntl } from '@umijs/max'; import { Button, Input, Space } from 'antd'; import React from 'react'; -import CompareConditions from './compare-conditions'; export interface RightActionsProps { handleInputChange: (e: React.ChangeEvent) => void; @@ -30,7 +29,7 @@ const RightActions: React.FC = ({ allowClear onChange={handleInputChange} > - + {/* */} + {/* + + */} + {settingButton} - + */} + 0 && ( + ({rowSelection.selectedRowKeys.length}) + ) + } + size="large" + showText={true} + disabled={!rowSelection.selectedRowKeys.length} + onSelect={handleActionSelect} + /> ); }; diff --git a/src/pages/benchmark/components/summary/basic.tsx b/src/pages/benchmark/components/summary/basic.tsx new file mode 100644 index 00000000..71a2ab09 --- /dev/null +++ b/src/pages/benchmark/components/summary/basic.tsx @@ -0,0 +1,244 @@ +import AutoTooltip from '@/components/auto-tooltip'; +import StatusTag from '@/components/status-tag'; +import { useIntl } from '@umijs/max'; +import { Col, Descriptions, DescriptionsProps, Row, Statistic } from 'antd'; +import dayjs from 'dayjs'; +import { round } from 'lodash'; +import React from 'react'; +import styled from 'styled-components'; +import { BenchmarkStatus, BenchmarkStatusLabelMap } from '../../config'; +import { useDetailContext } from '../../config/detail-context'; +import PercentileResult from './percentile-result'; +import Section from './section'; + +const Container = styled.div` + display: flex; + flex-direction: column; + gap: 16px; +`; + +const Card = styled.div` + height: 78px; + padding: 12px 16px; + border: 1px solid var(--ant-color-border); + border-radius: var(--ant-border-radius); + background-color: var(--ant-color-bg-container); +`; + +const Box = styled.div` + display: grid; + grid-template-columns: 1fr 1fr; + gap: 16px; +`; + +const Summary: React.FC = () => { + const { detailData, clusterList } = useDetailContext(); + const intl = useIntl(); + + const items: DescriptionsProps['items'] = [ + { + key: '1', + label: 'Model', + children: {detailData.model_name} + }, + { + key: '2', + label: 'Cluster', + children: ( + + {clusterList?.find((item) => item.value === detailData.cluster_id) + ?.label || detailData.cluster_id} + + ) + }, + { + key: '4', + label: 'Dataset', + children: detailData.dataset_name + }, + { + key: '5', + label: 'Concurrency', + children: detailData.concurrency + }, + { + key: '5', + label: 'Duration (s)', + children: detailData.duration + }, + { + key: '3', + label: 'Update Time', + children: dayjs(detailData.updated_at).format('YYYY-MM-DD HH:mm:ss') + } + ]; + const cardFields = [ + { + label: 'Total Requests', + key: 'total_requests', + value: detailData.total_requests, + unit: '' + }, + { + label: 'Success Rate', + key: 'success_rate', + value: detailData.success_rate, + color: 'var(--ant-color-success)', + unit: '%' + }, + { + label: 'TPS', + key: 'tps', + value: detailData.tokens_per_second_mean, + unit: 't/s' + }, + { + label: 'RPS', + key: 'rps', + value: detailData.requests_per_second_mean, + unit: 'req/s' + }, + // { + // label: 'ITL', + // key: 'itl', + // value: detailData.inter_token_latency_mean, + // unit: 'ms' + // }, + { + label: 'TPOT', + key: 'time_per_output_token_mean', + value: detailData.time_per_output_token_mean, + unit: 'ms' + }, + { + label: 'TTFT', + key: 'time_to_first_token_mean', + value: detailData.time_to_first_token_mean, + unit: 'ms' + } + ]; + + const throughputFields = [ + { + key: '3', + label: 'Total throughput', + children: `${round(detailData.total_through || 0, 2)} t/s` + }, + { + key: '1', + label: 'Request throughput', + children: `${round(detailData.request_through || 0, 2)} t/s` + }, + { + key: '2', + label: 'Output throughput', + children: `${round(detailData.output_through || 0, 2)} t/s` + } + ]; + + const latencyFields = [ + { + key: '1', + label: 'Request Latency', + children: `${round(detailData.request_latency_mean, 2)} t/s` + }, + { + key: '2', + label: 'Avg Time to First Token', + children: `${round(detailData.time_to_first_token_mean, 2)} t/s` + }, + { + key: '3', + label: 'Avg Time Per Output Token', + children: `${round(detailData.time_per_output_token_mean, 2)} t/s` + } + ]; + + return ( + + + {cardFields.map((field) => ( + + + + + + ))} + +
+
+ {detailData.model_instance_name} + +
+ +
+ + +
+
+ Throughput +
+ +
+
+
+ Latency +
+ +
+
+ +
+ ); +}; + +export default Summary; diff --git a/src/pages/benchmark/components/summary/index.tsx b/src/pages/benchmark/components/summary/index.tsx index 377aa992..457ca25a 100644 --- a/src/pages/benchmark/components/summary/index.tsx +++ b/src/pages/benchmark/components/summary/index.tsx @@ -1,9 +1,21 @@ import React from 'react'; -import { useDetailContext } from '../../config/detail-context'; +import styled from 'styled-components'; +import BasicInfo from './basic'; +import MetricsInfo from './metrics'; + +const Container = styled.div` + display: flex; + flex-direction: column; + gap: 24px; +`; const Summary: React.FC = () => { - const { detailData } = useDetailContext(); - return
Summary Content
; + return ( + + + + + ); }; export default Summary; diff --git a/src/pages/benchmark/components/summary/metrics.tsx b/src/pages/benchmark/components/summary/metrics.tsx new file mode 100644 index 00000000..bbb16dfb --- /dev/null +++ b/src/pages/benchmark/components/summary/metrics.tsx @@ -0,0 +1,40 @@ +import _ from 'lodash'; +import React, { useEffect } from 'react'; +import { useDetailContext } from '../../config/detail-context'; +import useQueryMtrics from '../../services/use-query-metrics'; + +const fields = [ + 'requests_per_second_mean', + 'request_latency_mean', + 'time_per_output_token_mean', + 'inter_token_latency_mean', + 'time_to_first_token_mean', + 'tokens_per_second_mean', + 'output_tokens_per_second_mean', + 'prompt_tokens_per_second_mean' +]; + +const Summary: React.FC = () => { + const { detailData, id } = useDetailContext(); + const { + detailData: metricsData, + fetchData, + cancelRequest + } = useQueryMtrics(); + + useEffect(() => { + if (id) { + fetchData({ + id: id, + data: { + ..._.pick(detailData, fields) + } + }); + } else { + cancelRequest(); + } + }, [id]); + return
; +}; + +export default Summary; diff --git a/src/pages/benchmark/components/summary/percentile-result.tsx b/src/pages/benchmark/components/summary/percentile-result.tsx new file mode 100644 index 00000000..88aa1c00 --- /dev/null +++ b/src/pages/benchmark/components/summary/percentile-result.tsx @@ -0,0 +1,119 @@ +import { Table } from 'antd'; +import React from 'react'; +import { useDetailContext } from '../../config/detail-context'; +import Section from './section'; + +// Define table columns: Percentile、 TTFT (ms) 、 ITL (ms) 、TPOT (ms)、Latency (ms)、Input Tokens (t/s)、 Output Tokens (t/s)、 Output (t/s)、Total (t/s) + +const columns = [ + { + title: 'Percentile', + dataIndex: 'percentile', + key: 'percentile' + }, + { + title: 'TTFT (ms)', + dataIndex: 'ttft', + key: 'ttft' + }, + { + title: 'ITL (ms)', + dataIndex: 'itl', + key: 'itl' + }, + { + title: 'TPOT (ms)', + dataIndex: 'tpot', + key: 'tpot' + }, + { + title: 'Latency (ms)', + dataIndex: 'latency', + key: 'latency' + }, + { + title: 'Input Tokens (t/s)', + dataIndex: 'inputTokens', + key: 'inputTokens' + }, + { + title: 'Output Tokens (t/s)', + dataIndex: 'outputTokens', + key: 'outputTokens' + }, + { + title: 'Output (t/s)', + dataIndex: 'output', + key: 'output' + }, + { + title: 'Total (t/s)', + dataIndex: 'total', + key: 'total' + } +]; + +// mock data example: three rows of percentiles: 50th, 90th, 99th +const data = [ + { + key: '1', + percentile: '50%', + ttft: 100, + itl: 50, + tpot: 20, + latency: 200, + inputTokens: 300, + outputTokens: 400, + output: 500, + total: 600 + }, + { + key: '2', + percentile: '90%', + ttft: 150, + itl: 70, + tpot: 30, + latency: 250, + inputTokens: 350, + outputTokens: 450, + output: 550, + total: 650 + }, + { + key: '3', + percentile: '99%', + ttft: 200, + itl: 90, + tpot: 40, + latency: 300, + inputTokens: 400, + outputTokens: 500, + output: 600, + total: 700 + } +]; + +const PercentileResult: React.FC = () => { + const { detailData, id } = useDetailContext(); + + return ( +
+
+
+ ); +}; + +export default PercentileResult; diff --git a/src/pages/benchmark/components/summary/section.tsx b/src/pages/benchmark/components/summary/section.tsx new file mode 100644 index 00000000..67d105c8 --- /dev/null +++ b/src/pages/benchmark/components/summary/section.tsx @@ -0,0 +1,42 @@ +import styled from 'styled-components'; + +const Container = styled.div` + padding: 16px; + border: 1px solid var(--ant-color-border); + border-radius: 4px; + .section-title { + font-weight: 500; + margin-bottom: 12px; + font-size: 14px; + } + th.ant-descriptions-item { + padding-bottom: 0; + } +`; + +const Title = styled.div` + display: flex; + font-weight: 500; + margin-bottom: 12px; + font-size: 14px; + gap: 8px; + align-items: center; +`; + +const DetailSection: React.FC<{ + children: React.ReactNode; + title?: React.ReactNode; + styles?: { + container?: React.CSSProperties; + title?: React.CSSProperties; + }; +}> = ({ children, title, styles }) => { + return ( + + {title && {title}} + {children} + + ); +}; + +export default DetailSection; diff --git a/src/pages/benchmark/config/detail-context.ts b/src/pages/benchmark/config/detail-context.ts index 8307ee8f..546bc3f9 100644 --- a/src/pages/benchmark/config/detail-context.ts +++ b/src/pages/benchmark/config/detail-context.ts @@ -1,7 +1,9 @@ import { createContext, useContext } from 'react'; +import { BenchmarkDetail } from './detail-types'; interface DetailContextProps { - detailData: any; + detailData: BenchmarkDetail; + clusterList?: Global.BaseOption[]; id: number; loading?: boolean; } diff --git a/src/pages/benchmark/config/detail-types.ts b/src/pages/benchmark/config/detail-types.ts new file mode 100644 index 00000000..c5e1bb9e --- /dev/null +++ b/src/pages/benchmark/config/detail-types.ts @@ -0,0 +1,117 @@ +export interface ComputedResourceClaim { + is_unified_memory: boolean; + offload_layers: any; + total_layers: any; + ram: any; + vram: Record; + tensor_split: any; + vram_utilization: any; +} + +export interface InstancesData { + computed_resource_claim: ComputedResourceClaim; + ports: number[]; + worker_id: number; + worker_name: string; + worker_ip: string; + gpu_type: string; + gpu_indexes: number[]; + gpu_ids: string[]; + id: number; + name: string; + resolved_path: string; + state: string; + state_message: string; + backend: any; + backend_version: any; + api_detected_backend_version: any; + backend_parameters: any; + image_name: any; + run_command: any; + env: any; + extended_kv_cache: any; + speculative_config: any; + subordinate_workers: any; +} + +export interface WorkerData { + id: number; + name: string; + cpu_total: number; + memory_total: number; + os: { + name: string; + version: string; + }; +} + +export interface GPUData { + vendor: string; + type: string; + index: number; + device_index: number; + device_chip_index: number; + arch_family: string; + name: string; + uuid: string; + driver_version: string; + runtime_version: string; + compute_capability: string; + id: string; + worker_id: number; + worker_name: string; + memory_total: number; + core_total: number; +} + +export interface BenchmarkDetail { + requests_per_second_mean: number; + request_latency_mean: number; + time_per_output_token_mean: number; + inter_token_latency_mean: number; + time_to_first_token_mean: number; + tokens_per_second_mean: number; + output_tokens_per_second_mean: number; + prompt_tokens_per_second_mean: number; + name: string; + description: string; + labels: Record; + dataset_id: number; + dataset_name: string; + dataset_source: string; + dataset_prompt_tokens: number; + dataset_output_tokens: number; + cluster_id: number; + model_id: number; + model_name: string; + model_instance_name: string; + request_rate: number; + total_requests: number; + state: string; + state_message: any; + progress: any; + worker_id: number; + pid: number; + snapshot: { + instances: Record; + workers: Record; + gpus: Record; + }; + gpu_summary: string; + gpu_vendor_summary: string; + id: number; + created_at: string; + updated_at: string; +} + +export interface BenchmarkMetricsFormData { + requests_per_second_mean: number; + request_latency_mean: number; + time_per_output_token_mean: number; + inter_token_latency_mean: number; + time_to_first_token_mean: number; + tokens_per_second_mean: number; + output_tokens_per_second_mean: number; + prompt_tokens_per_second_mean: number; + raw_metrics: Record; +} diff --git a/src/pages/benchmark/config/form-context.ts b/src/pages/benchmark/config/form-context.ts index 12386fbc..c3b3e3ff 100644 --- a/src/pages/benchmark/config/form-context.ts +++ b/src/pages/benchmark/config/form-context.ts @@ -3,9 +3,9 @@ import { createContext, useContext } from 'react'; interface FormContextProps { action: PageActionType; - clusterList: Global.BaseOption[]; - modelList: Global.BaseOption[]; - modelInstanceList: Global.BaseOption[]; + open?: boolean; + clusterList?: Global.BaseOption[]; + modelList?: Global.BaseOption[]; } const FormContext = createContext({} as FormContextProps); diff --git a/src/pages/benchmark/config/types.ts b/src/pages/benchmark/config/types.ts index 92481e3b..a6fd5dcd 100644 --- a/src/pages/benchmark/config/types.ts +++ b/src/pages/benchmark/config/types.ts @@ -67,6 +67,7 @@ export interface GPUSnapshot { export interface FormData { name: string; + profile: string; description: string; labels: Record; cluster_id: number; diff --git a/src/pages/benchmark/details.tsx b/src/pages/benchmark/details.tsx index 4220da4d..b55ad8f5 100644 --- a/src/pages/benchmark/details.tsx +++ b/src/pages/benchmark/details.tsx @@ -1,55 +1,32 @@ -import { useIntl } from '@umijs/max'; -import { Tabs, TabsProps } from 'antd'; -import React, { useState } from 'react'; -import PageBox from '../_components/page-box'; -import Configure from './components/configure'; -import Environment from './components/environment'; -import Logs from './components/logs'; -import Summary from './components/summary'; -import DetailContext from './config/detail-context'; +import { useIntl, useNavigate, useSearchParams } from '@umijs/max'; +import React from 'react'; +import { PageContainerInner } from '../_components/page-box'; +import PageBreadcrumb from '../_components/page-breadcrumb'; +import DetailContent from './components/detail-content'; const Details: React.FC = () => { + const navigate = useNavigate(); const intl = useIntl(); - const [activeKey, setActiveKey] = useState('summary'); + const [searchParams] = useSearchParams(); + const name = searchParams.get('name'); - const items: TabsProps['items'] = [ + const breadcrumbItems = [ { - key: 'summary', - label: intl.formatMessage({ id: 'benchmark.detail.summary.title' }), - children: + title: {intl.formatMessage({ id: 'benchmark.title' })}, + onClick: () => navigate(-1) }, { - key: 'configure', - label: intl.formatMessage({ id: 'benchmark.detail.configure.title' }), - children: - }, - { - key: 'environment', - label: intl.formatMessage({ id: 'benchmark.detail.environment.title' }), - children: - }, - { - key: 'logs', - label: intl.formatMessage({ id: 'benchmark.detail.logs.title' }), - children: + title: name } ]; - - const handleChangeTab = (key: string) => { - setActiveKey(key); - }; - return ( - - - - - + + }} + > + + ); }; diff --git a/src/pages/benchmark/forms/basic.tsx b/src/pages/benchmark/forms/basic.tsx index 257c6721..9d3809ea 100644 --- a/src/pages/benchmark/forms/basic.tsx +++ b/src/pages/benchmark/forms/basic.tsx @@ -1,11 +1,16 @@ import LabelSelector from '@/components/label-selector'; import SealInput from '@/components/seal-form/seal-input'; import SealSelect from '@/components/seal-form/seal-select'; +import { PageAction } from '@/config'; import useAppUtils from '@/hooks/use-app-utils'; +import { ClusterStatusValueMap } from '@/pages/cluster-management/config'; +import { useQueryClusterList } from '@/pages/cluster-management/services/use-query-cluster-list'; +import { useQueryModelInstancesList } from '@/pages/llmodels/services/use-query-model-instances'; +import { useQueryModelList } from '@/pages/llmodels/services/use-query-model-list'; import { useIntl } from '@umijs/max'; import { Form } from 'antd'; import _ from 'lodash'; -import React from 'react'; +import React, { useEffect } from 'react'; import { useFormContext } from '../config/form-context'; import { FormData } from '../config/types'; @@ -14,12 +19,81 @@ const BasicForm: React.FC = () => { const form = Form.useFormInstance(); const labels = Form.useWatch('labels', form); const { getRuleMessage } = useAppUtils(); - const { modelList, modelInstanceList, clusterList } = useFormContext(); + const { action, open } = useFormContext(); + const { + loading: clusterLoading, + fetchClusterList, + cancelRequest: cancelClusterRequest, + clusterList + } = useQueryClusterList(); + const { + loading: modelLoading, + fetchModelList, + cancelRequest: cancelModelRequest, + modelList + } = useQueryModelList(); + const { + loading: instanceLoading, + fetchInstanceList, + cancelRequest: cancelInstanceRequest, + instanceList + } = useQueryModelInstancesList(); const handleLabelsChange = (labels: object) => { form.setFieldValue('labels', labels); }; + const onModelListOpenChange = async (open: boolean) => { + if (open && modelList.length === 0) { + await form.validateFields(['cluster_id']); + const cluster_id = form.getFieldValue('cluster_id'); + if (cluster_id) { + fetchModelList({ page: -1, cluster_id }); + } + } + }; + + const onInstanceOpenChange = async (open: boolean) => { + if (open && instanceList.length === 0) { + const model_id = form.getFieldValue('model_id'); + await form.validateFields(['model_name']); + if (model_id) fetchInstanceList({ id: model_id }); + } + }; + + const handleOnModelChange = (value: string, option: any) => { + form.setFieldValue('model_id', option?.id); + }; + + useEffect(() => { + const initClusterId = (list: any[]) => { + // Find default cluster + const defaultCluster = list?.find((item) => item.is_default); + if (defaultCluster) { + return defaultCluster.id; + } + + const cluster_id = + list?.find((item) => item.state === ClusterStatusValueMap.Ready)?.id || + list?.[0]?.id; + + return cluster_id; + }; + fetchClusterList({ page: -1 }).then((list) => { + if (list.length > 0 && action === PageAction.CREATE) { + form.setFieldValue('cluster_id', initClusterId(list)); + } + }); + }, [form, action]); + + useEffect(() => { + if (!open) { + cancelClusterRequest(); + cancelModelRequest(); + cancelInstanceRequest(); + } + }, [open]); + return ( <> @@ -47,13 +121,14 @@ const BasicForm: React.FC = () => { ]} > - name="model_id" + name="model_name" rules={[ { required: true, @@ -62,11 +137,21 @@ const BasicForm: React.FC = () => { ]} > ({ + ...item, + label: item.name, + value: item.name + }))} + onOpenChange={onModelListOpenChange} + onChange={handleOnModelChange} label={intl.formatMessage({ id: 'benchmark.table.model' })} required > + name="model_id" hidden={true}> + + name="model_instance_name" rules={[ @@ -77,7 +162,9 @@ const BasicForm: React.FC = () => { ]} > diff --git a/src/pages/benchmark/forms/dataset.tsx b/src/pages/benchmark/forms/dataset.tsx index 5670fedf..4cf2f0da 100644 --- a/src/pages/benchmark/forms/dataset.tsx +++ b/src/pages/benchmark/forms/dataset.tsx @@ -1,16 +1,42 @@ +import SealInput from '@/components/seal-form/seal-input'; import SealSelect from '@/components/seal-form/seal-select'; import useAppUtils from '@/hooks/use-app-utils'; import { useIntl } from '@umijs/max'; import { Form } from 'antd'; -import React from 'react'; +import React, { useEffect } from 'react'; import { profileOptions } from '../config'; +import { useFormContext } from '../config/form-context'; import { FormData } from '../config/types'; +import useQueryDataset from '../services/use-query-dataset'; import RandomSettingsForm from './random-settings'; const DatasetForm: React.FC = () => { const intl = useIntl(); const form = Form.useFormInstance(); const { getRuleMessage } = useAppUtils(); + const { action, open } = useFormContext(); + const { + dataList: datasetList, + loading: datasetLoading, + fetchData, + cancelRequest: cancelDatasetRequest + } = useQueryDataset(); + + const handleOnDataSetChange = (value: any, option: any) => { + form.setFieldValue('dataset_id', option?.data?.id); + }; + + const handleOnDatasetOpenChange = async (open: boolean) => { + if (!datasetList.length && open) { + await fetchData({ page: -1 }); + } + }; + + useEffect(() => { + if (!open) { + cancelDatasetRequest(); + } + }, [open]); return ( <> @@ -40,11 +66,20 @@ const DatasetForm: React.FC = () => { ]} > ({ + label: item.name, + value: item.name + }))} + loading={datasetLoading} + onChange={handleOnDataSetChange} + onOpenChange={handleOnDatasetOpenChange} label={intl.formatMessage({ id: 'benchmark.table.dataset' })} required > + hidden name="dataset_id"> + + name="request_rate" diff --git a/src/pages/benchmark/forms/index.tsx b/src/pages/benchmark/forms/index.tsx index b09c71dd..a000d44d 100644 --- a/src/pages/benchmark/forms/index.tsx +++ b/src/pages/benchmark/forms/index.tsx @@ -22,6 +22,7 @@ interface ProviderFormProps { ref?: any; action: PageActionType; currentData?: ListItem; // Used when action is EDIT + open?: boolean; onFinish: (values: FormData) => Promise; } @@ -31,9 +32,10 @@ const TABKeysMap = { }; const ProviderForm: React.FC = forwardRef((props, ref) => { - const { action, currentData, onFinish } = props; + const { action, currentData, onFinish, open } = props; const intl = useIntl(); const [form] = Form.useForm(); + const { getScrollElementScrollableHeight } = useWrapperContext(); const [activeKey, setActiveKey] = useState([TABKeysMap.PROFILE]); const scrollTabsRef = useRef(null); @@ -91,7 +93,12 @@ const ProviderForm: React.FC = forwardRef((props, ref) => { }} getScrollElementScrollableHeight={getScrollElementScrollableHeight} > - +
{ return ( <> - name="input_length" + name="dataset_prompt_tokens" rules={[ { required: true, @@ -26,13 +26,22 @@ const DatasetForm: React.FC = () => { ]} > - name="output_length" + name="dataset_output_tokens" rules={[ { required: true, @@ -44,7 +53,16 @@ const DatasetForm: React.FC = () => { ]} > void + handleSelect: (val: string, record: ListItem) => void, + onCellClick?: (record: ListItem, dataIndex: string) => void ): ColumnsType => { const intl = useIntl(); @@ -39,9 +42,11 @@ const useBenchmarkColumns = ( title: intl.formatMessage({ id: 'common.table.name' }), dataIndex: 'name', sorter: tableSorter(1), - render: (text: string) => ( + render: (text: string, record) => ( - {text} + onCellClick?.(record, 'name')}> + {text} + ) }, @@ -68,6 +73,10 @@ const useBenchmarkColumns = ( { title: intl.formatMessage({ id: 'common.table.status' }), dataIndex: 'state', + ellipsis: { + showTitle: false + }, + width: 100, render: (value: number, record: ListItem) => ( ( - {text} + {_.round(text, 1)} ) }, { title: intl.formatMessage({ id: 'benchmark.table.tpot' }), - dataIndex: 'tpot', + dataIndex: 'time_per_output_token_mean', sorter: tableSorter(1), render: (text: string) => ( - {text} + {_.round(text, 2)} ) }, { title: intl.formatMessage({ id: 'benchmark.table.ttft' }), - dataIndex: 'ttft', + dataIndex: 'time_to_first_token_mean', sorter: tableSorter(1), render: (text: string) => ( - {text} + {_.round(text, 2)} ) }, { title: intl.formatMessage({ id: 'benchmark.table.rps' }), - dataIndex: 'requests_per_second', + dataIndex: 'requests_per_second_mean', sorter: tableSorter(1), render: (text: string) => ( - {text} + {_.round(text, 0)} ) }, { title: intl.formatMessage({ id: 'benchmark.table.tps' }), - dataIndex: 'tokens_per_second', + dataIndex: 'tokens_per_second_mean', sorter: tableSorter(1), render: (text: string) => ( - {text} + {_.round(text, 2)} ) }, @@ -153,7 +162,9 @@ const useBenchmarkColumns = ( dataIndex: 'created_at', sorter: tableSorter(3), render: (value: string) => ( - {dayjs(value).format('YYYY-MM-DD HH:mm:ss')} + + {dayjs(value).format('YYYY-MM-DD HH:mm:ss')} + ) }, { @@ -170,7 +181,7 @@ const useBenchmarkColumns = ( ) } ]; - }, [intl, handleSelect]); + }, [intl, onCellClick, handleSelect]); }; export default useBenchmarkColumns; diff --git a/src/pages/benchmark/hooks/use-column-settings.tsx b/src/pages/benchmark/hooks/use-column-settings.tsx new file mode 100644 index 00000000..ce1cb4fc --- /dev/null +++ b/src/pages/benchmark/hooks/use-column-settings.tsx @@ -0,0 +1,201 @@ +import { SettingOutlined } from '@ant-design/icons'; +import { useIntl } from '@umijs/max'; +import { Button, Checkbox, Col, Popover, Row, Tooltip } from 'antd'; +import React from 'react'; +import styled from 'styled-components'; + +const Container = styled.div` + max-height: 450px; + overflow-y: auto; + padding: 12px; + .title { + font-weight: 500; + margin-bottom: 12px; + } + .btn-wrapper { + margin-top: 12px; + display: flex; + justify-content: space-between; + align-items: center; + border-top: 1px solid var(--ant-color-split); + padding-top: 12px; + } + .buttons { + display: flex; + gap: 8px; + justify-content: flex-end; + } +`; + +const useColumnSettings = () => { + const intl = useIntl(); + + const [open, setOpen] = React.useState(false); + const [selectedColumns, setSelectedColumns] = React.useState([]); + + const handleToggle = () => { + setOpen(!open); + }; + + const allColumns = [ + { + title: intl.formatMessage({ id: 'clusters.title' }), + dataIndex: 'cluster_id' + }, + { + title: intl.formatMessage({ id: 'resources.worker' }), + dataIndex: 'worker_id' + }, + { + title: intl.formatMessage({ id: 'common.table.name' }), + dataIndex: 'name' + }, + { + title: intl.formatMessage({ id: 'benchmark.table.model' }), + dataIndex: 'model_name' + }, + { + title: intl.formatMessage({ id: 'benchmark.table.dataset' }), + dataIndex: 'dataset_name' + }, + { + title: 'Latency', + dataIndex: 'latency_mean' + }, + { + title: 'Throughput', + dataIndex: 'throughput_mean' + }, + { + title: 'Throughput request', + dataIndex: 'throughput_request_mean' + }, + { + title: 'generated tokens', + dataIndex: 'generated_tokens_mean' + }, + { + title: 'ITL Avg', + dataIndex: 'inter_token_latency_mean' + }, + { + title: intl.formatMessage({ id: 'common.table.status' }), + dataIndex: 'state' + }, + { + title: intl.formatMessage({ id: 'benchmark.table.requestRate' }), + dataIndex: 'request_rate' + }, + { + title: intl.formatMessage({ id: 'benchmark.table.gpu' }), + dataIndex: 'gpu_summary' + }, + { + title: intl.formatMessage({ id: 'benchmark.table.itl' }), + dataIndex: 'inter_token_latency_mean' + }, + { + title: intl.formatMessage({ id: 'benchmark.table.tpot' }), + dataIndex: 'time_per_output_token_mean' + }, + { + title: intl.formatMessage({ id: 'benchmark.table.ttft' }), + dataIndex: 'time_to_first_token_mean' + }, + { + title: intl.formatMessage({ id: 'benchmark.table.rps' }), + dataIndex: 'requests_per_second_mean' + }, + { + title: intl.formatMessage({ id: 'benchmark.table.tps' }), + dataIndex: 'tokens_per_second_mean' + }, + { + title: intl.formatMessage({ id: 'common.table.createTime' }), + dataIndex: 'created_at' + }, + { + title: intl.formatMessage({ id: 'common.table.operation' }), + dataIndex: 'operations' + } + ]; + + const contentRender = () => { + return ( + +
Column Settings
+ { + setSelectedColumns(checkedValues as string[]); + }} + > + + {allColumns.map((col) => ( + + + {col.title} + + + ))} + + +
+ +
+ + +
+
+
+ ); + }; + + const SettingsButton = ( + + + + + + ); + + return { + SettingsButton, + selectedColumns + }; +}; + +export default useColumnSettings; diff --git a/src/pages/benchmark/hooks/use-view-detail.ts b/src/pages/benchmark/hooks/use-view-detail.ts new file mode 100644 index 00000000..53f93e9a --- /dev/null +++ b/src/pages/benchmark/hooks/use-view-detail.ts @@ -0,0 +1,35 @@ +import { useState } from 'react'; +import { BenchmarkListItem as ListItem } from '../config/types'; + +const useViewDetail = () => { + const [openModalStatus, setOpenModalStatus] = useState<{ + open: boolean; + currentData?: ListItem; + }>({ + open: false, + currentData: undefined + }); + + const openModal = (rows?: ListItem) => { + setOpenModalStatus({ + open: true, + currentData: rows + }); + }; + + const closeModal = () => { + setOpenModalStatus({ + open: false, + currentData: undefined + }); + }; + + return { + openViewDetailModalStatus: openModalStatus, + setOpenViewDetailModalStatus: setOpenModalStatus, + openViewDetailModal: openModal, + closeViewDetailModal: closeModal + }; +}; + +export default useViewDetail; diff --git a/src/pages/benchmark/index.tsx b/src/pages/benchmark/index.tsx index d8dbe41a..bdedc662 100644 --- a/src/pages/benchmark/index.tsx +++ b/src/pages/benchmark/index.tsx @@ -4,7 +4,7 @@ import { FilterBar } from '@/components/page-tools'; import { PageAction } from '@/config'; import { TABLE_SORT_DIRECTIONS } from '@/config/settings'; import useTableFetch from '@/hooks/use-table-fetch'; -import { useIntl } from '@umijs/max'; +import { useIntl, useNavigate } from '@umijs/max'; import { useMemoizedFn } from 'ahooks'; import { ConfigProvider, Table, message } from 'antd'; import _ from 'lodash'; @@ -17,11 +17,14 @@ import { updateBenchmark } from './apis'; import AddBenchmarkModal from './components/add-benchmark-modal'; +import DetailModal from './components/detail-modal'; import LeftActions from './components/left-actions'; import RightActions from './components/right-actions'; import { FormData, BenchmarkListItem as ListItem } from './config/types'; import useBenchmarkColumns from './hooks/use-benchmark-columns'; +import useColumnSettings from './hooks/use-column-settings'; import useCreateBenchmark from './hooks/use-create-benchmark'; +import useViewDetail from './hooks/use-view-detail'; const Benchmark: React.FC = () => { const { @@ -43,8 +46,15 @@ const Benchmark: React.FC = () => { contentForDelete: 'menu.models.benchmark' }); const intl = useIntl(); + const navigate = useNavigate(); const { openBenchmarkModal, closeBenchmarkModal, openBenchmarkModalStatus } = useCreateBenchmark(); + const { + openViewDetailModal, + closeViewDetailModal, + openViewDetailModalStatus + } = useViewDetail(); + const { SettingsButton, selectedColumns } = useColumnSettings(); const handleAddBenchmark = () => { openBenchmarkModal(PageAction.CREATE, 'Add Benchmark'); @@ -89,6 +99,16 @@ const Benchmark: React.FC = () => { } }); + const handleOnCellClick = useMemoizedFn( + (record: ListItem, dataIndex: string) => { + if (dataIndex === 'name') { + navigate( + `/models/benchmark/detail?id=${record.id}&name=${record.name}` + ); + } + } + ); + const renderEmpty = (type?: string) => { if (type !== 'Table') return; return ( @@ -111,7 +131,11 @@ const Benchmark: React.FC = () => { ); }; - const columns = useBenchmarkColumns(sortOrder, handleSelect); + const columns = useBenchmarkColumns( + sortOrder, + handleSelect, + handleOnCellClick + ); return ( <> @@ -132,6 +156,7 @@ const Benchmark: React.FC = () => { } right={ { onCancel={handleModalCancel} onOk={handleModalOk} > + ); diff --git a/src/pages/benchmark/services/use-query-dataset.ts b/src/pages/benchmark/services/use-query-dataset.ts new file mode 100644 index 00000000..77d9f6f3 --- /dev/null +++ b/src/pages/benchmark/services/use-query-dataset.ts @@ -0,0 +1,22 @@ +import { useQueryDataList } from '@/hooks/use-query-data-list'; +import { queryDatasetList } from '../apis'; +import { DatasetListItem } from '../config/types'; + +const useQueryDataset = () => { + const { dataList, loading, fetchData, cancelRequest } = useQueryDataList< + DatasetListItem, + Global.SearchParams + >({ + key: 'datasetList', + fetchList: queryDatasetList + }); + + return { + dataList, + loading, + fetchData, + cancelRequest + }; +}; + +export default useQueryDataset; diff --git a/src/pages/benchmark/services/use-query-detail.ts b/src/pages/benchmark/services/use-query-detail.ts new file mode 100644 index 00000000..e2bce7ad --- /dev/null +++ b/src/pages/benchmark/services/use-query-detail.ts @@ -0,0 +1,17 @@ +import { useQueryData } from '@/hooks/use-query-data-list'; +import { queryBenchmarkDetail } from '../apis'; +import { BenchmarkDetail } from '../config/detail-types'; + +export default function useQueryBenchmarkDetail() { + const { detailData, loading, cancelRequest, fetchData } = + useQueryData({ + fetchDetail: queryBenchmarkDetail, + key: 'benchmarkDetail' + }); + return { + detailData, + loading, + cancelRequest, + fetchData + }; +} diff --git a/src/pages/benchmark/services/use-query-metrics.ts b/src/pages/benchmark/services/use-query-metrics.ts new file mode 100644 index 00000000..de28520c --- /dev/null +++ b/src/pages/benchmark/services/use-query-metrics.ts @@ -0,0 +1,22 @@ +import { useQueryData } from '@/hooks/use-query-data-list'; +import { queryBenchmarkMetrics } from '../apis'; +import { BenchmarkMetricsFormData } from '../config/detail-types'; + +export default function useQueryBenchmarkMetrics() { + const { detailData, loading, cancelRequest, fetchData } = useQueryData< + any, + { + id: number; + data: BenchmarkMetricsFormData; + } + >({ + fetchDetail: queryBenchmarkMetrics, + key: 'benchmarkMetrics' + }); + return { + detailData, + loading, + cancelRequest, + fetchData + }; +} diff --git a/src/pages/cluster-management/apis/index.ts b/src/pages/cluster-management/apis/index.ts index 9c26838c..551a74c5 100644 --- a/src/pages/cluster-management/apis/index.ts +++ b/src/pages/cluster-management/apis/index.ts @@ -106,10 +106,14 @@ export async function deleteCredential(id: number) { // ===================== Cluster ===================== -export async function queryClusterList(params: Global.SearchParams) { +export async function queryClusterList( + params: Global.SearchParams, + options?: any +) { return request>(`${CLUSTERS_API}`, { method: 'GET', - params + params, + cancelToken: options?.token }); } diff --git a/src/pages/cluster-management/services/use-query-cluster-list.tsx b/src/pages/cluster-management/services/use-query-cluster-list.tsx new file mode 100644 index 00000000..6ce0bac0 --- /dev/null +++ b/src/pages/cluster-management/services/use-query-cluster-list.tsx @@ -0,0 +1,67 @@ +import { createAxiosToken } from '@/hooks/use-chunk-request'; +import { useRequest } from 'ahooks'; +import { message } from 'antd'; +import { CancelTokenSource } from 'axios'; +import { useEffect, useRef, useState } from 'react'; +import { queryClusterList } from '../apis'; +import { ClusterListItem } from '../config/types'; + +/** + * + * @returns loading, fetch, dataList + */ +export const useQueryClusterList = () => { + const axiosTokenRef = useRef(null); + const [dataList, setDataList] = useState< + Array & { label: string; value: number }> + >([]); + + const { + runAsync: fetchData, + loading, + cancel + } = useRequest( + async (params: { page: number; perPage?: number }) => { + axiosTokenRef.current?.cancel(); + axiosTokenRef.current = createAxiosToken(); + const res = await queryClusterList(params, { + token: axiosTokenRef.current.token + }); + setDataList( + res.items?.map((item: ClusterListItem) => ({ + ...item, + label: item.name, + value: item.id + })) || [] + ); + return res.items || []; + }, + { + manual: true, + onSuccess: (response) => {}, + onError: (error) => { + message.error(error?.message || 'Failed to fetch cluster list'); + setDataList([]); + } + } + ); + + const cancelRequest = () => { + cancel(); + axiosTokenRef.current?.cancel(); + }; + + useEffect(() => { + return () => { + cancel(); + axiosTokenRef.current?.cancel(); + }; + }, []); + + return { + loading, + clusterList: dataList, + cancelRequest, + fetchClusterList: fetchData + }; +}; diff --git a/src/pages/llmodels/components/view-logs-modal.tsx b/src/pages/llmodels/components/view-logs-modal.tsx index 5393ae82..ebd7eaf9 100644 --- a/src/pages/llmodels/components/view-logs-modal.tsx +++ b/src/pages/llmodels/components/view-logs-modal.tsx @@ -106,7 +106,7 @@ const ViewLogsModal: React.FC = (props) => { maskClosable={false} keyboard={true} styles={{ - content: { + wrapper: { borderRadius: 0 } }} diff --git a/src/pages/llmodels/services/use-query-model-instances.ts b/src/pages/llmodels/services/use-query-model-instances.ts new file mode 100644 index 00000000..62b15d05 --- /dev/null +++ b/src/pages/llmodels/services/use-query-model-instances.ts @@ -0,0 +1,71 @@ +import { createAxiosToken } from '@/hooks/use-chunk-request'; +import { useRequest } from 'ahooks'; +import { message } from 'antd'; +import { CancelTokenSource } from 'axios'; +import { useEffect, useRef, useState } from 'react'; +import { queryModelInstancesList } from '../apis'; +import { ModelInstanceListItem } from '../config/types'; + +/** + * + * @returns loading, fetch, dataList + */ +export const useQueryModelInstancesList = () => { + const axiosTokenRef = useRef(null); + const [dataList, setDataList] = useState< + Array & { label: string; value: string }> + >([]); + + const { + runAsync: fetchData, + loading, + cancel + } = useRequest( + async (params: { id: number }) => { + const query = { + page: -1, + id: params.id + }; + axiosTokenRef.current?.cancel(); + axiosTokenRef.current = createAxiosToken(); + const res = await queryModelInstancesList(query, { + token: axiosTokenRef.current.token + }); + setDataList( + res.items?.map((item: ModelInstanceListItem) => ({ + ...item, + label: item.name, + value: item.name + })) || [] + ); + return res.items || []; + }, + { + manual: true, + onSuccess: (response) => {}, + onError: (error) => { + message.error(error?.message || 'Failed to fetch model instances list'); + setDataList([]); + } + } + ); + + const cancelRequest = () => { + cancel(); + axiosTokenRef.current?.cancel(); + }; + + useEffect(() => { + return () => { + cancel(); + axiosTokenRef.current?.cancel(); + }; + }, []); + + return { + loading, + instanceList: dataList, + cancelRequest, + fetchInstanceList: fetchData + }; +}; diff --git a/src/pages/llmodels/services/use-query-model-list.ts b/src/pages/llmodels/services/use-query-model-list.ts new file mode 100644 index 00000000..7a1a6b6a --- /dev/null +++ b/src/pages/llmodels/services/use-query-model-list.ts @@ -0,0 +1,67 @@ +import { createAxiosToken } from '@/hooks/use-chunk-request'; +import { useRequest } from 'ahooks'; +import { message } from 'antd'; +import { CancelTokenSource } from 'axios'; +import { useEffect, useRef, useState } from 'react'; +import { queryModelsList } from '../apis'; +import { ListItem } from '../config/types'; + +/** + * + * @returns loading, fetch, dataList + */ +export const useQueryModelList = () => { + const axiosTokenRef = useRef(null); + const [dataList, setDataList] = useState< + Array & { label: string; value: number }> + >([]); + + const { + runAsync: fetchData, + loading, + cancel + } = useRequest( + async (params: { page: number; perPage?: number; cluster_id?: number }) => { + axiosTokenRef.current?.cancel(); + axiosTokenRef.current = createAxiosToken(); + const res = await queryModelsList(params, { + token: axiosTokenRef.current.token + }); + setDataList( + res.items?.map((item: ListItem) => ({ + ...item, + label: item.name, + value: item.id + })) || [] + ); + return res.items || []; + }, + { + manual: true, + onSuccess: (response) => {}, + onError: (error) => { + message.error(error?.message || 'Failed to fetch model list'); + setDataList([]); + } + } + ); + + const cancelRequest = () => { + cancel(); + axiosTokenRef.current?.cancel(); + }; + + useEffect(() => { + return () => { + cancel(); + axiosTokenRef.current?.cancel(); + }; + }, []); + + return { + loading, + modelList: dataList, + cancelRequest, + fetchModelList: fetchData + }; +}; diff --git a/src/request-config.ts b/src/request-config.ts index 046f213e..1c370eb4 100644 --- a/src/request-config.ts +++ b/src/request-config.ts @@ -8,6 +8,10 @@ import { DEFAULT_ENTER_PAGE } from './config/settings'; const NoBaseURLAPIs = ['/auth', '/v1', '/version', '/proxy', '/update']; export const requestConfig: RequestConfig = { + headers: { + 'Content-Security-Policy': "frame-ancestors 'self'", + 'X-Frame-Options': 'SAMEORIGIN' + }, errorConfig: { errorThrower: (res: any) => { // to do something