chore: detail api

This commit is contained in:
jialin
2026-01-30 18:48:55 +08:00
parent 358df925e7
commit 8f2eea70f6
43 changed files with 1938 additions and 97 deletions
+142
View File
@@ -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<ListItem, Params = any>(option: {
key: string;
fetchList: (
params: Params,
options?: any
) => Promise<Global.PageResponse<ListItem>>;
getLabel?: (item: ListItem) => string;
getValue?: (item: ListItem) => any;
errorMsg?: string;
}) {
const { key, fetchList, getLabel, getValue, errorMsg } = option;
const axiosTokenRef = useRef<CancelTokenSource | null>(null);
const [dataList, setDataList] = useState<
Array<ListItem & { label: string; value: any }>
>([]);
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<Detail, Params = any>(option: {
key: string;
fetchDetail: (params: Params, options?: any) => Promise<Detail>;
getData?: (response: Detail) => any;
errorMsg?: string;
}) {
const { key, fetchDetail, getData, errorMsg } = option;
const axiosTokenRef = useRef<CancelTokenSource | null>(null);
const [detailData, setDetailData] = useState<Detail>({} 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
};
}
+1
View File
@@ -46,6 +46,7 @@ const NO_CONTAINER_PAGES = [
'text2images',
'clusterDetail',
'clusterCreate',
'benchmarkDetail',
'video'
];
@@ -10,6 +10,9 @@ const StyledBreadcrumb = styled(Breadcrumb)`
a {
background: unset;
}
a:hover {
background: unset;
}
}
.ant-breadcrumb-separator {
display: flex;
+32 -3
View File
@@ -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<Global.PageResponse<BenchmarkListItem>>(`${DATASETS_API}`, {
return request<Global.PageResponse<DatasetListItem>>(`${DATASETS_API}`, {
method: 'GET',
params,
cancelToken: options?.token
});
}
export async function queryBenchmarkMetrics(
params: {
id: number;
data: BenchmarkMetricsFormData;
},
options?: {
token?: CancelToken;
}
) {
return request<any>(`${BENCHMARKS_API}/${params.id}/metrics`, {
method: 'POST',
data: params.data,
cancelToken: options?.token
});
}
@@ -49,6 +49,7 @@ const AddBenchmark: React.FC<AddModalProps> = ({
<BenchmarkForm
ref={form}
action={action}
open={open}
currentData={currentData}
onFinish={handleOk}
/>
@@ -184,7 +184,7 @@ const CompareConditions: React.FC = () => {
}}
>
<Container>
<span className="holder">Compare Filter</span>
<span className="holder">Export filter</span>
</Container>
</Popover>
);
@@ -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: <Summary />,
icon: <IconFont type="icon-basic" />
},
{
key: 'configure',
label: intl.formatMessage({ id: 'benchmark.detail.configure.title' }),
children: <Configure />,
icon: <IconFont type="icon-settings-02" />
},
{
key: 'environment',
label: intl.formatMessage({ id: 'benchmark.detail.environment.title' }),
children: <Environment />,
icon: <IconFont type="icon-server02" />
},
{
key: 'logs',
label: intl.formatMessage({ id: 'benchmark.detail.logs.title' }),
children: <Logs />,
icon: <IconFont type="icon-logs" />
}
];
const handleChangeTab = (key: string) => {
setActiveKey(key);
};
useEffect(() => {
if (id) {
fetchData(id);
fetchClusterList({ page: -1 });
} else {
cancelRequest();
cancelClusterRequest();
}
}, [id]);
return (
<DetailContext.Provider
value={{
detailData: detailData || {},
clusterList: clusterList,
loading: loading,
id: Number(id)
}}
>
<Tabs
size="small"
activeKey={activeKey}
onChange={handleChangeTab}
items={items}
type="card"
/>
</DetailContext.Provider>
);
};
export default Details;
@@ -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<DetailModalProps> = ({
open,
currentData,
onClose
}) => {
return (
<FormDrawer
title={currentData?.name}
open={open}
onCancel={onClose}
width={700}
footer={null}
>
<DetailContent currentData={currentData} />
</FormDrawer>
);
};
export default DetailModal;
@@ -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<GPUData> = (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 (
<Section title={`GPU ${props.index}`}>
<Descriptions
items={items}
colon={false}
column={3}
layout="vertical"
styles={{
content: {
justifyContent: 'flex-start'
}
}}
></Descriptions>
</Section>
);
};
export default Environment;
@@ -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 <div>Environment Content</div>;
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 (
<Container>
<WorkerData
workerData={mainWorker.workerData}
gpuData={mainWorker.gpuData}
title={
<div className="flex-center gap-8">
<Tag color="geekblue">Main</Tag>
<span>{mainWorker?.workerData?.name}</span>
</div>
}
></WorkerData>
<WorkerData
workerData={mainWorker.workerData}
gpuData={mainWorker.gpuData}
title={
<div className="flex-center gap-8">
<Tag color="geekblue">Sub</Tag>
<span>{mainWorker?.workerData?.name}</span>
</div>
}
></WorkerData>
{/* {subWorkers?.map?.((worker, index) => (
<WorkerData
key={index}
workerData={worker.workerData}
gpuData={worker.gpuData}
></WorkerData>
))} */}
</Container>
);
};
export default Environment;
@@ -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<WorkerData> = (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 (
<div>
<Descriptions
items={items}
colon={false}
column={3}
layout="vertical"
styles={{
content: {
justifyContent: 'flex-start'
}
}}
></Descriptions>
</div>
);
};
export default Environment;
@@ -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 (
<Container>
<Section
title={title}
styles={{
container: {
border: 'none',
paddingInline: 16,
borderRadius: 0,
borderBottom: '1px solid var(--ant-color-split)'
}
}}
>
<Metadata {...props.workerData} />
</Section>
<Content>
<Row gutter={16}>
{props.gpuData.map((gpu, index) => (
<Col span={12} key={index}>
<GPUData {...gpu} />
</Col>
))}
</Row>
</Content>
</Container>
);
};
export default Environment;
@@ -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<HTMLInputElement>) => void;
@@ -30,7 +29,7 @@ const RightActions: React.FC<RightActionsProps> = ({
allowClear
onChange={handleInputChange}
></Input>
<CompareConditions></CompareConditions>
{/* <CompareConditions></CompareConditions> */}
<Button
type="text"
style={{ color: 'var(--ant-color-text-tertiary)' }}
+19 -2
View File
@@ -1,9 +1,26 @@
import React from 'react';
import LogsViewer from '@/components/logs-viewer/virtual-log-list';
import React, { useRef } from 'react';
import { BENCHMARKS_API } from '../../apis';
import { useDetailContext } from '../../config/detail-context';
const Logs: React.FC = () => {
const { id } = useDetailContext();
return <div>Logs Content</div>;
const logsViewerRef = useRef<any>(null);
return (
<div>
<LogsViewer
ref={logsViewerRef}
diffHeight={175}
url={`${BENCHMARKS_API}/${id}/logs`}
tail={undefined}
enableScorllLoad={true}
isDownloading={false}
params={{
follow: true
}}
></LogsViewer>
</div>
);
};
export default Logs;
@@ -1,7 +1,8 @@
import DropdownButtons from '@/components/drop-down-buttons';
import {
DeleteOutlined,
PlusOutlined,
SettingOutlined
DownloadOutlined,
PlusOutlined
} from '@ant-design/icons';
import { useIntl } from '@umijs/max';
import { Button, Space } from 'antd';
@@ -11,6 +12,7 @@ export interface RightActionsProps {
handleDeleteByBatch: () => void;
handleClickPrimary?: () => void;
handleSettingFields?: () => void;
settingButton?: React.ReactNode;
buttonText?: string;
rowSelection: {
selectedRowKeys: React.Key[];
@@ -21,14 +23,38 @@ const RightActions: React.FC<RightActionsProps> = ({
handleDeleteByBatch,
handleClickPrimary,
handleSettingFields,
settingButton,
buttonText,
rowSelection
}) => {
const intl = useIntl();
const ButtonList = [
{
label: 'common.button.export',
key: 'export',
icon: <DownloadOutlined />
},
{
label: 'common.button.delete',
key: 'start',
props: {
danger: true
},
icon: <DeleteOutlined />
}
];
const handleActionSelect = (val: string) => {};
return (
<Space size={16}>
<Button onClick={handleSettingFields} icon={<SettingOutlined />}></Button>
{/* <Tooltip title="Column Settings">
<Button
onClick={handleSettingFields}
icon={<SettingOutlined />}
></Button>
</Tooltip> */}
{settingButton}
<Button
icon={<PlusOutlined></PlusOutlined>}
type="primary"
@@ -36,7 +62,7 @@ const RightActions: React.FC<RightActionsProps> = ({
>
{buttonText}
</Button>
<Button
{/* <Button
icon={<DeleteOutlined />}
danger
onClick={handleDeleteByBatch}
@@ -48,7 +74,19 @@ const RightActions: React.FC<RightActionsProps> = ({
<span>({rowSelection?.selectedRowKeys?.length})</span>
)}
</span>
</Button>
</Button> */}
<DropdownButtons
items={ButtonList}
extra={
rowSelection.selectedRowKeys.length > 0 && (
<span>({rowSelection.selectedRowKeys.length})</span>
)
}
size="large"
showText={true}
disabled={!rowSelection.selectedRowKeys.length}
onSelect={handleActionSelect}
/>
</Space>
);
};
@@ -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: <AutoTooltip ghost>{detailData.model_name}</AutoTooltip>
},
{
key: '2',
label: 'Cluster',
children: (
<AutoTooltip ghost>
{clusterList?.find((item) => item.value === detailData.cluster_id)
?.label || detailData.cluster_id}
</AutoTooltip>
)
},
{
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 (
<Container>
<Row gutter={16}>
{cardFields.map((field) => (
<Col span={8} key={field.key} style={{ padding: '8px' }}>
<Card>
<Statistic
title={field.label}
value={field.value}
precision={2}
styles={{
content: {
color: field.color,
fontSize: 16,
fontWeight: 500
},
header: {
paddingBottom: 4
}
}}
suffix={field.unit}
/>
</Card>
</Col>
))}
</Row>
<Section>
<div className="flex-center section-title gap-8">
<span>{detailData.model_instance_name}</span>
<StatusTag
statusValue={{
status: BenchmarkStatus[detailData.state],
text: BenchmarkStatusLabelMap[detailData.state],
message: detailData.state_message || undefined
}}
/>
</div>
<Descriptions
items={items}
colon={false}
column={3}
layout="vertical"
styles={{
content: {
justifyContent: 'flex-start'
}
}}
></Descriptions>
</Section>
<Box>
<Section>
<div className="flex-center section-title gap-8">
<span>Throughput</span>
</div>
<Descriptions
items={throughputFields}
colon={true}
column={1}
styles={{
content: {
justifyContent: 'flex-end'
}
}}
></Descriptions>
</Section>
<Section>
<div className="flex-center section-title gap-8">
<span>Latency</span>
</div>
<Descriptions
items={latencyFields}
colon={true}
column={1}
styles={{
content: {
justifyContent: 'flex-end'
}
}}
></Descriptions>
</Section>
</Box>
<PercentileResult />
</Container>
);
};
export default Summary;
@@ -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 <div>Summary Content</div>;
return (
<Container>
<BasicInfo />
<MetricsInfo />
</Container>
);
};
export default Summary;
@@ -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 <div></div>;
};
export default Summary;
@@ -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 (
<Section title="Percentiles Result">
<Table
size="small"
columns={columns}
dataSource={data}
rowKey="percentile"
pagination={false}
styles={{
body: {
cell: {
height: 54
}
}
}}
></Table>
</Section>
);
};
export default PercentileResult;
@@ -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 (
<Container style={styles?.container}>
{title && <Title style={styles?.title}>{title}</Title>}
{children}
</Container>
);
};
export default DetailSection;
+3 -1
View File
@@ -1,7 +1,9 @@
import { createContext, useContext } from 'react';
import { BenchmarkDetail } from './detail-types';
interface DetailContextProps {
detailData: any;
detailData: BenchmarkDetail;
clusterList?: Global.BaseOption<number>[];
id: number;
loading?: boolean;
}
+117
View File
@@ -0,0 +1,117 @@
export interface ComputedResourceClaim {
is_unified_memory: boolean;
offload_layers: any;
total_layers: any;
ram: any;
vram: Record<string, number>;
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<string, any>;
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<string, InstancesData>;
workers: Record<string, WorkerData>;
gpus: Record<string, GPUData>;
};
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<string, any>;
}
+3 -3
View File
@@ -3,9 +3,9 @@ import { createContext, useContext } from 'react';
interface FormContextProps {
action: PageActionType;
clusterList: Global.BaseOption<number>[];
modelList: Global.BaseOption<number>[];
modelInstanceList: Global.BaseOption<number>[];
open?: boolean;
clusterList?: Global.BaseOption<number>[];
modelList?: Global.BaseOption<number>[];
}
const FormContext = createContext<FormContextProps>({} as FormContextProps);
+1
View File
@@ -67,6 +67,7 @@ export interface GPUSnapshot {
export interface FormData {
name: string;
profile: string;
description: string;
labels: Record<string, string>;
cluster_id: number;
+19 -42
View File
@@ -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: <Summary />
title: <a>{intl.formatMessage({ id: 'benchmark.title' })}</a>,
onClick: () => navigate(-1)
},
{
key: 'configure',
label: intl.formatMessage({ id: 'benchmark.detail.configure.title' }),
children: <Configure />
},
{
key: 'environment',
label: intl.formatMessage({ id: 'benchmark.detail.environment.title' }),
children: <Environment />
},
{
key: 'logs',
label: intl.formatMessage({ id: 'benchmark.detail.logs.title' }),
children: <Logs />
title: name
}
];
const handleChangeTab = (key: string) => {
setActiveKey(key);
};
return (
<PageBox>
<DetailContext.Provider value={{}}>
<Tabs
activeKey={activeKey}
onChange={handleChangeTab}
items={items}
type="card"
/>
</DetailContext.Provider>
</PageBox>
<PageContainerInner
header={{
title: <PageBreadcrumb items={breadcrumbItems} />
}}
>
<DetailContent></DetailContent>
</PageContainerInner>
);
};
+92 -5
View File
@@ -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 (
<>
<Form.Item<FormData>
@@ -47,13 +121,14 @@ const BasicForm: React.FC = () => {
]}
>
<SealSelect
loading={clusterLoading}
options={clusterList}
label={intl.formatMessage({ id: 'clusters.title' })}
required
></SealSelect>
</Form.Item>
<Form.Item<FormData>
name="model_id"
name="model_name"
rules={[
{
required: true,
@@ -62,11 +137,21 @@ const BasicForm: React.FC = () => {
]}
>
<SealSelect
options={modelList}
loading={modelLoading}
options={modelList.map((item) => ({
...item,
label: item.name,
value: item.name
}))}
onOpenChange={onModelListOpenChange}
onChange={handleOnModelChange}
label={intl.formatMessage({ id: 'benchmark.table.model' })}
required
></SealSelect>
</Form.Item>
<Form.Item<FormData> name="model_id" hidden={true}>
<SealInput.Input></SealInput.Input>
</Form.Item>
<Form.Item<FormData>
name="model_instance_name"
rules={[
@@ -77,7 +162,9 @@ const BasicForm: React.FC = () => {
]}
>
<SealSelect
options={modelInstanceList}
loading={instanceLoading}
options={instanceList}
onOpenChange={onInstanceOpenChange}
label={intl.formatMessage({ id: 'benchmark.table.instance' })}
required
></SealSelect>
+37 -2
View File
@@ -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 = () => {
]}
>
<SealSelect
options={[]}
options={datasetList.map((item) => ({
label: item.name,
value: item.name
}))}
loading={datasetLoading}
onChange={handleOnDataSetChange}
onOpenChange={handleOnDatasetOpenChange}
label={intl.formatMessage({ id: 'benchmark.table.dataset' })}
required
></SealSelect>
</Form.Item>
<Form.Item<FormData> hidden name="dataset_id">
<SealInput.Input></SealInput.Input>
</Form.Item>
<RandomSettingsForm></RandomSettingsForm>
<Form.Item<FormData>
name="request_rate"
+9 -2
View File
@@ -22,6 +22,7 @@ interface ProviderFormProps {
ref?: any;
action: PageActionType;
currentData?: ListItem; // Used when action is EDIT
open?: boolean;
onFinish: (values: FormData) => Promise<void>;
}
@@ -31,9 +32,10 @@ const TABKeysMap = {
};
const ProviderForm: React.FC<ProviderFormProps> = 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<string[]>([TABKeysMap.PROFILE]);
const scrollTabsRef = useRef<any>(null);
@@ -91,7 +93,12 @@ const ProviderForm: React.FC<ProviderFormProps> = forwardRef((props, ref) => {
}}
getScrollElementScrollableHeight={getScrollElementScrollableHeight}
>
<FormContext.Provider value={{ action }}>
<FormContext.Provider
value={{
action,
open
}}
>
<Form form={form} onFinish={onFinish} initialValues={{}}>
<Basic />
<CollapsePanel
+22 -4
View File
@@ -14,7 +14,7 @@ const DatasetForm: React.FC = () => {
return (
<>
<Form.Item<FormData>
name="input_length"
name="dataset_prompt_tokens"
rules={[
{
required: true,
@@ -26,13 +26,22 @@ const DatasetForm: React.FC = () => {
]}
>
<SealSelect
options={[]}
options={[
{
label: '100',
value: 100
},
{
label: '128',
value: 128
}
]}
label={intl.formatMessage({ id: 'benchmark.table.inputTokenLength' })}
required
></SealSelect>
</Form.Item>
<Form.Item<FormData>
name="output_length"
name="dataset_output_tokens"
rules={[
{
required: true,
@@ -44,7 +53,16 @@ const DatasetForm: React.FC = () => {
]}
>
<SealSelect
options={[]}
options={[
{
label: '4',
value: 4
},
{
label: '8',
value: 8
}
]}
label={intl.formatMessage({
id: 'benchmark.table.outputTokenLength'
})}
@@ -5,8 +5,10 @@ import icons from '@/components/icon-font/icons';
import StatusTag from '@/components/status-tag';
import { tableSorter } from '@/config/settings';
import { useIntl } from '@umijs/max';
import { Typography } from 'antd';
import { ColumnsType } from 'antd/es/table';
import dayjs from 'dayjs';
import _ from 'lodash';
import { useMemo } from 'react';
import { BenchmarkStatus, BenchmarkStatusLabelMap } from '../config';
import { BenchmarkListItem as ListItem } from '../config/types';
@@ -29,7 +31,8 @@ const actionList = [
const useBenchmarkColumns = (
sortOrder: string[],
handleSelect: (val: string, record: ListItem) => void
handleSelect: (val: string, record: ListItem) => void,
onCellClick?: (record: ListItem, dataIndex: string) => void
): ColumnsType<ListItem> => {
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) => (
<AutoTooltip ghost minWidth={20}>
{text}
<Typography.Link onClick={() => onCellClick?.(record, 'name')}>
{text}
</Typography.Link>
</AutoTooltip>
)
},
@@ -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) => (
<StatusTag
statusValue={{
@@ -100,51 +109,51 @@ const useBenchmarkColumns = (
},
{
title: intl.formatMessage({ id: 'benchmark.table.itl' }),
dataIndex: 'itl',
dataIndex: 'inter_token_latency_mean',
sorter: tableSorter(1),
render: (text: string) => (
<AutoTooltip ghost minWidth={20}>
{text}
{_.round(text, 1)}
</AutoTooltip>
)
},
{
title: intl.formatMessage({ id: 'benchmark.table.tpot' }),
dataIndex: 'tpot',
dataIndex: 'time_per_output_token_mean',
sorter: tableSorter(1),
render: (text: string) => (
<AutoTooltip ghost minWidth={20}>
{text}
{_.round(text, 2)}
</AutoTooltip>
)
},
{
title: intl.formatMessage({ id: 'benchmark.table.ttft' }),
dataIndex: 'ttft',
dataIndex: 'time_to_first_token_mean',
sorter: tableSorter(1),
render: (text: string) => (
<AutoTooltip ghost minWidth={20}>
{text}
{_.round(text, 2)}
</AutoTooltip>
)
},
{
title: intl.formatMessage({ id: 'benchmark.table.rps' }),
dataIndex: 'requests_per_second',
dataIndex: 'requests_per_second_mean',
sorter: tableSorter(1),
render: (text: string) => (
<AutoTooltip ghost minWidth={20}>
{text}
{_.round(text, 0)}
</AutoTooltip>
)
},
{
title: intl.formatMessage({ id: 'benchmark.table.tps' }),
dataIndex: 'tokens_per_second',
dataIndex: 'tokens_per_second_mean',
sorter: tableSorter(1),
render: (text: string) => (
<AutoTooltip ghost minWidth={20}>
{text}
{_.round(text, 2)}
</AutoTooltip>
)
},
@@ -153,7 +162,9 @@ const useBenchmarkColumns = (
dataIndex: 'created_at',
sorter: tableSorter(3),
render: (value: string) => (
<span>{dayjs(value).format('YYYY-MM-DD HH:mm:ss')}</span>
<AutoTooltip ghost>
{dayjs(value).format('YYYY-MM-DD HH:mm:ss')}
</AutoTooltip>
)
},
{
@@ -170,7 +181,7 @@ const useBenchmarkColumns = (
)
}
];
}, [intl, handleSelect]);
}, [intl, onCellClick, handleSelect]);
};
export default useBenchmarkColumns;
@@ -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<string[]>([]);
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 (
<Container>
<div className="title">Column Settings</div>
<Checkbox.Group
value={selectedColumns}
onChange={(checkedValues) => {
setSelectedColumns(checkedValues as string[]);
}}
>
<Row>
{allColumns.map((col) => (
<Col key={col.dataIndex} span={12}>
<Checkbox value={col.dataIndex} style={{ marginBottom: 8 }}>
<span className="text-secondary">{col.title}</span>
</Checkbox>
</Col>
))}
</Row>
</Checkbox.Group>
<div className="btn-wrapper">
<Button
size="middle"
onClick={() => {
setSelectedColumns([]);
}}
>
Clear All
</Button>
<div className="buttons">
<Button
size="middle"
type="primary"
onClick={() => {
setSelectedColumns(allColumns.map((col) => col.dataIndex));
}}
>
Select All
</Button>
<Button
size="middle"
type="primary"
onClick={() => {
setSelectedColumns(allColumns.map((col) => col.dataIndex));
}}
>
Confirm
</Button>
</div>
</div>
</Container>
);
};
const SettingsButton = (
<Popover
trigger={'click'}
arrow={false}
placement="bottomRight"
content={contentRender()}
styles={{
root: {
width: '420px'
}
}}
>
<Tooltip title="Column Settings">
<Button onClick={handleToggle} icon={<SettingOutlined />}></Button>
</Tooltip>
</Popover>
);
return {
SettingsButton,
selectedColumns
};
};
export default useColumnSettings;
@@ -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;
+32 -2
View File
@@ -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={
<RightActions
settingButton={SettingsButton}
handleDeleteByBatch={handleDeleteBatch}
handleClickPrimary={handleAddBenchmark}
buttonText={intl.formatMessage({
@@ -171,6 +196,11 @@ const Benchmark: React.FC = () => {
onCancel={handleModalCancel}
onOk={handleModalOk}
></AddBenchmarkModal>
<DetailModal
open={openViewDetailModalStatus.open}
currentData={openViewDetailModalStatus.currentData}
onClose={closeViewDetailModal}
></DetailModal>
<DeleteModal ref={modalRef}></DeleteModal>
</>
);
@@ -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;
@@ -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<BenchmarkDetail>({
fetchDetail: queryBenchmarkDetail,
key: 'benchmarkDetail'
});
return {
detailData,
loading,
cancelRequest,
fetchData
};
}
@@ -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
};
}
+6 -2
View File
@@ -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<Global.PageResponse<ClusterListItem>>(`${CLUSTERS_API}`, {
method: 'GET',
params
params,
cancelToken: options?.token
});
}
@@ -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<CancelTokenSource | null>(null);
const [dataList, setDataList] = useState<
Array<Partial<ClusterListItem> & { 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
};
};
@@ -106,7 +106,7 @@ const ViewLogsModal: React.FC<ViewModalProps> = (props) => {
maskClosable={false}
keyboard={true}
styles={{
content: {
wrapper: {
borderRadius: 0
}
}}
@@ -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<CancelTokenSource | null>(null);
const [dataList, setDataList] = useState<
Array<Partial<ModelInstanceListItem> & { 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
};
};
@@ -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<CancelTokenSource | null>(null);
const [dataList, setDataList] = useState<
Array<Partial<ListItem> & { 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
};
};
+4
View File
@@ -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