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}
{children}
} - {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 })} > - {/* */}
{ 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 ( + +
+
{label}
+
{value}
+
+
+ ); +}; +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"] } -