diff --git a/config/routes.ts b/config/routes.ts
index 5706e256..2bfd8cdc 100644
--- a/config/routes.ts
+++ b/config/routes.ts
@@ -31,6 +31,13 @@ export default [
access: 'canSeeAdmin',
component: './resources'
},
+ // {
+ // name: 'usage',
+ // path: '/usage',
+ // key: 'usage',
+ // icon: 'BarChartOutlined',
+ // component: './usage'
+ // },
{
name: 'apikeys',
path: '/api-keys',
diff --git a/src/components/echarts/chart.tsx b/src/components/echarts/chart.tsx
index cf8c5eb2..e1cf3049 100644
--- a/src/components/echarts/chart.tsx
+++ b/src/components/echarts/chart.tsx
@@ -50,7 +50,7 @@ const Chart: React.FC<{
return () => {
window.removeEventListener('resize', handleResize);
};
- }, [resize, chart]);
+ }, [resize]);
return
;
};
diff --git a/src/components/seal-table/components/header.tsx b/src/components/seal-table/components/header.tsx
index e624644d..ef146156 100644
--- a/src/components/seal-table/components/header.tsx
+++ b/src/components/seal-table/components/header.tsx
@@ -48,4 +48,4 @@ const Header: React.FC = (props) => {
);
};
-export default Header;
+export default React.memo(Header);
diff --git a/src/components/seal-table/index.tsx b/src/components/seal-table/index.tsx
index 402e991d..a970fe48 100644
--- a/src/components/seal-table/index.tsx
+++ b/src/components/seal-table/index.tsx
@@ -8,7 +8,7 @@ import {
type PaginationProps
} from 'antd';
import _ from 'lodash';
-import React, { useCallback, useEffect, useRef, useState } from 'react';
+import React, { useEffect, useMemo, useRef, useState } from 'react';
import Header from './components/header';
import TableRow from './components/table-row';
import './styles/index.less';
@@ -75,7 +75,7 @@ const SealTable: React.FC = (
pagination?.onShowSizeChange?.(current, size);
};
- const renderHeaderPrefix = () => {
+ const renderHeaderPrefix = useMemo(() => {
if (expandable && rowSelection) {
return (
@@ -107,9 +107,9 @@ const SealTable: React.FC
= (
);
}
return null;
- };
+ }, [expandable, rowSelection, selectAll, indeterminate]);
- const renderContent = useCallback(() => {
+ const renderContent = useMemo(() => {
if (!props.dataSource.length) {
return (
@@ -148,12 +148,12 @@ const SealTable: React.FC
= (
{
- {renderHeaderPrefix()}
+ {renderHeaderPrefix}
}
-
{renderContent()}
+
{renderContent}
{pagination && (
diff --git a/src/components/status-tag/copy.less b/src/components/status-tag/copy.less
new file mode 100644
index 00000000..8b54e76b
--- /dev/null
+++ b/src/components/status-tag/copy.less
@@ -0,0 +1,20 @@
+:local(.status-content-wrapper) {
+ // padding-top: 24px;
+ &:hover {
+ :global(.copy-button-wrapper) {
+ display: block;
+ }
+ }
+
+ :global {
+ .copy-button-wrapper {
+ display: none;
+ background-color: #383838;
+ position: absolute;
+ z-index: 10;
+ right: 0;
+ top: 0;
+ border-radius: 0 8px 0 4px;
+ }
+ }
+}
diff --git a/src/components/status-tag/index.tsx b/src/components/status-tag/index.tsx
index c6789bb9..ae0844dd 100644
--- a/src/components/status-tag/index.tsx
+++ b/src/components/status-tag/index.tsx
@@ -3,7 +3,9 @@ import { StatusType } from '@/config/types';
import { InfoCircleOutlined } from '@ant-design/icons';
import { Tooltip } from 'antd';
import classNames from 'classnames';
-import { useEffect, useState } from 'react';
+import { useEffect, useMemo, useState } from 'react';
+import CopyButton from '../copy-button';
+import CopyStyle from './copy.less';
import './index.less';
export const StatusMaps = {
@@ -58,6 +60,20 @@ const StatusTag: React.FC
= ({
}
return {text};
};
+ const renderTitle = useMemo(() => {
+ return (
+
+
+
+
+
{statusValue.message}
+
+ );
+ }, [statusValue]);
return (
= ({
>
{statusValue.message ? (
diff --git a/src/locales/en-US.ts b/src/locales/en-US.ts
index 6e6483f7..6601455f 100644
--- a/src/locales/en-US.ts
+++ b/src/locales/en-US.ts
@@ -5,6 +5,7 @@ import menu from './en-US/menu';
import models from './en-US/models';
import playground from './en-US/playground';
import resources from './en-US/resources';
+import usage from './en-US/usage';
import users from './en-US/users';
export default {
@@ -15,5 +16,6 @@ export default {
...resources,
...apikeys,
...users,
- ...dashboard
+ ...dashboard,
+ ...usage
};
diff --git a/src/locales/en-US/common.ts b/src/locales/en-US/common.ts
index f6374632..88d7821c 100644
--- a/src/locales/en-US/common.ts
+++ b/src/locales/en-US/common.ts
@@ -5,6 +5,8 @@ export default {
'common.button.editmode': 'Edit mode',
'common.button.add': 'Add',
'common.button.login': 'Log In',
+ 'common.button.select': 'Select',
+ 'common.button.selected': 'Selected',
'common.button.continue': 'Continue',
'common.button.upload': 'Upload',
'common.button.save': 'Save',
diff --git a/src/locales/en-US/menu.ts b/src/locales/en-US/menu.ts
index f9f9fef6..f0063913 100644
--- a/src/locales/en-US/menu.ts
+++ b/src/locales/en-US/menu.ts
@@ -6,5 +6,6 @@ export default {
'menu.apikeys': 'API Keys',
'menu.users': 'Users',
'menu.profile': 'Profile',
- 'menu.login': 'Login'
+ 'menu.login': 'Login',
+ 'menu.usage': 'Usage'
};
diff --git a/src/locales/en-US/models.ts b/src/locales/en-US/models.ts
index 66cad915..0780fbbe 100644
--- a/src/locales/en-US/models.ts
+++ b/src/locales/en-US/models.ts
@@ -1,5 +1,5 @@
export default {
- 'models.button.deploy': 'Deploy Model From',
+ 'models.button.deploy': 'Deploy Model',
'models.title': 'Models',
'models.title.edit': 'Edit Model',
'models.table.models': 'models',
diff --git a/src/locales/en-US/usage.ts b/src/locales/en-US/usage.ts
new file mode 100644
index 00000000..98c2dc7b
--- /dev/null
+++ b/src/locales/en-US/usage.ts
@@ -0,0 +1,5 @@
+export default {
+ 'usage.title': 'Usage',
+ 'usage.filter.user': 'Filter by User',
+ 'usage.filter.model': 'Filter by Model'
+};
diff --git a/src/locales/lang-config-map.tsx b/src/locales/lang-config-map.tsx
index aa6c0b54..2512146e 100644
--- a/src/locales/lang-config-map.tsx
+++ b/src/locales/lang-config-map.tsx
@@ -1,6 +1,6 @@
import IconFont from '@/components/icon-font';
-export default {
+const langConfigMap = {
'ar-EG': {
lang: 'ar-EG',
label: 'العربية',
@@ -326,3 +326,7 @@ export default {
title: '語言'
}
};
+
+export type LangConfigType = keyof typeof langConfigMap;
+
+export default langConfigMap;
diff --git a/src/locales/zh-CN.ts b/src/locales/zh-CN.ts
index 80926799..fbb797a6 100644
--- a/src/locales/zh-CN.ts
+++ b/src/locales/zh-CN.ts
@@ -5,6 +5,7 @@ import menu from './zh-CN/menu';
import models from './zh-CN/models';
import playground from './zh-CN/playground';
import resources from './zh-CN/resources';
+import usage from './zh-CN/usage';
import users from './zh-CN/users';
export default {
@@ -15,5 +16,6 @@ export default {
...resources,
...apikeys,
...users,
- ...dashboard
+ ...dashboard,
+ ...usage
};
diff --git a/src/locales/zh-CN/common.ts b/src/locales/zh-CN/common.ts
index 04d4a47a..afd69f0b 100644
--- a/src/locales/zh-CN/common.ts
+++ b/src/locales/zh-CN/common.ts
@@ -5,6 +5,8 @@ export default {
'common.button.editmode': '编辑模式',
'common.button.add': '添加',
'common.button.login': '登录',
+ 'common.button.select': '选择',
+ 'common.button.selected': '已选择',
'common.button.continue': '继续',
'common.button.upload': '上传',
'common.button.save': '保存',
diff --git a/src/locales/zh-CN/menu.ts b/src/locales/zh-CN/menu.ts
index 3e9b6d66..60c41720 100644
--- a/src/locales/zh-CN/menu.ts
+++ b/src/locales/zh-CN/menu.ts
@@ -6,5 +6,6 @@ export default {
'menu.apikeys': 'API 密钥',
'menu.users': '用户',
'menu.profile': '个人信息',
- 'menu.login': '登录'
+ 'menu.login': '登录',
+ 'menu.usage': '使用量'
};
diff --git a/src/locales/zh-CN/usage.ts b/src/locales/zh-CN/usage.ts
new file mode 100644
index 00000000..976a48f0
--- /dev/null
+++ b/src/locales/zh-CN/usage.ts
@@ -0,0 +1,5 @@
+export default {
+ 'usage.title': '使用量',
+ 'usage.filter.user': '按用户查询',
+ 'usage.filter.model': '按模型查询'
+};
diff --git a/src/pages/dashboard/components/resource-utilization.tsx b/src/pages/dashboard/components/resource-utilization.tsx
index 08315ec6..0e0af920 100644
--- a/src/pages/dashboard/components/resource-utilization.tsx
+++ b/src/pages/dashboard/components/resource-utilization.tsx
@@ -2,7 +2,7 @@ import LineChart from '@/components/echarts/line-chart';
import { useIntl } from '@umijs/max';
import dayjs from 'dayjs';
import _ from 'lodash';
-import { memo, useContext } from 'react';
+import { memo, useContext, useMemo } from 'react';
import { DashboardContext } from '../config/dashboard-context';
const chartColorMap = {
@@ -119,7 +119,7 @@ const UtilizationOvertime: React.FC = () => {
const tooltipValueFormatter = (value: any) => {
return !value ? value : `${value}%`;
};
- const generateData = () => {
+ const generateData = useMemo(() => {
const legendData: string[] = [];
const xAxisData: string[] = [];
let seriesData: { value: number; time: string; type: string }[] = [];
@@ -147,16 +147,15 @@ const UtilizationOvertime: React.FC = () => {
legendData,
xAxisData: _.uniq(xAxisData)
};
- };
- const { seriesData, legendData, xAxisData } = generateData();
+ }, [data]);
return (
<>
= (props) => {
borderRadius: '8px 0 0 8px'
}
}}
- width="90%"
+ width={
+ modelSource === modelSourceMap.huggingface_value
+ ? 'calc(100vw - 220px)'
+ : 600
+ }
footer={false}
>
-
-
-
+ {modelSource === modelSourceMap.huggingface_value && (
+
+
+
+ )}
{modelSource === modelSourceMap.huggingface_value && (
diff --git a/src/pages/llmodels/components/hf-model-file.tsx b/src/pages/llmodels/components/hf-model-file.tsx
index f370f5d1..fe3c4b0a 100644
--- a/src/pages/llmodels/components/hf-model-file.tsx
+++ b/src/pages/llmodels/components/hf-model-file.tsx
@@ -1,6 +1,7 @@
import { convertFileSize } from '@/utils';
import { SearchOutlined } from '@ant-design/icons';
-import { Button, Col, Empty, Row, Space, Spin } from 'antd';
+import { useIntl } from '@umijs/max';
+import { Col, Empty, Row, Space, Spin } from 'antd';
import classNames from 'classnames';
import _ from 'lodash';
import { useEffect, useState } from 'react';
@@ -14,6 +15,7 @@ interface HFModelFileProps {
onSelectFile?: (file: any) => void;
}
const HFModelFile: React.FC = (props) => {
+ const intl = useIntl();
const [dataSource, setDataSource] = useState({
fileList: [],
loading: false
@@ -61,6 +63,13 @@ const HFModelFile: React.FC = (props) => {
return null;
};
+ const handleOnEnter = (e: any, item: any) => {
+ e.stopPropagation();
+ if (e.key === 'Enter') {
+ handleSelectModelFile(item);
+ }
+ };
+
useEffect(() => {
handleFetchModelFiles();
}, [props.repo]);
@@ -72,16 +81,17 @@ const HFModelFile: React.FC = (props) => {
{dataSource.fileList.length ? (
-
+
{_.map(dataSource.fileList, (item: any) => {
return (
handleSelectModelFile(item)}
+ onKeyDown={(e) => handleOnEnter(e, item)}
>
{item.path}
@@ -91,9 +101,15 @@ const HFModelFile: React.FC = (props) => {
{getModelQuantizationType(item)}
-
+ {/* */}
diff --git a/src/pages/llmodels/components/hf-model-item.tsx b/src/pages/llmodels/components/hf-model-item.tsx
index b610af16..9e15faed 100644
--- a/src/pages/llmodels/components/hf-model-item.tsx
+++ b/src/pages/llmodels/components/hf-model-item.tsx
@@ -4,7 +4,7 @@ import {
FolderOutlined,
HeartOutlined
} from '@ant-design/icons';
-import { Button, Space, Tag } from 'antd';
+import { Space, Tag } from 'antd';
import classNames from 'classnames';
import dayjs from 'dayjs';
import _ from 'lodash';
@@ -42,14 +42,12 @@ const HFModelItem: React.FC = (props) => {
{props.task && (
-
- {props.task}
-
+ {props.task}
)}
@@ -86,9 +84,9 @@ const HFModelItem: React.FC = (props) => {
})}
- */}
)}
diff --git a/src/pages/llmodels/components/model-card.tsx b/src/pages/llmodels/components/model-card.tsx
index aaa63780..e96dd313 100644
--- a/src/pages/llmodels/components/model-card.tsx
+++ b/src/pages/llmodels/components/model-card.tsx
@@ -1,4 +1,5 @@
import IconFont from '@/components/icon-font';
+import { downloadFile } from '@huggingface/hub';
import { Button, Empty, Tag } from 'antd';
import React, { useEffect, useState } from 'react';
import { queryHuggingfaceModelDetail } from '../apis';
@@ -9,6 +10,13 @@ const ModelCard: React.FC<{ repo: string }> = (props) => {
const { repo } = props;
const [modelData, setModelData] = useState({});
+ const loadFile = async (repo: string, sha: string) => {
+ const res = await (
+ await downloadFile({ repo, revision: sha, path: 'README.md' })
+ )?.text();
+ return res;
+ };
+
const getModelCardData = async () => {
if (!repo) {
setModelData(null);
@@ -16,6 +24,12 @@ const ModelCard: React.FC<{ repo: string }> = (props) => {
}
try {
const res = await queryHuggingfaceModelDetail({ repo });
+ const modelConfig = await loadFile(repo, res.sha);
+ console.log('modelcard=======', {
+ res,
+ repo,
+ modelConfig
+ });
setModelData(res);
} catch (error) {
setModelData({});
diff --git a/src/pages/llmodels/components/search-model.tsx b/src/pages/llmodels/components/search-model.tsx
index 23d8c4ed..6a0d9e95 100644
--- a/src/pages/llmodels/components/search-model.tsx
+++ b/src/pages/llmodels/components/search-model.tsx
@@ -1,4 +1,5 @@
import IconFont from '@/components/icon-font';
+import { BulbOutlined } from '@ant-design/icons';
import { useIntl } from '@umijs/max';
import { Button, Input, Select } from 'antd';
import _ from 'lodash';
@@ -243,6 +244,7 @@ const SearchModel: React.FC = (props) => {
renderHFSearch()
) : (
+
{intl.formatMessage(
{ id: 'model.form.ollamatips' },
{ name: intl.formatMessage({ id: 'model.form.ollama.model' }) }
diff --git a/src/pages/llmodels/components/search-result.tsx b/src/pages/llmodels/components/search-result.tsx
index 237acd76..e6e10488 100644
--- a/src/pages/llmodels/components/search-result.tsx
+++ b/src/pages/llmodels/components/search-result.tsx
@@ -21,6 +21,12 @@ const SearchResult: React.FC
= (props) => {
e.stopPropagation();
onSelect?.(item);
};
+ const handleOnEnter = (e: any, item: any) => {
+ e.stopPropagation();
+ if (e.key === 'Enter') {
+ onSelect?.(item);
+ }
+ };
return (
@@ -29,7 +35,10 @@ const SearchResult: React.FC = (props) => {
{resultList.map((item, index) => (
- handleSelect(e, item)}>
+
handleSelect(e, item)}
+ onKeyDown={(e) => handleOnEnter(e, item)}
+ >
(
+ []
+ );
+ const imgCountRef = useRef(0);
+
const inputRef = useRef(null);
useEffect(() => {
@@ -61,6 +68,63 @@ const MessageItem: React.FC<{
uid: message.uid
});
};
+
+ const getPasteContent = useCallback(async (event: any) => {
+ const clipboardData = event.clipboardData || window.clipboardData;
+ const items = clipboardData.items;
+ const imgPromises: Promise[] = [];
+
+ for (let i = 0; i < items.length; i++) {
+ let item = items[i];
+ console.log('item===========', item);
+
+ if (item.kind === 'file' && item.type.indexOf('image') !== -1) {
+ const file = item.getAsFile();
+ const imgPromise = new Promise((resolve, reject) => {
+ const reader = new FileReader();
+ reader.onload = function (event) {
+ const base64String = event.target?.result as string;
+ if (base64String) {
+ resolve(base64String);
+ } else {
+ reject('Failed to convert image to base64');
+ }
+ };
+ reader.readAsDataURL(file);
+ });
+ imgPromises.push(imgPromise);
+ } else if (item.kind === 'string') {
+ // string
+ }
+ }
+
+ try {
+ const imgs = await Promise.all(imgPromises);
+ if (imgs.length) {
+ const list = _.map(imgs, (img: string) => {
+ imgCountRef.current += 1;
+ return {
+ uid: imgCountRef.current,
+ dataUrl: img
+ };
+ });
+ setImgList((pre) => {
+ return [...pre, ...list];
+ });
+ }
+ } catch (error) {
+ console.error('Error processing images:', error);
+ }
+ }, []);
+
+ const handleDeleteImg = useCallback(
+ (uid: number) => {
+ const list = imgList.filter((item) => item.uid !== uid);
+ setImgList(list);
+ },
+ [imgList]
+ );
+
const handleMessageChange = (e: any) => {
// setIsTyping(true);
handleUpdateMessage({ role: message.role, message: e.target.value });
@@ -86,6 +150,16 @@ const MessageItem: React.FC<{
onDelete();
};
+ const handleOnPaste = (e: any) => {
+ e.preventDefault();
+ const text = e.clipboardData.getData('text');
+ if (text) {
+ handleUpdateMessage({ role: message.role, message: text });
+ } else {
+ getPasteContent(e);
+ }
+ };
+
useHotkeys(
HotKeys.SUBMIT,
() => {
@@ -107,6 +181,7 @@ const MessageItem: React.FC<{
+
void;
+}> = ({ dataList, onDelete }) => {
+ const handleOnDelete = (uid: number) => {
+ onDelete(uid);
+ };
+
+ return (
+
+ {_.map(dataList, (item: any) => {
+ return (
+
+
+ handleOnDelete(item.uid)}>
+
+
+
+ );
+ })}
+
+ );
+};
+
+export default ThumbImg;
diff --git a/src/pages/playground/index.tsx b/src/pages/playground/index.tsx
index d82e2855..6b0a7a4d 100644
--- a/src/pages/playground/index.tsx
+++ b/src/pages/playground/index.tsx
@@ -1,7 +1,7 @@
import IconFont from '@/components/icon-font';
import { PageContainer } from '@ant-design/pro-components';
import { useIntl, useSearchParams } from '@umijs/max';
-import { Button, Divider } from 'antd';
+import { Button, Divider, Space } from 'antd';
import classNames from 'classnames';
import { useRef, useState } from 'react';
import GroundLeft from './components/ground-left';
@@ -24,7 +24,7 @@ const Playground: React.FC = () => {
+
{
>
}
>
- >
+
]}
className="playground-container"
>
@@ -58,14 +58,6 @@ const Playground: React.FC = () => {
collapse: collapse
})}
>
- {/* setCollapse(!collapse)}
- icon={collapse ? : }
- style={{ color: 'var(--ant-color-text-tertiary)' }}
- size="small"
- type="text"
- className="collapse-btn"
- > */}
{
search: ''
});
- const fetchData = async () => {
+ const fetchData = useCallback(async () => {
setDataSource((pre) => {
pre.loading = true;
return { ...pre };
@@ -70,7 +70,7 @@ const Resources: React.FC = () => {
});
console.log('error', error);
}
- };
+ }, [queryParams]);
const handlePageChange = (page: number, perPage: number | undefined) => {
console.log(page, perPage);
@@ -81,9 +81,12 @@ const Resources: React.FC = () => {
});
};
- const handleTableChange = (pagination: any, filters: any, sorter: any) => {
- setSortOrder(sorter.order);
- };
+ const handleTableChange = useCallback(
+ (pagination: any, filters: any, sorter: any) => {
+ setSortOrder(sorter.order);
+ },
+ []
+ );
const handleSearch = (e: any) => {
fetchData();
@@ -207,6 +210,7 @@ const Resources: React.FC = () => {
}
>
{record.quota}k
+ },
+ {
+ title: 'Token Utilization',
+ dataIndex: 'utilization',
+ key: 'utilization',
+ render: (text: any, record: any) => {record.utilization}%
+ },
+ {
+ title: 'Members',
+ dataIndex: 'members',
+ key: 'members'
+ }
+];
+
+const projectData = [
+ {
+ id: 1,
+ name: 'copilot-dev',
+ quota: 100,
+ utilization: 50,
+ members: 4
+ },
+ {
+ id: 2,
+ name: 'rag-wiki',
+ quota: 200,
+ utilization: 70,
+ members: 3
+ },
+ {
+ id: 3,
+ name: 'smart-auto-agent',
+ quota: 100,
+ utilization: 20,
+ members: 5
+ },
+ {
+ id: 4,
+ name: 'office-auto-docs',
+ quota: 100,
+ utilization: 25,
+ members: 1
+ },
+ {
+ id: 5,
+ name: 'smart-customer-service',
+ quota: 100,
+ utilization: 46,
+ members: 2
+ }
+];
+const ActiveTable = () => {
+ const intl = useIntl();
+ const data = useContext(DashboardContext).active_models || [];
+ const modelColumns = [
+ {
+ title: intl.formatMessage({ id: 'common.table.name' }),
+ dataIndex: 'name',
+ key: 'name'
+ },
+ // {
+ // title: intl.formatMessage({ id: 'dashboard.gpuutilization' }),
+ // dataIndex: 'gpu_utilization',
+ // key: 'gpu_utilization',
+ // render: (text: any, record: any) => (
+ //
+ // )
+ // },
+ {
+ title: intl.formatMessage({ id: 'dashboard.allocatevram' }),
+ dataIndex: 'resource_claim.memory',
+ key: 'gpu_memory',
+ render: (text: any, record: any) => {
+ return (
+
+ {convertFileSize(record.resource_claim?.gpu_memory || 0)} /{' '}
+ {convertFileSize(record.resource_claim?.memory || 0)}
+
+ );
+ }
+ },
+ {
+ title: intl.formatMessage({ id: 'models.form.replicas' }),
+ dataIndex: 'instance_count',
+ key: 'instance_count'
+ },
+ {
+ title: intl.formatMessage({ id: 'dashboard.tokens' }),
+ dataIndex: 'token_count',
+ key: 'token_count'
+ }
+ ];
+ return (
+
+
+
+ {intl.formatMessage({ id: 'dashboard.activeModels' })}
+
+ }
+ right={false}
+ />
+
+
+
+ );
+};
+
+export default ActiveTable;
diff --git a/src/pages/usage/components/dahboard-inner.tsx b/src/pages/usage/components/dahboard-inner.tsx
new file mode 100644
index 00000000..e2d5ca4e
--- /dev/null
+++ b/src/pages/usage/components/dahboard-inner.tsx
@@ -0,0 +1,63 @@
+import PageTools from '@/components/page-tools';
+import { SyncOutlined } from '@ant-design/icons';
+import { useIntl } from '@umijs/max';
+import { Button, Input, Select, Space } from 'antd';
+import { memo, useCallback, useEffect, useState } from 'react';
+import { queryDashboardData } from '../apis';
+import DashboardContext from '../config/dashboard-context';
+import { DashboardProps } from '../config/types';
+import ActiveTable from './active-table';
+import Overview from './over-view';
+import Usage from './usage';
+
+const Page: React.FC<{ setLoading: (loading: boolean) => void }> = ({
+ setLoading
+}) => {
+ const intl = useIntl();
+ const [data, setData] = useState({} as DashboardProps);
+
+ const getDashboardData = useCallback(async () => {
+ try {
+ setLoading(true);
+ const res = await queryDashboardData();
+ setData(res);
+ setLoading(false);
+ } catch (error) {
+ setLoading(false);
+ setData({} as DashboardProps);
+ }
+ }, []);
+ useEffect(() => {
+ getDashboardData();
+ }, []);
+ return (
+
+
+ {/* */}
+
+
+
+ }
+ >
+
+ }
+ >
+
+
+
+ );
+};
+
+export default memo(Page);
diff --git a/src/pages/usage/components/over-view.less b/src/pages/usage/components/over-view.less
new file mode 100644
index 00000000..e3b421ef
--- /dev/null
+++ b/src/pages/usage/components/over-view.less
@@ -0,0 +1,26 @@
+:local(.card-body) {
+ box-shadow: none !important;
+
+ :global(.ant-card-body) {
+ height: 110px;
+ display: flex;
+ justify-content: space-around;
+ border-radius: var(--ant-border-radius-lg);
+ border: 1px solid var(--color-border-1);
+ }
+}
+
+:local(.row) {
+ :global(.ant-col-5) {
+ flex: 0 0 20%;
+ max-width: 20%;
+ }
+}
+
+.content {
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+ align-items: center;
+ font-size: 14px;
+}
diff --git a/src/pages/usage/components/over-view.tsx b/src/pages/usage/components/over-view.tsx
new file mode 100644
index 00000000..e3c6c8c9
--- /dev/null
+++ b/src/pages/usage/components/over-view.tsx
@@ -0,0 +1,77 @@
+import { useIntl } from '@umijs/max';
+import { Card, Col, Row, Space } from 'antd';
+import _ from 'lodash';
+import React, { useContext } from 'react';
+import { overviewConfigs } from '../config';
+import { DashboardContext } from '../config/dashboard-context';
+import '../styles/index.less';
+import styles from './over-view.less';
+
+const renderCardItem = (data: {
+ label: string;
+ value: React.ReactNode;
+ bgColor: string;
+}) => {
+ const { label, value, bgColor } = data;
+ return (
+
+
+
+ );
+};
+const Overview: React.FC = () => {
+ const intl = useIntl();
+ const data = useContext(DashboardContext).resource_counts || {};
+ console.log('overview===');
+ const renderValue = (
+ value:
+ | number
+ | {
+ healthy: number;
+ warning: number;
+ error: number;
+ }
+ ) => {
+ if (typeof value === 'number') {
+ return value;
+ }
+ return (
+
+ {value.healthy}
+ {value.warning}
+ {value.error}
+
+ );
+ };
+ return (
+
+
+ {overviewConfigs.map((config, index) => (
+
+ {renderCardItem({
+ label: intl.formatMessage({ id: config.label }),
+ value: renderValue(_.get(data, config.key, 0)),
+ bgColor: config.backgroundColor
+ })}
+
+ ))}
+
+
+ );
+};
+
+export default Overview;
diff --git a/src/pages/usage/components/resource-utilization.tsx b/src/pages/usage/components/resource-utilization.tsx
new file mode 100644
index 00000000..0e0af920
--- /dev/null
+++ b/src/pages/usage/components/resource-utilization.tsx
@@ -0,0 +1,168 @@
+import LineChart from '@/components/echarts/line-chart';
+import { useIntl } from '@umijs/max';
+import dayjs from 'dayjs';
+import _ from 'lodash';
+import { memo, useContext, useMemo } from 'react';
+import { DashboardContext } from '../config/dashboard-context';
+
+const chartColorMap = {
+ tickLineColor: 'rgba(217,217,217,0.5)',
+ axislabelColor: 'rgba(0, 0, 0, 0.4)'
+};
+
+const TypeKeyMap = {
+ cpu: {
+ label: 'CPU',
+ type: 'CPU',
+ intl: false,
+ color: 'rgba(250, 173, 20,.8)'
+ },
+ memory: {
+ label: 'dashboard.memory',
+ type: 'Memory',
+ intl: true,
+ color: 'rgba(114, 46, 209,.8)'
+ },
+ gpu: {
+ label: 'GPU',
+ type: 'GPU',
+ intl: false,
+ color: 'rgba(84, 204, 152,.8)'
+ },
+ gpu_memory: {
+ label: 'dashboard.vram',
+ type: 'VRAM',
+ intl: true,
+ color: 'rgba(255, 107, 179, 80%)'
+ }
+};
+
+const option = {
+ title: {
+ text: ''
+ },
+ legend: {
+ itemWidth: 8,
+ itemHeight: 8,
+ data: []
+ },
+ grid: {
+ left: 0,
+ right: 20,
+ bottom: 20,
+ containLabel: true
+ },
+ tooltip: {
+ trigger: 'axis',
+ formatter(params: any) {
+ let result = `${params[0].axisValue}`;
+ params.forEach((item: any) => {
+ result += `
+
+
+ ${item.seriesName}:
+
+ ${item.data.value}
+ `;
+ });
+ return `${result}
`;
+ }
+ },
+ xAxis: {
+ type: 'category',
+ boundaryGap: true,
+ axisTick: {
+ show: true,
+ lineStyle: {
+ color: chartColorMap.tickLineColor
+ }
+ },
+ axisLabel: {
+ color: chartColorMap.axislabelColor,
+ // fontFamily: 'unset',
+ fontSize: 12
+ },
+ axisLine: {
+ show: false
+ },
+ data: []
+ },
+ yAxis: {
+ max: 100,
+ min: 0,
+ splitLine: {
+ show: true,
+ lineStyle: {
+ type: 'dashed'
+ }
+ },
+ axisLabel: {
+ color: chartColorMap.axislabelColor,
+ // fontFamily: 'unset',
+ fontSize: 12
+ },
+ axisTick: {
+ show: false
+ },
+ type: 'value'
+ },
+ series: []
+};
+
+const UtilizationOvertime: React.FC = () => {
+ console.log('systemload=====================');
+ const intl = useIntl();
+ const data = useContext(DashboardContext)?.system_load?.history || {};
+
+ const typeList = ['gpu', 'cpu', 'memory', 'gpu_memory'];
+
+ const tooltipValueFormatter = (value: any) => {
+ return !value ? value : `${value}%`;
+ };
+ const generateData = useMemo(() => {
+ const legendData: string[] = [];
+ const xAxisData: string[] = [];
+ let seriesData: { value: number; time: string; type: string }[] = [];
+ seriesData = _.map(typeList, (item: string) => {
+ const itemConfig = _.get(TypeKeyMap, item, {});
+ const name = itemConfig.intl
+ ? intl.formatMessage({ id: itemConfig.label })
+ : itemConfig.label;
+ legendData.push(name);
+ const itemDataList = _.get(data, item, []);
+ return {
+ name: name,
+ color: itemConfig.color,
+ data: _.map(itemDataList, (item: any) => {
+ xAxisData.push(dayjs(item.timestamp * 1000).format('HH:mm:ss'));
+ return {
+ time: dayjs(item.timestamp * 1000).format('HH:mm:ss'),
+ value: _.round(_.get(item, 'value', 0), 1)
+ };
+ })
+ };
+ });
+ return {
+ seriesData,
+ legendData,
+ xAxisData: _.uniq(xAxisData)
+ };
+ }, [data]);
+
+ return (
+ <>
+
+ >
+ );
+};
+
+export default memo(UtilizationOvertime);
diff --git a/src/pages/usage/components/system-load.tsx b/src/pages/usage/components/system-load.tsx
new file mode 100644
index 00000000..cd9e55d7
--- /dev/null
+++ b/src/pages/usage/components/system-load.tsx
@@ -0,0 +1,119 @@
+import CardWrapper from '@/components/card-wrapper';
+import GaugeChart from '@/components/echarts/gauge';
+import PageTools from '@/components/page-tools';
+import breakpoints from '@/config/breakpoints';
+import useWindowResize from '@/hooks/use-window-resize';
+import { useIntl } from '@umijs/max';
+import { Col, Row } from 'antd';
+import dayjs from 'dayjs';
+import _ from 'lodash';
+import { useContext, useEffect, useState } from 'react';
+import { DashboardContext } from '../config/dashboard-context';
+import ResourceUtilization from './resource-utilization';
+
+const strokeColorFunc = (percent: number) => {
+ if (percent <= 50) {
+ return 'rgb(84, 204, 152, 80%)';
+ }
+ if (percent <= 80) {
+ return 'rgba(250, 173, 20, 80%)';
+ }
+ return ' rgba(255, 77, 79, 80%)';
+};
+
+const SystemLoad = () => {
+ const intl = useIntl();
+ const data = useContext(DashboardContext)?.system_load?.current || {};
+ const { size } = useWindowResize();
+ const [paddingRight, setPaddingRight] = useState('20px');
+ const [smallChartHeight, setSmallChartHeight] = useState(190);
+ const [largeChartHeight, setLargeChartHeight] = useState(400);
+ const thresholds = [0.5, 0.7, 1];
+ const height = 400;
+ const currentDate = dayjs().format('YYYY-MM-DD');
+
+ const handleSelectDate = (date: any) => {};
+
+ useEffect(() => {
+ if (size.width < breakpoints.xl) {
+ setPaddingRight('0');
+ } else {
+ setPaddingRight('20px');
+ }
+ }, [size.width]);
+
+ return (
+
+
+
{intl.formatMessage({ id: 'dashboard.systemload' })}
+ }
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default SystemLoad;
diff --git a/src/pages/usage/components/usage-inner/index.tsx b/src/pages/usage/components/usage-inner/index.tsx
new file mode 100644
index 00000000..9ebc3747
--- /dev/null
+++ b/src/pages/usage/components/usage-inner/index.tsx
@@ -0,0 +1,179 @@
+import PageTools from '@/components/page-tools';
+
+import { useIntl } from '@umijs/max';
+import dayjs from 'dayjs';
+import _ from 'lodash';
+import { memo, useCallback, useContext } from 'react';
+import { DashboardContext } from '../../config/dashboard-context';
+import RequestTokenInner from './request-token-inner';
+
+const baseColorMap = {
+ baseL2: 'rgba(13,171,219,0.8)',
+ baseL1: 'rgba(0,34,255,0.8)',
+ base: 'rgba(0,85,255,0.8)',
+ baseR1: 'rgba(0,255,233,0.8)',
+ baseR2: 'rgba(48,0,255,0.8)',
+ baseR3: 'rgba(85,167,255,0.8)'
+};
+
+const getCurrentMonthDays = () => {
+ const now = dayjs();
+ const daysInMonth = now.daysInMonth();
+ const year = dayjs().year();
+ const month = dayjs().month() + 1;
+
+ const dates = [];
+ for (let day = 1; day <= daysInMonth; day++) {
+ dates.push(dayjs(`${year}-${month}-${day}`).format('YYYY-MM-DD'));
+ }
+ return dates;
+};
+
+const UsageInner: React.FC<{ paddingRight: string }> = ({ paddingRight }) => {
+ const intl = useIntl();
+ const currentDate = dayjs();
+ const currentMonthDays = getCurrentMonthDays();
+ let requestData: {
+ name: string;
+ color: string;
+ areaStyle: any;
+ data: { time: string; value: number }[];
+ }[] = [];
+ let tokenData: { time: string; value: number }[] = [];
+ let userData: { name: string; value: number }[] = [];
+ const xAxisData: string[] = currentMonthDays;
+ let topUserList: string[] = [];
+
+ const { model_usage } = useContext(DashboardContext);
+ const data = model_usage || {};
+
+ const handleSelectDate = (date: any) => {
+ // fetchData?.();
+ };
+
+ const generateData = useCallback(() => {
+ const requestList: {
+ name: string;
+ color: string;
+ areaStyle: any;
+ data: { time: string; value: number }[];
+ } = {
+ name: 'API requests',
+ areaStyle: {},
+ color: baseColorMap.base,
+ data: []
+ };
+
+ const completionData: any = {
+ name: 'Completion tokens',
+ color: baseColorMap.base,
+ data: []
+ };
+ const prompData: any = {
+ name: 'Prompt tokens',
+ color: baseColorMap.baseR3,
+ data: []
+ };
+
+ const topUserPrompt: any = {
+ name: 'Prompt tokens',
+ color: baseColorMap.baseR3,
+ data: []
+ };
+ const topUserCompletion: any = {
+ name: 'Completion tokens',
+ color: baseColorMap.base,
+ data: []
+ };
+
+ _.each(xAxisData, (date: string) => {
+ // tokens data
+ const item = _.find(data.completion_token_history, (item: any) => {
+ return dayjs(item.timestamp * 1000).format('YYYY-MM-DD') === date;
+ });
+ if (!item) {
+ completionData.data.push({
+ titme: date,
+ value: 0
+ });
+ } else {
+ completionData.data.push({
+ value: item.value,
+ time: dayjs(item.timestamp * 1000).format('YYYY-MM-DD')
+ });
+ }
+
+ const promptItem = _.find(data.prompt_token_history, (item: any) => {
+ return dayjs(item.timestamp * 1000).format('YYYY-MM-DD') === date;
+ });
+ if (!promptItem) {
+ prompData.data.push({
+ value: 0,
+ time: date
+ });
+ } else {
+ prompData.data.push({
+ value: promptItem.value,
+ time: dayjs(promptItem.timestamp * 1000).format('YYYY-MM-DD')
+ });
+ }
+
+ // ============== api request data =================
+ const requestItem = _.find(data.api_request_history, (item: any) => {
+ return dayjs(item.timestamp * 1000).format('YYYY-MM-DD') === date;
+ });
+
+ if (!requestItem) {
+ requestList.data.push({
+ time: date,
+ value: 0
+ });
+ } else {
+ requestList.data.push({
+ time: dayjs(requestItem.timestamp * 1000).format('YYYY-MM-DD'),
+ value: requestItem.value
+ });
+ }
+ });
+
+ // ========== top users ============
+ if (!data.top_users?.length) {
+ userData = [];
+ topUserList = [];
+ } else {
+ const users: string[] = [];
+ _.each(data.top_users, (item: any) => {
+ users.push(item.username);
+ topUserPrompt.data.push({
+ name: item.username,
+ value: item.prompt_token_count
+ });
+ topUserCompletion.data.push({
+ name: item.username,
+ value: item.completion_token_count
+ });
+ });
+ topUserList = _.uniq(users);
+ userData = [topUserCompletion, topUserPrompt];
+ }
+
+ requestData = [requestList];
+ tokenData = [completionData, prompData];
+ }, [data, xAxisData]);
+
+ generateData();
+
+ return (
+ <>
+
+
+ >
+ );
+};
+
+export default memo(UsageInner);
diff --git a/src/pages/usage/components/usage-inner/request-token-inner.tsx b/src/pages/usage/components/usage-inner/request-token-inner.tsx
new file mode 100644
index 00000000..3e4dc141
--- /dev/null
+++ b/src/pages/usage/components/usage-inner/request-token-inner.tsx
@@ -0,0 +1,63 @@
+import CardWrapper from '@/components/card-wrapper';
+import BarChart from '@/components/echarts/bar-chart';
+import LineChart from '@/components/echarts/line-chart';
+import { useIntl } from '@umijs/max';
+import { Col, Row } from 'antd';
+import dayjs from 'dayjs';
+import React from 'react';
+
+interface RequestTokenInnerProps {
+ requestData: {
+ name: string;
+ color: string;
+ areaStyle: any;
+ data: { time: string; value: number }[];
+ }[];
+ tokenData: { time: string; value: number }[];
+ xAxisData: string[];
+}
+const RequestTokenInner: React.FC<
+ RequestTokenInnerProps & { paddingRight: string }
+> = (props) => {
+ console.log('request token inner=====================');
+ const { requestData, tokenData, xAxisData, paddingRight } = props;
+ const intl = useIntl();
+ const labelFormatter = (v: any) => {
+ return dayjs(v).format('MM-DD');
+ };
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default React.memo(RequestTokenInner);
diff --git a/src/pages/usage/components/usage-inner/top-user.tsx b/src/pages/usage/components/usage-inner/top-user.tsx
new file mode 100644
index 00000000..c82d79f9
--- /dev/null
+++ b/src/pages/usage/components/usage-inner/top-user.tsx
@@ -0,0 +1,27 @@
+import CardWrapper from '@/components/card-wrapper';
+import HBar from '@/components/echarts/h-bar';
+import { useIntl } from '@umijs/max';
+import React from 'react';
+
+interface TopUserProps {
+ userData: { name: string; value: number }[];
+ topUserList: string[];
+}
+const TopUser: React.FC = (props) => {
+ console.log('TopUser=====================');
+ const { userData, topUserList } = props;
+ const intl = useIntl();
+
+ return (
+
+
+
+ );
+};
+
+export default React.memo(TopUser);
diff --git a/src/pages/usage/components/usage.tsx b/src/pages/usage/components/usage.tsx
new file mode 100644
index 00000000..a5e677a5
--- /dev/null
+++ b/src/pages/usage/components/usage.tsx
@@ -0,0 +1,23 @@
+import breakpoints from '@/config/breakpoints';
+import useWindowResize from '@/hooks/use-window-resize';
+import { useEffect, useState } from 'react';
+import UserInner from './usage-inner';
+
+const Usage = () => {
+ const {
+ size: { width }
+ } = useWindowResize();
+ const [paddingRight, setPaddingRight] = useState('20px');
+
+ useEffect(() => {
+ if (width < breakpoints.xl) {
+ setPaddingRight('0');
+ } else {
+ setPaddingRight('20px');
+ }
+ }, [width]);
+
+ return ;
+};
+
+export default Usage;
diff --git a/src/pages/usage/config/dashboard-context.ts b/src/pages/usage/config/dashboard-context.ts
new file mode 100644
index 00000000..fffb7358
--- /dev/null
+++ b/src/pages/usage/config/dashboard-context.ts
@@ -0,0 +1,8 @@
+import { createContext } from 'react';
+import { DashboardProps } from './types';
+
+export const DashboardContext = createContext<
+ DashboardProps & { fetchData: () => Promise }
+>({} as DashboardProps & { fetchData: () => Promise });
+
+export default DashboardContext;
diff --git a/src/pages/usage/config/index.ts b/src/pages/usage/config/index.ts
new file mode 100644
index 00000000..fe25e99c
--- /dev/null
+++ b/src/pages/usage/config/index.ts
@@ -0,0 +1,23 @@
+export const overviewConfigs = [
+ {
+ key: 'worker_count',
+ label: 'dashboard.workers',
+ backgroundColor: 'var(--color-white-1)'
+ },
+ {
+ key: 'gpu_count',
+ label: 'dashboard.totalgpus',
+ backgroundColor: 'var(--color-white-1)'
+ },
+
+ {
+ key: 'model_count',
+ label: 'dashboard.models',
+ backgroundColor: 'var(--color-white-1)'
+ },
+ {
+ key: 'model_instance_count',
+ label: 'models.form.replicas',
+ backgroundColor: 'var(--color-white-1)'
+ }
+];
diff --git a/src/pages/usage/config/types.ts b/src/pages/usage/config/types.ts
new file mode 100644
index 00000000..fc83bc84
--- /dev/null
+++ b/src/pages/usage/config/types.ts
@@ -0,0 +1,46 @@
+export interface DashboardProps {
+ resource_counts: {
+ worker_count: number;
+ gpu_count: number;
+ model_count: number;
+ model_instance_count: number;
+ };
+ system_load: {
+ current: {
+ cpu: number;
+ memory: number;
+ gpu: number;
+ gpu_memory: number;
+ };
+ history: {
+ cpu: {
+ timestamp: number;
+ value: number;
+ }[];
+ memory: {
+ timestamp: number;
+ value: number;
+ }[];
+ gpu: {
+ timestamp: number;
+ value: number;
+ }[];
+ gpu_memory: {
+ timestamp: number;
+ value: number;
+ }[];
+ };
+ };
+ model_usage: {
+ api_request_history: any[];
+ completion_token_history: any[];
+ prompt_token_history: any[];
+ top_users: {
+ user_id: number;
+ username: string;
+ prompt_token_count: number;
+ completion_token_count: number;
+ }[];
+ };
+ active_models: any[];
+}
diff --git a/src/pages/usage/index.tsx b/src/pages/usage/index.tsx
new file mode 100644
index 00000000..99d9719c
--- /dev/null
+++ b/src/pages/usage/index.tsx
@@ -0,0 +1,20 @@
+import { PageContainer } from '@ant-design/pro-components';
+import { Spin } from 'antd';
+import { memo, useState } from 'react';
+import DashboardInner from './components/dahboard-inner';
+
+const Dashboard: React.FC = () => {
+ const [loading, setLoading] = useState(false);
+
+ return (
+ <>
+
+
+
+
+
+ >
+ );
+};
+
+export default memo(Dashboard);
diff --git a/src/pages/usage/styles/index.less b/src/pages/usage/styles/index.less
new file mode 100644
index 00000000..3ac537c8
--- /dev/null
+++ b/src/pages/usage/styles/index.less
@@ -0,0 +1,17 @@
+.value-box {
+ display: flex;
+ justify-content: space-around;
+ align-items: center;
+
+ .value-healthy {
+ color: var(--ant-green-6);
+ }
+
+ .value-warning {
+ color: var(--ant-gold-6);
+ }
+
+ .value-error {
+ color: var(--ant-red-6);
+ }
+}
diff --git a/tsconfig.json b/tsconfig.json
index 03da2f34..299e70a3 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -21,4 +21,3 @@
},
"include": ["./**/*.d.ts", "./**/*.ts", "./**/*.tsx"]
}
-