chore: watch api
This commit is contained in:
@@ -98,6 +98,7 @@ export default defineConfig({
|
|||||||
hash: true,
|
hash: true,
|
||||||
access: {},
|
access: {},
|
||||||
model: {},
|
model: {},
|
||||||
|
valtio: {},
|
||||||
initialState: {},
|
initialState: {},
|
||||||
request: {},
|
request: {},
|
||||||
locale: {
|
locale: {
|
||||||
|
|||||||
+4
-1
@@ -18,12 +18,15 @@
|
|||||||
"@monaco-editor/react": "^4.6.0",
|
"@monaco-editor/react": "^4.6.0",
|
||||||
"@types/lodash": "^4.17.4",
|
"@types/lodash": "^4.17.4",
|
||||||
"@umijs/max": "^4.2.1",
|
"@umijs/max": "^4.2.1",
|
||||||
"antd": "^5.17.0",
|
"ansi-to-html": "^0.7.2",
|
||||||
|
"antd": "^5.18.3",
|
||||||
"antd-style": "^3.6.2",
|
"antd-style": "^3.6.2",
|
||||||
"axios": "^1.7.2",
|
"axios": "^1.7.2",
|
||||||
"classnames": "^2.5.1",
|
"classnames": "^2.5.1",
|
||||||
"crypto-js": "^4.2.0",
|
"crypto-js": "^4.2.0",
|
||||||
"dayjs": "^1.11.11",
|
"dayjs": "^1.11.11",
|
||||||
|
"has-ansi": "^5.0.1",
|
||||||
|
"jotai": "^2.8.4",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"numeral": "^2.0.6",
|
"numeral": "^2.0.6",
|
||||||
"query-string": "^9.0.0",
|
"query-string": "^9.0.0",
|
||||||
|
|||||||
Generated
+375
-271
File diff suppressed because it is too large
Load Diff
+6
-20
@@ -9,11 +9,14 @@ let currentUserInfo: any = {};
|
|||||||
|
|
||||||
// 全局初始化数据配置,用于 Layout 用户信息和权限初始化
|
// 全局初始化数据配置,用于 Layout 用户信息和权限初始化
|
||||||
// 更多信息见文档:https://umijs.org/docs/api/runtime-config#getinitialstate
|
// 更多信息见文档:https://umijs.org/docs/api/runtime-config#getinitialstate
|
||||||
export async function getInitialState() {
|
export async function getInitialState(): Promise<{
|
||||||
|
fetchUserInfo: () => Promise<Global.UserInfo>;
|
||||||
|
currentUser?: Global.UserInfo;
|
||||||
|
}> {
|
||||||
// 如果不是登录页面,执行
|
// 如果不是登录页面,执行
|
||||||
const { location } = history;
|
const { location } = history;
|
||||||
|
|
||||||
const fetchUserInfo = async () => {
|
const fetchUserInfo = async (): Promise<Global.UserInfo> => {
|
||||||
try {
|
try {
|
||||||
const data = await queryCurrentUserState({
|
const data = await queryCurrentUserState({
|
||||||
skipErrorHandler: true
|
skipErrorHandler: true
|
||||||
@@ -22,7 +25,7 @@ export async function getInitialState() {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
history.push(loginPath);
|
history.push(loginPath);
|
||||||
}
|
}
|
||||||
return undefined;
|
return {} as Global.UserInfo;
|
||||||
};
|
};
|
||||||
|
|
||||||
if (![loginPath].includes(location.pathname)) {
|
if (![loginPath].includes(location.pathname)) {
|
||||||
@@ -40,23 +43,6 @@ export async function getInitialState() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// export const patchClientRoutes = async (params: { routes: any[] }) => {
|
|
||||||
// const { routes } = params;
|
|
||||||
// console.log('routes============999', routes);
|
|
||||||
// const data = await queryCurrentUserState({
|
|
||||||
// skipErrorHandler: true
|
|
||||||
// });
|
|
||||||
|
|
||||||
// routes.unshift({
|
|
||||||
// path: '/',
|
|
||||||
// element: data?.is_admin ? (
|
|
||||||
// <Navigate to="/dashboard" replace />
|
|
||||||
// ) : (
|
|
||||||
// <Navigate to="/playground" replace />
|
|
||||||
// )
|
|
||||||
// });
|
|
||||||
// };
|
|
||||||
|
|
||||||
export const request: RequestConfig = {
|
export const request: RequestConfig = {
|
||||||
baseURL: ' /v1',
|
baseURL: ' /v1',
|
||||||
...requestConfig
|
...requestConfig
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 10 KiB |
@@ -14,6 +14,18 @@
|
|||||||
margin-left: 8px;
|
margin-left: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.m-r-10 {
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.m-r-5 {
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.m-r-8 {
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
.flex {
|
.flex {
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
@@ -35,3 +47,8 @@
|
|||||||
.opct-8 {
|
.opct-8 {
|
||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.flex-column {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
.logs-viewer-wrap-w2 {
|
||||||
|
.wrap {
|
||||||
|
padding: 5px 0 5px 10px;
|
||||||
|
overflow: auto;
|
||||||
|
background-color: var(--color-logs-bg);
|
||||||
|
border-radius: var(--border-radius-small);
|
||||||
|
|
||||||
|
.content {
|
||||||
|
word-wrap: break-word;
|
||||||
|
|
||||||
|
&.line-break {
|
||||||
|
word-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
color: var(--color-logs-text);
|
||||||
|
font-size: var(--font-size-small);
|
||||||
|
line-height: 22px;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
background-color: var(--color-logs-bg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
import useSetChunkRequest from '@/hooks/use-chunk-request';
|
||||||
|
import Convert from 'ansi-to-html';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import hasAnsi from 'has-ansi';
|
||||||
|
import { useEffect, useRef, useState } from 'react';
|
||||||
|
import './index.less';
|
||||||
|
|
||||||
|
interface LogsViewerProps {
|
||||||
|
height: number;
|
||||||
|
content: string;
|
||||||
|
url: string;
|
||||||
|
params?: object;
|
||||||
|
}
|
||||||
|
const LogsViewer: React.FC<LogsViewerProps> = (props) => {
|
||||||
|
const { height, content, url } = props;
|
||||||
|
const [nowrap, setNowrap] = useState(false);
|
||||||
|
const [logsContent, setLogsContent] = useState(content);
|
||||||
|
const { setChunkRequest } = useSetChunkRequest();
|
||||||
|
const chunkRequedtRef = useRef<any>(null);
|
||||||
|
|
||||||
|
const convert = new Convert();
|
||||||
|
|
||||||
|
const updateContent = (newVal: string) => {
|
||||||
|
if (hasAnsi(newVal)) {
|
||||||
|
const htmlStr = `${convert.toHtml(newVal)}`;
|
||||||
|
setLogsContent(htmlStr);
|
||||||
|
} else {
|
||||||
|
setLogsContent(newVal);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const createChunkConnection = async () => {
|
||||||
|
chunkRequedtRef.current?.current?.cancel?.();
|
||||||
|
chunkRequedtRef.current = setChunkRequest({
|
||||||
|
url,
|
||||||
|
params: {
|
||||||
|
...props.params,
|
||||||
|
watch: true
|
||||||
|
},
|
||||||
|
contentType: 'text',
|
||||||
|
handler: updateContent
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
createChunkConnection();
|
||||||
|
return () => {
|
||||||
|
chunkRequedtRef.current?.current?.cancel?.();
|
||||||
|
};
|
||||||
|
}, [url, props.params]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="logs-viewer-wrap-w2">
|
||||||
|
<div className="wrap" style={{ height: height }}>
|
||||||
|
<div className={classNames('content', { 'line-break': nowrap })}>
|
||||||
|
<div className="text">{logsContent}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LogsViewer;
|
||||||
@@ -1,9 +1,14 @@
|
|||||||
import { Progress } from 'antd';
|
import { Progress, Tooltip } from 'antd';
|
||||||
import { memo, useMemo } from 'react';
|
import { memo, useMemo } from 'react';
|
||||||
|
|
||||||
const RenderProgress = memo(
|
const RenderProgress = memo(
|
||||||
(props: { percent: number; steps?: number; download?: boolean }) => {
|
(props: {
|
||||||
const { percent, steps = 5, download } = props;
|
percent: number;
|
||||||
|
steps?: number;
|
||||||
|
download?: boolean;
|
||||||
|
label?: React.ReactNode;
|
||||||
|
}) => {
|
||||||
|
const { percent, steps = 5, download, label } = props;
|
||||||
|
|
||||||
const strokeColor = useMemo(() => {
|
const strokeColor = useMemo(() => {
|
||||||
if (download) {
|
if (download) {
|
||||||
@@ -19,16 +24,48 @@ const RenderProgress = memo(
|
|||||||
}, [percent]);
|
}, [percent]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Progress
|
<>
|
||||||
steps={steps}
|
{label ? (
|
||||||
format={() => {
|
<Tooltip title={label}>
|
||||||
return (
|
<Progress
|
||||||
<span style={{ color: 'var(--ant-color-text)' }}>{percent}%</span>
|
percentPosition={{ align: 'center', type: 'inner' }}
|
||||||
);
|
size={[undefined, 12]}
|
||||||
}}
|
format={() => {
|
||||||
percent={percent}
|
return (
|
||||||
strokeColor={strokeColor}
|
<span
|
||||||
/>
|
style={{
|
||||||
|
color: 'var(--ant-color-text)'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{percent}%
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
percent={percent}
|
||||||
|
strokeColor={strokeColor}
|
||||||
|
></Progress>
|
||||||
|
</Tooltip>
|
||||||
|
) : (
|
||||||
|
<Progress
|
||||||
|
type="line"
|
||||||
|
percentPosition={{ align: 'center', type: 'inner' }}
|
||||||
|
size={[undefined, 12]}
|
||||||
|
format={() => {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
color: 'var(--ant-color-text)'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{percent}%
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
percent={percent}
|
||||||
|
strokeColor={strokeColor}
|
||||||
|
></Progress>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
@wrapheight: 54px;
|
@wrapheight: 54px;
|
||||||
@inputheight: 32px;
|
@inputheight: 32px;
|
||||||
@borderRadius: 8px;
|
@borderRadius: 8px;
|
||||||
|
@input-inner-padding: 12px;
|
||||||
|
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -115,7 +116,7 @@
|
|||||||
:global(.ant-select-selector) {
|
:global(.ant-select-selector) {
|
||||||
border: none !important;
|
border: none !important;
|
||||||
padding-block: 5px;
|
padding-block: 5px;
|
||||||
padding-inline: 12px;
|
padding-inline: @input-inner-padding !important;
|
||||||
box-shadow: none !important;
|
box-shadow: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -167,7 +168,7 @@
|
|||||||
border: none;
|
border: none;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
padding-block: 5px;
|
padding-block: 5px;
|
||||||
padding-inline: 12px;
|
padding-inline: @input-inner-padding;
|
||||||
height: @inputheight !important;
|
height: @inputheight !important;
|
||||||
|
|
||||||
&.seal-textarea {
|
&.seal-textarea {
|
||||||
@@ -182,7 +183,7 @@
|
|||||||
:global(input.ant-input-number-input) {
|
:global(input.ant-input-number-input) {
|
||||||
height: @inputheight !important;
|
height: @inputheight !important;
|
||||||
padding-block: 5px;
|
padding-block: 5px;
|
||||||
padding-inline: 12px;
|
padding-inline: @input-inner-padding;
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(.ant-input-group) {
|
:global(.ant-input-group) {
|
||||||
|
|||||||
@@ -23,6 +23,7 @@
|
|||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
border-radius: var(--ant-table-header-border-radius);
|
border-radius: var(--ant-table-header-border-radius);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
box-shadow: var(--box-shadow-base);
|
||||||
}
|
}
|
||||||
|
|
||||||
.expanded-row {
|
.expanded-row {
|
||||||
|
|||||||
Vendored
+1
@@ -18,6 +18,7 @@ declare namespace Global {
|
|||||||
username: string;
|
username: string;
|
||||||
is_admin: boolean;
|
is_admin: boolean;
|
||||||
full_name: string;
|
full_name: string;
|
||||||
|
require_password_change: boolean;
|
||||||
id: number;
|
id: number;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ html {
|
|||||||
// --color-fill-1: #fff;
|
// --color-fill-1: #fff;
|
||||||
--color-fill-2: #fff;
|
--color-fill-2: #fff;
|
||||||
--color-fill-3: #f3f6fa;
|
--color-fill-3: #f3f6fa;
|
||||||
|
--color-logs-bg: #1e1e1e;
|
||||||
|
--color-logs-text: #d4d4d4;
|
||||||
--menu-border-radius-base: 8px;
|
--menu-border-radius-base: 8px;
|
||||||
--border-radius-base: 16px;
|
--border-radius-base: 16px;
|
||||||
--border-radius-middle: 20px;
|
--border-radius-middle: 20px;
|
||||||
|
|||||||
@@ -199,7 +199,7 @@ const useSetChunkRequest = () => {
|
|||||||
retryCount.current = totalCount;
|
retryCount.current = totalCount;
|
||||||
clearTimeout(timer.current);
|
clearTimeout(timer.current);
|
||||||
axiosChunkRequest(requestConfig.current);
|
axiosChunkRequest(requestConfig.current);
|
||||||
return axiosToken.current;
|
return axiosToken;
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -87,7 +87,6 @@ export default (props: any) => {
|
|||||||
loading: false,
|
loading: false,
|
||||||
setInitialState: null
|
setInitialState: null
|
||||||
};
|
};
|
||||||
console.log('initialInfo==========', initialInfo);
|
|
||||||
const { initialState, loading, setInitialState } = initialInfo;
|
const { initialState, loading, setInitialState } = initialInfo;
|
||||||
|
|
||||||
const userConfig = {
|
const userConfig = {
|
||||||
@@ -97,7 +96,6 @@ export default (props: any) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const formatMessage = (args) => {
|
const formatMessage = (args) => {
|
||||||
console.log('formatMessage', args);
|
|
||||||
return intl.formatMessage({ id: args.id });
|
return intl.formatMessage({ id: args.id });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -11,5 +11,6 @@ export default {
|
|||||||
'models.form.replicas': 'Replicas',
|
'models.form.replicas': 'Replicas',
|
||||||
'models.form.s3address': 'S3 Address',
|
'models.form.s3address': 'S3 Address',
|
||||||
'models.openinplayground': 'Open in Playground',
|
'models.openinplayground': 'Open in Playground',
|
||||||
'models.instances': 'instances'
|
'models.instances': 'instances',
|
||||||
|
'model.form.ollama.model': 'Ollama Model'
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -11,5 +11,6 @@ export default {
|
|||||||
'models.form.replicas': '副本数',
|
'models.form.replicas': '副本数',
|
||||||
'models.form.s3address': 'S3 地址',
|
'models.form.s3address': 'S3 地址',
|
||||||
'models.openinplayground': '在 Playground 中打开',
|
'models.openinplayground': '在 Playground 中打开',
|
||||||
'models.instances': '实例'
|
'models.instances': '实例',
|
||||||
|
'model.form.ollama.model': 'Ollama 模型'
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ const modelColumns = [
|
|||||||
dataIndex: 'gpu_utilization',
|
dataIndex: 'gpu_utilization',
|
||||||
key: 'gpu_utilization',
|
key: 'gpu_utilization',
|
||||||
render: (text: any, record: any) => (
|
render: (text: any, record: any) => (
|
||||||
<ProgressBar percent={_.round(text, 2)}></ProgressBar>
|
<ProgressBar percent={_.round(text, 0)}></ProgressBar>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -24,7 +24,7 @@ const modelColumns = [
|
|||||||
dataIndex: 'gpu_memory_utilization',
|
dataIndex: 'gpu_memory_utilization',
|
||||||
key: 'gpu_memory_utilization',
|
key: 'gpu_memory_utilization',
|
||||||
render: (text: any, record: any) => (
|
render: (text: any, record: any) => (
|
||||||
<ProgressBar percent={_.round(text, 2)}></ProgressBar>
|
<ProgressBar percent={_.round(text, 0)}></ProgressBar>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -223,7 +223,7 @@ const AddModal: React.FC<AddModalProps> = (props) => {
|
|||||||
>
|
>
|
||||||
<SealAutoComplete
|
<SealAutoComplete
|
||||||
filterOption
|
filterOption
|
||||||
label="Ollama Model"
|
label={intl.formatMessage({ id: 'model.form.ollama.model' })}
|
||||||
required
|
required
|
||||||
options={ollamaModelOptions}
|
options={ollamaModelOptions}
|
||||||
></SealAutoComplete>
|
></SealAutoComplete>
|
||||||
|
|||||||
@@ -1,15 +1,17 @@
|
|||||||
|
import LogsViewer from '@/components/logs-viewer';
|
||||||
import { Modal } from 'antd';
|
import { Modal } from 'antd';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
type ViewModalProps = {
|
type ViewModalProps = {
|
||||||
content?: string;
|
content: string;
|
||||||
title: string;
|
title: string;
|
||||||
open: boolean;
|
open: boolean;
|
||||||
|
url: string;
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ViewCodeModal: React.FC<ViewModalProps> = (props) => {
|
const ViewCodeModal: React.FC<ViewModalProps> = (props) => {
|
||||||
const { title, open, onCancel, content } = props || {};
|
const { title, open, url, onCancel, content = '' } = props || {};
|
||||||
if (!open) {
|
if (!open) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -27,7 +29,14 @@ const ViewCodeModal: React.FC<ViewModalProps> = (props) => {
|
|||||||
style={{ top: '80px' }}
|
style={{ top: '80px' }}
|
||||||
footer={null}
|
footer={null}
|
||||||
>
|
>
|
||||||
<div>{content}</div>
|
<LogsViewer
|
||||||
|
content={content}
|
||||||
|
height={400}
|
||||||
|
url={url}
|
||||||
|
params={{
|
||||||
|
follow: false
|
||||||
|
}}
|
||||||
|
></LogsViewer>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ export interface ModelInstanceListItem {
|
|||||||
s3_address: string;
|
s3_address: string;
|
||||||
worker_id: number;
|
worker_id: number;
|
||||||
worker_ip: string;
|
worker_ip: string;
|
||||||
|
gpu_index: number;
|
||||||
pid: number;
|
pid: number;
|
||||||
port: number;
|
port: number;
|
||||||
state: string;
|
state: string;
|
||||||
|
|||||||
@@ -23,27 +23,26 @@ import {
|
|||||||
import { PageContainer } from '@ant-design/pro-components';
|
import { PageContainer } from '@ant-design/pro-components';
|
||||||
import { Access, useAccess, useIntl, useNavigate } from '@umijs/max';
|
import { Access, useAccess, useIntl, useNavigate } from '@umijs/max';
|
||||||
import {
|
import {
|
||||||
App,
|
|
||||||
Button,
|
Button,
|
||||||
Col,
|
Col,
|
||||||
Input,
|
Input,
|
||||||
Modal,
|
Modal,
|
||||||
Progress,
|
|
||||||
Row,
|
Row,
|
||||||
Space,
|
Space,
|
||||||
|
Tag,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
message
|
message
|
||||||
} from 'antd';
|
} from 'antd';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { useEffect, useRef, useState } from 'react';
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
MODELS_API,
|
MODELS_API,
|
||||||
|
MODEL_INSTANCE_API,
|
||||||
createModel,
|
createModel,
|
||||||
createModelInstance,
|
createModelInstance,
|
||||||
deleteModel,
|
deleteModel,
|
||||||
deleteModelInstance,
|
deleteModelInstance,
|
||||||
queryModelInstanceLogs,
|
|
||||||
queryModelInstancesList,
|
queryModelInstancesList,
|
||||||
queryModelsList,
|
queryModelsList,
|
||||||
updateModel
|
updateModel
|
||||||
@@ -54,7 +53,7 @@ import { status } from './config';
|
|||||||
import { FormData, ListItem, ModelInstanceListItem } from './config/types';
|
import { FormData, ListItem, ModelInstanceListItem } from './config/types';
|
||||||
|
|
||||||
const Models: React.FC = () => {
|
const Models: React.FC = () => {
|
||||||
const { modal } = App.useApp();
|
// const { modal } = App.useApp();
|
||||||
const access = useAccess();
|
const access = useAccess();
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -75,6 +74,9 @@ const Models: React.FC = () => {
|
|||||||
const [currentData, setCurrentData] = useState<ListItem | undefined>(
|
const [currentData, setCurrentData] = useState<ListItem | undefined>(
|
||||||
undefined
|
undefined
|
||||||
);
|
);
|
||||||
|
const [currentInstanceUrl, setCurrentInstanceUrl] = useState<string>('');
|
||||||
|
|
||||||
|
const chunkRequedtRef = useRef<any>();
|
||||||
const timer = useRef<any>();
|
const timer = useRef<any>();
|
||||||
let axiosToken = createAxiosToken();
|
let axiosToken = createAxiosToken();
|
||||||
const [queryParams, setQueryParams] = useState({
|
const [queryParams, setQueryParams] = useState({
|
||||||
@@ -152,8 +154,9 @@ const Models: React.FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const createModelsChunkRequest = () => {
|
const createModelsChunkRequest = () => {
|
||||||
|
chunkRequedtRef.current?.current?.cancel?.();
|
||||||
try {
|
try {
|
||||||
setChunkRequest({
|
chunkRequedtRef.current = setChunkRequest({
|
||||||
url: MODELS_API,
|
url: MODELS_API,
|
||||||
params: {
|
params: {
|
||||||
..._.pickBy(
|
..._.pickBy(
|
||||||
@@ -242,7 +245,6 @@ const Models: React.FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleOpenPlayGround = (row: any) => {
|
const handleOpenPlayGround = (row: any) => {
|
||||||
console.log('handleOpenPlayGround', row);
|
|
||||||
navigate(`/playground?model=${row.name}`);
|
navigate(`/playground?model=${row.name}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -260,12 +262,34 @@ const Models: React.FC = () => {
|
|||||||
} catch (error) {}
|
} catch (error) {}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleStreamData = (data: any) => {
|
||||||
|
setLogContent(data);
|
||||||
|
};
|
||||||
|
|
||||||
const handleViewLogs = async (row: any) => {
|
const handleViewLogs = async (row: any) => {
|
||||||
try {
|
try {
|
||||||
const data = await queryModelInstanceLogs(row.id);
|
// const result = await fetchChunkedData({
|
||||||
setLogContent(data);
|
// url: `/v1${MODEL_INSTANCE_API}/${row.id}/logs`,
|
||||||
|
// params: {
|
||||||
|
// follow: false
|
||||||
|
// },
|
||||||
|
// method: 'GET'
|
||||||
|
// });
|
||||||
|
// if (!result) {
|
||||||
|
// setLogContent('');
|
||||||
|
// } else {
|
||||||
|
// const { reader, decoder } = result;
|
||||||
|
// await readStreamData(reader, decoder, (chunk: any) => {
|
||||||
|
// handleStreamData(chunk);
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
|
||||||
|
setCurrentInstanceUrl(`${MODEL_INSTANCE_API}/${row.id}/logs`);
|
||||||
|
|
||||||
setOpenLogModal(true);
|
setOpenLogModal(true);
|
||||||
} catch (error) {}
|
} catch (error) {
|
||||||
|
console.log('error:', error);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
const handleDeleteInstace = (row: any) => {
|
const handleDeleteInstace = (row: any) => {
|
||||||
Modal.confirm({
|
Modal.confirm({
|
||||||
@@ -293,7 +317,7 @@ const Models: React.FC = () => {
|
|||||||
setHoverChildIndex(-1);
|
setHoverChildIndex(-1);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getModelInstances = async (row: any) => {
|
const getModelInstances = useCallback(async (row: any) => {
|
||||||
const params = {
|
const params = {
|
||||||
id: row.id,
|
id: row.id,
|
||||||
page: 1,
|
page: 1,
|
||||||
@@ -301,7 +325,7 @@ const Models: React.FC = () => {
|
|||||||
};
|
};
|
||||||
const data = await queryModelInstancesList(params);
|
const data = await queryModelInstancesList(params);
|
||||||
return data.items || [];
|
return data.items || [];
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
const handleEdit = (row: ListItem) => {
|
const handleEdit = (row: ListItem) => {
|
||||||
setCurrentData(row);
|
setCurrentData(row);
|
||||||
@@ -315,11 +339,11 @@ const Models: React.FC = () => {
|
|||||||
}, [queryParams]);
|
}, [queryParams]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchDataByPolling();
|
createModelsChunkRequest();
|
||||||
return () => {
|
return () => {
|
||||||
clearInterval(timer.current);
|
chunkRequedtRef.current?.current?.cancel?.();
|
||||||
};
|
};
|
||||||
}, []);
|
}, [queryParams]);
|
||||||
|
|
||||||
const renderChildren = (list: any) => {
|
const renderChildren = (list: any) => {
|
||||||
return (
|
return (
|
||||||
@@ -337,20 +361,19 @@ const Models: React.FC = () => {
|
|||||||
>
|
>
|
||||||
<RowChildren>
|
<RowChildren>
|
||||||
<Row style={{ width: '100%' }} align="middle">
|
<Row style={{ width: '100%' }} align="middle">
|
||||||
<Col span={4}>
|
<Col span={6}>
|
||||||
|
<Tag>{item.gpu_index}</Tag>
|
||||||
{item.worker_ip}:{item.port}
|
{item.worker_ip}:{item.port}
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={5}>
|
<Col span={4}>
|
||||||
<span>{item.huggingface_filename}</span>
|
<span>{item.huggingface_filename}</span>
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={4}>
|
|
||||||
{dayjs(item.updated_at).format('YYYY-MM-DD HH:mm:ss')}
|
|
||||||
</Col>
|
|
||||||
<Col span={4}>
|
<Col span={4}>
|
||||||
{item.state && (
|
{item.state && (
|
||||||
<StatusTag
|
<StatusTag
|
||||||
download={
|
download={
|
||||||
item.download_progress !== 100
|
item.state !== 'Running'
|
||||||
? { percent: item.download_progress }
|
? { percent: item.download_progress }
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
@@ -361,7 +384,10 @@ const Models: React.FC = () => {
|
|||||||
></StatusTag>
|
></StatusTag>
|
||||||
)}
|
)}
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={7}>
|
<Col span={5}>
|
||||||
|
{dayjs(item.updated_at).format('YYYY-MM-DD HH:mm:ss')}
|
||||||
|
</Col>
|
||||||
|
<Col span={5}>
|
||||||
{hoverChildIndex === `${item.id}-${index}` && (
|
{hoverChildIndex === `${item.id}-${index}` && (
|
||||||
<Space size={20}>
|
<Space size={20}>
|
||||||
<Tooltip
|
<Tooltip
|
||||||
@@ -415,6 +441,7 @@ const Models: React.FC = () => {
|
|||||||
<Input
|
<Input
|
||||||
placeholder={intl.formatMessage({ id: 'common.filter.name' })}
|
placeholder={intl.formatMessage({ id: 'common.filter.name' })}
|
||||||
style={{ width: 300 }}
|
style={{ width: 300 }}
|
||||||
|
size="large"
|
||||||
allowClear
|
allowClear
|
||||||
onChange={handleNameChange}
|
onChange={handleNameChange}
|
||||||
></Input>
|
></Input>
|
||||||
@@ -455,7 +482,7 @@ const Models: React.FC = () => {
|
|||||||
rowKey="id"
|
rowKey="id"
|
||||||
expandable={true}
|
expandable={true}
|
||||||
onChange={handleTableChange}
|
onChange={handleTableChange}
|
||||||
pollingChildren={true}
|
pollingChildren={false}
|
||||||
loadChildren={getModelInstances}
|
loadChildren={getModelInstances}
|
||||||
renderChildren={renderChildren}
|
renderChildren={renderChildren}
|
||||||
pagination={{
|
pagination={{
|
||||||
@@ -470,26 +497,32 @@ const Models: React.FC = () => {
|
|||||||
>
|
>
|
||||||
<SealColumn
|
<SealColumn
|
||||||
title={intl.formatMessage({ id: 'models.table.name' })}
|
title={intl.formatMessage({ id: 'models.table.name' })}
|
||||||
dataIndex="name"
|
dataIndex="huggingface_repo_id"
|
||||||
key="name"
|
key="huggingface_repo_id"
|
||||||
width={400}
|
width={400}
|
||||||
span={8}
|
span={6}
|
||||||
render={(text, record) => {
|
render={(text, record) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Tooltip>{text}</Tooltip>
|
<Tooltip>{text}</Tooltip>
|
||||||
{record.progress && (
|
|
||||||
<Progress
|
|
||||||
percent={record.progress}
|
|
||||||
strokeColor="var(--ant-color-primary)"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<SealColumn
|
<SealColumn
|
||||||
span={8}
|
title={intl.formatMessage({ id: 'models.form.source' })}
|
||||||
|
dataIndex="source"
|
||||||
|
key="source"
|
||||||
|
span={4}
|
||||||
|
/>
|
||||||
|
<SealColumn
|
||||||
|
title={intl.formatMessage({ id: 'models.form.replicas' })}
|
||||||
|
dataIndex="replicas"
|
||||||
|
key="replicas"
|
||||||
|
span={4}
|
||||||
|
/>
|
||||||
|
<SealColumn
|
||||||
|
span={5}
|
||||||
title={intl.formatMessage({ id: 'common.table.createTime' })}
|
title={intl.formatMessage({ id: 'common.table.createTime' })}
|
||||||
dataIndex="created_at"
|
dataIndex="created_at"
|
||||||
key="createTime"
|
key="createTime"
|
||||||
@@ -502,7 +535,7 @@ const Models: React.FC = () => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<SealColumn
|
<SealColumn
|
||||||
span={8}
|
span={5}
|
||||||
title={intl.formatMessage({ id: 'common.table.operation' })}
|
title={intl.formatMessage({ id: 'common.table.operation' })}
|
||||||
key="operation"
|
key="operation"
|
||||||
render={(text, record) => {
|
render={(text, record) => {
|
||||||
@@ -558,8 +591,10 @@ const Models: React.FC = () => {
|
|||||||
onOk={handleModalOk}
|
onOk={handleModalOk}
|
||||||
></AddModal>
|
></AddModal>
|
||||||
<ViewLogsModal
|
<ViewLogsModal
|
||||||
|
url={currentInstanceUrl}
|
||||||
title={intl.formatMessage({ id: 'common.button.viewlog' })}
|
title={intl.formatMessage({ id: 'common.button.viewlog' })}
|
||||||
open={openLogModal}
|
open={openLogModal}
|
||||||
|
content={logContent}
|
||||||
onCancel={handleLogModalCancel}
|
onCancel={handleLogModalCancel}
|
||||||
></ViewLogsModal>
|
></ViewLogsModal>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -0,0 +1,142 @@
|
|||||||
|
import LogoIcon from '@/assets/images/logo.png';
|
||||||
|
import SealInput from '@/components/seal-form/seal-input';
|
||||||
|
import { GlobalOutlined, LockOutlined, UserOutlined } from '@ant-design/icons';
|
||||||
|
import { SelectLang, history, useIntl, useModel } from '@umijs/max';
|
||||||
|
import { Button, Checkbox, Form } from 'antd';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
import { flushSync } from 'react-dom';
|
||||||
|
import { login } from '../apis';
|
||||||
|
|
||||||
|
const renderLogo = () => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: '400px',
|
||||||
|
display: 'flex',
|
||||||
|
marginBottom: 24,
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img src={LogoIcon} alt="logo" style={{ width: '44px' }} />
|
||||||
|
<span style={{ fontSize: 34, marginLeft: 12 }}>SEAL</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
const LoginForm: React.FC<{
|
||||||
|
setCurrentUser: (userInfo: any) => void;
|
||||||
|
}> = ({ setCurrentUser }) => {
|
||||||
|
const { initialState, setInitialState } = useModel('@@initialState');
|
||||||
|
const { globalState, setGlobalState } = useModel('global');
|
||||||
|
const intl = useIntl();
|
||||||
|
const [form] = Form.useForm();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.log('initstate===', {
|
||||||
|
initialState,
|
||||||
|
globalState
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
const gotoDefaultPage = (userInfo: any) => {
|
||||||
|
const pathname = userInfo?.is_admin ? '/dashboard' : '/playground';
|
||||||
|
history.push(pathname);
|
||||||
|
};
|
||||||
|
const fetchUserInfo = async () => {
|
||||||
|
const userInfo = await initialState?.fetchUserInfo?.();
|
||||||
|
|
||||||
|
if (userInfo) {
|
||||||
|
flushSync(() => {
|
||||||
|
setInitialState((s: any) => ({
|
||||||
|
...s,
|
||||||
|
currentUser: userInfo
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return userInfo;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLogin = async (values: any) => {
|
||||||
|
console.log('values', values, form);
|
||||||
|
try {
|
||||||
|
await login({
|
||||||
|
username: values.username,
|
||||||
|
password: values.password
|
||||||
|
});
|
||||||
|
const userInfo = await fetchUserInfo();
|
||||||
|
setGlobalState({
|
||||||
|
userInfo
|
||||||
|
});
|
||||||
|
// if (userInfo?.require_password_change) {
|
||||||
|
// setCurrentUser(userInfo);
|
||||||
|
// } else {
|
||||||
|
// setCurrentUser(null);
|
||||||
|
// gotoDefaultPage(userInfo);
|
||||||
|
// }
|
||||||
|
gotoDefaultPage(userInfo);
|
||||||
|
} catch (error) {
|
||||||
|
console.log('error====', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div style={{ position: 'fixed', right: 0, top: 0, padding: '0 20px' }}>
|
||||||
|
<SelectLang icon={<GlobalOutlined />} reload={false} />
|
||||||
|
</div>
|
||||||
|
<Form
|
||||||
|
form={form}
|
||||||
|
style={{ width: '400px', margin: '0 auto', paddingTop: '5%' }}
|
||||||
|
onFinish={handleLogin}
|
||||||
|
>
|
||||||
|
<div>{renderLogo()}</div>
|
||||||
|
<Form.Item
|
||||||
|
name="username"
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: intl.formatMessage(
|
||||||
|
{ id: 'common.form.rule.input' },
|
||||||
|
{ name: intl.formatMessage({ id: 'common.form.username' }) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<SealInput.Input
|
||||||
|
label={intl.formatMessage({ id: 'common.form.username' })}
|
||||||
|
prefix={<UserOutlined />}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
name="password"
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: intl.formatMessage(
|
||||||
|
{ id: 'common.form.rule.input' },
|
||||||
|
{ name: intl.formatMessage({ id: 'common.form.password' }) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<SealInput.Password
|
||||||
|
prefix={<LockOutlined />}
|
||||||
|
label={intl.formatMessage({ id: 'common.form.password' })}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item name="autoLogin">
|
||||||
|
<div style={{ paddingLeft: 10 }}>
|
||||||
|
<Checkbox>
|
||||||
|
{intl.formatMessage({ id: 'common.login.rember' })}
|
||||||
|
</Checkbox>
|
||||||
|
</div>
|
||||||
|
</Form.Item>
|
||||||
|
<Button htmlType="submit" type="primary" block>
|
||||||
|
{intl.formatMessage({ id: 'menu.login' })}
|
||||||
|
</Button>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LoginForm;
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
import SealInput from '@/components/seal-form/seal-input';
|
||||||
|
import { GlobalOutlined, LockOutlined } from '@ant-design/icons';
|
||||||
|
import { SelectLang, history, useIntl, useModel } from '@umijs/max';
|
||||||
|
import { Button, Form, message } from 'antd';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
import { updatePassword } from '../apis';
|
||||||
|
|
||||||
|
const PasswordForm: React.FC = () => {
|
||||||
|
const { globalState, setGlobalState } = useModel('global');
|
||||||
|
const [currentUser, setCurrentUser] = useState(null);
|
||||||
|
const intl = useIntl();
|
||||||
|
const [form] = Form.useForm();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.log('initstate===', {
|
||||||
|
globalState
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
const gotoDefaultPage = (userInfo: any) => {
|
||||||
|
const pathname = userInfo?.is_admin ? '/dashboard' : '/playground';
|
||||||
|
history.push(pathname);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async (values: any) => {
|
||||||
|
console.log('values', values, form);
|
||||||
|
try {
|
||||||
|
await updatePassword({
|
||||||
|
new_password: values.new_password,
|
||||||
|
comfirm_password: values.comfirm_password
|
||||||
|
});
|
||||||
|
gotoDefaultPage(currentUser);
|
||||||
|
message.success(intl.formatMessage({ id: 'common.message.success' }));
|
||||||
|
} catch (error) {
|
||||||
|
console.log('error====', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div style={{ position: 'fixed', right: 0, top: 0, padding: '0 20px' }}>
|
||||||
|
<SelectLang icon={<GlobalOutlined />} reload={false} />
|
||||||
|
</div>
|
||||||
|
<Form
|
||||||
|
form={form}
|
||||||
|
style={{ width: '400px', margin: '0 auto', paddingTop: '5%' }}
|
||||||
|
onFinish={handleSubmit}
|
||||||
|
>
|
||||||
|
<div>修改密码</div>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
name="new_password"
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: intl.formatMessage(
|
||||||
|
{ id: 'common.form.rule.input' },
|
||||||
|
{ name: intl.formatMessage({ id: 'common.form.password' }) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<SealInput.Password
|
||||||
|
prefix={<LockOutlined />}
|
||||||
|
label={intl.formatMessage({ id: 'common.form.password' })}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
name="comfirm_password"
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: intl.formatMessage(
|
||||||
|
{ id: 'common.form.rule.input' },
|
||||||
|
{ name: intl.formatMessage({ id: 'common.form.password' }) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<SealInput.Password
|
||||||
|
prefix={<LockOutlined />}
|
||||||
|
label={intl.formatMessage({ id: 'common.form.password' })}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Button htmlType="submit" type="primary" block>
|
||||||
|
{intl.formatMessage({ id: 'common.button.submit' })}
|
||||||
|
</Button>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PasswordForm;
|
||||||
+5
-114
@@ -1,122 +1,13 @@
|
|||||||
import LogoIcon from '@/assets/images/logo.png';
|
import { useState } from 'react';
|
||||||
import SealInput from '@/components/seal-form/seal-input';
|
import LoginForm from './components/login-form';
|
||||||
import { GlobalOutlined, LockOutlined, UserOutlined } from '@ant-design/icons';
|
|
||||||
import { SelectLang, history, useIntl, useModel } from '@umijs/max';
|
|
||||||
import { Button, Checkbox, Form } from 'antd';
|
|
||||||
import { flushSync } from 'react-dom';
|
|
||||||
import { login } from './apis';
|
|
||||||
|
|
||||||
const renderLogo = () => {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
width: '400px',
|
|
||||||
display: 'flex',
|
|
||||||
marginBottom: 24,
|
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems: 'center'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<img src={LogoIcon} alt="logo" style={{ width: '44px' }} />
|
|
||||||
<span style={{ fontSize: 34, marginLeft: 12 }}>SEAL</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
const Login = () => {
|
const Login = () => {
|
||||||
const { initialState, setInitialState } = useModel('@@initialState');
|
const [currentUser, setCurrentUser] = useState(null);
|
||||||
const intl = useIntl();
|
|
||||||
const [form] = Form.useForm();
|
|
||||||
|
|
||||||
const gotoDefaultPage = (userInfo: any) => {
|
|
||||||
const pathname = userInfo?.is_admin ? '/dashboard' : '/playground';
|
|
||||||
history.push(pathname);
|
|
||||||
};
|
|
||||||
const fetchUserInfo = async () => {
|
|
||||||
const userInfo = await initialState?.fetchUserInfo?.();
|
|
||||||
|
|
||||||
if (userInfo) {
|
|
||||||
flushSync(() => {
|
|
||||||
setInitialState((s: any) => ({
|
|
||||||
...s,
|
|
||||||
currentUser: userInfo
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return userInfo;
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleLogin = async (values: any) => {
|
|
||||||
console.log('values', values, form);
|
|
||||||
try {
|
|
||||||
await login({
|
|
||||||
username: values.username,
|
|
||||||
password: values.password
|
|
||||||
});
|
|
||||||
const userInfo = await fetchUserInfo();
|
|
||||||
|
|
||||||
gotoDefaultPage(userInfo);
|
|
||||||
} catch (error) {
|
|
||||||
console.log('error====', error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div style={{ position: 'fixed', right: 0, top: 0, padding: '0 20px' }}>
|
<LoginForm setCurrentUser={setCurrentUser} />
|
||||||
<SelectLang icon={<GlobalOutlined />} reload={false} />
|
{/* <PasswordForm /> */}
|
||||||
</div>
|
|
||||||
<Form
|
|
||||||
form={form}
|
|
||||||
style={{ width: '400px', margin: '0 auto', paddingTop: '5%' }}
|
|
||||||
onFinish={handleLogin}
|
|
||||||
>
|
|
||||||
<div>{renderLogo()}</div>
|
|
||||||
<Form.Item
|
|
||||||
name="username"
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: true,
|
|
||||||
message: intl.formatMessage(
|
|
||||||
{ id: 'common.form.rule.input' },
|
|
||||||
{ name: intl.formatMessage({ id: 'common.form.username' }) }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<SealInput.Input
|
|
||||||
label={intl.formatMessage({ id: 'common.form.username' })}
|
|
||||||
prefix={<UserOutlined />}
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
<Form.Item
|
|
||||||
name="password"
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: true,
|
|
||||||
message: intl.formatMessage(
|
|
||||||
{ id: 'common.form.rule.input' },
|
|
||||||
{ name: intl.formatMessage({ id: 'common.form.password' }) }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<SealInput.Password
|
|
||||||
prefix={<LockOutlined />}
|
|
||||||
label={intl.formatMessage({ id: 'common.form.password' })}
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item name="autoLogin">
|
|
||||||
<div style={{ paddingLeft: 10 }}>
|
|
||||||
<Checkbox>
|
|
||||||
{intl.formatMessage({ id: 'common.login.rember' })}
|
|
||||||
</Checkbox>
|
|
||||||
</div>
|
|
||||||
</Form.Item>
|
|
||||||
<Button htmlType="submit" type="primary" block>
|
|
||||||
{intl.formatMessage({ id: 'menu.login' })}
|
|
||||||
</Button>
|
|
||||||
</Form>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -14,43 +14,3 @@ export async function execChatCompletions(params: any) {
|
|||||||
export const queryModelsList = async () => {
|
export const queryModelsList = async () => {
|
||||||
return request(`${OPENAI_MODELS}`);
|
return request(`${OPENAI_MODELS}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const fetchChatStream = async (params: any) => {
|
|
||||||
const response = await fetch(`${CHAT_API}`, {
|
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify(params),
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error('Network response was not ok');
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const reader = response?.body?.getReader();
|
|
||||||
const decoder = new TextDecoder('utf-8');
|
|
||||||
return {
|
|
||||||
reader,
|
|
||||||
decoder
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const receiveChatStream = async (
|
|
||||||
reader: any,
|
|
||||||
decoder: TextDecoder,
|
|
||||||
callback: (data: any) => void
|
|
||||||
) => {
|
|
||||||
const { done, value } = await reader.read();
|
|
||||||
|
|
||||||
if (done) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let chunk = decoder.decode(value, { stream: true });
|
|
||||||
if (chunk.startsWith('data:')) {
|
|
||||||
chunk = chunk.substring('data:'.length);
|
|
||||||
}
|
|
||||||
const item = JSON.parse(chunk?.trim());
|
|
||||||
callback(item);
|
|
||||||
await receiveChatStream(reader, decoder, callback);
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import TransitionWrapper from '@/components/transition';
|
import TransitionWrapper from '@/components/transition';
|
||||||
import HotKeys from '@/config/hotkeys';
|
import HotKeys from '@/config/hotkeys';
|
||||||
|
import { fetchChunkedData, readStreamData } from '@/utils/fetch-chunk-data';
|
||||||
import { EyeInvisibleOutlined } from '@ant-design/icons';
|
import { EyeInvisibleOutlined } from '@ant-design/icons';
|
||||||
import { PageContainer } from '@ant-design/pro-components';
|
import { PageContainer } from '@ant-design/pro-components';
|
||||||
import { useIntl } from '@umijs/max';
|
import { useIntl } from '@umijs/max';
|
||||||
@@ -7,7 +8,7 @@ import { Button, Input, Spin } from 'antd';
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { useRef, useState } from 'react';
|
import { useRef, useState } from 'react';
|
||||||
import { useHotkeys } from 'react-hotkeys-hook';
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
import { fetchChatStream, receiveChatStream } from '../apis';
|
import { CHAT_API } from '../apis';
|
||||||
import { Roles } from '../config';
|
import { Roles } from '../config';
|
||||||
import '../style/ground-left.less';
|
import '../style/ground-left.less';
|
||||||
import '../style/system-message-wrap.less';
|
import '../style/system-message-wrap.less';
|
||||||
@@ -65,7 +66,12 @@ const MessageList: React.FC<MessageProps> = (props) => {
|
|||||||
setActiveIndex(messageList.length - 1);
|
setActiveIndex(messageList.length - 1);
|
||||||
};
|
};
|
||||||
|
|
||||||
const joinMessage = (chunk: any) => {
|
const joinMessage = (str: any) => {
|
||||||
|
let data = str;
|
||||||
|
if (data.startsWith('data:')) {
|
||||||
|
data = data.substring('data:'.length);
|
||||||
|
}
|
||||||
|
const chunk = JSON.parse(data?.trim());
|
||||||
if (_.get(chunk, 'choices.0.finish_reason')) {
|
if (_.get(chunk, 'choices.0.finish_reason')) {
|
||||||
setTokenResult({
|
setTokenResult({
|
||||||
...chunk?.usage
|
...chunk?.usage
|
||||||
@@ -104,14 +110,17 @@ const MessageList: React.FC<MessageProps> = (props) => {
|
|||||||
...parameters,
|
...parameters,
|
||||||
stream: true
|
stream: true
|
||||||
};
|
};
|
||||||
const result = await fetchChatStream(chatParams);
|
const result = await fetchChunkedData({
|
||||||
|
data: chatParams,
|
||||||
|
url: CHAT_API
|
||||||
|
});
|
||||||
|
|
||||||
if (!result) {
|
if (!result) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const { reader, decoder } = result;
|
const { reader, decoder } = result;
|
||||||
|
|
||||||
await receiveChatStream(reader, decoder, (data: any) => {
|
await readStreamData(reader, decoder, (data: any) => {
|
||||||
joinMessage(data);
|
joinMessage(data);
|
||||||
});
|
});
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
@@ -218,12 +227,7 @@ const MessageList: React.FC<MessageProps> = (props) => {
|
|||||||
})}
|
})}
|
||||||
{loading && (
|
{loading && (
|
||||||
<Spin>
|
<Spin>
|
||||||
<MessageItem
|
<div style={{ height: '46px' }}></div>
|
||||||
message={{ role: Roles.Assistant, content: '' }}
|
|
||||||
isFocus={false}
|
|
||||||
onDelete={() => {}}
|
|
||||||
updateMessage={() => {}}
|
|
||||||
/>
|
|
||||||
</Spin>
|
</Spin>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { request } from '@umijs/max';
|
import { request } from '@umijs/max';
|
||||||
import { ListItem } from '../config/types';
|
import { GPUDeviceItem, ListItem } from '../config/types';
|
||||||
|
|
||||||
export const WORKERS_API = '/workers';
|
export const WORKERS_API = '/workers';
|
||||||
|
export const GPU_DEVICES_API = '/gpu-devices';
|
||||||
|
|
||||||
export async function queryWorkersList(
|
export async function queryWorkersList(
|
||||||
params: Global.Pagination & { query?: string }
|
params: Global.Pagination & { query?: string }
|
||||||
@@ -11,3 +12,18 @@ export async function queryWorkersList(
|
|||||||
params
|
params
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function queryGpuDevicesList(
|
||||||
|
params: Global.Pagination & { query?: string }
|
||||||
|
) {
|
||||||
|
return request<Global.PageResponse<GPUDeviceItem>>(`${GPU_DEVICES_API}`, {
|
||||||
|
methos: 'GET',
|
||||||
|
params
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function queryGPUDeviceItem(id: string) {
|
||||||
|
return request<GPUDeviceItem>(`${GPU_DEVICES_API}/${id}`, {
|
||||||
|
methos: 'GET'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,15 +1,18 @@
|
|||||||
import PageTools from '@/components/page-tools';
|
import PageTools from '@/components/page-tools';
|
||||||
import ProgressBar from '@/components/progress-bar';
|
import ProgressBar from '@/components/progress-bar';
|
||||||
import StatusTag from '@/components/status-tag';
|
|
||||||
import useTableRowSelection from '@/hooks/use-table-row-selection';
|
import useTableRowSelection from '@/hooks/use-table-row-selection';
|
||||||
import useTableSort from '@/hooks/use-table-sort';
|
import useTableSort from '@/hooks/use-table-sort';
|
||||||
|
import { convertFileSize } from '@/utils';
|
||||||
import { SyncOutlined } from '@ant-design/icons';
|
import { SyncOutlined } from '@ant-design/icons';
|
||||||
|
import { useIntl } from '@umijs/max';
|
||||||
import { Button, Input, Space, Table } from 'antd';
|
import { Button, Input, Space, Table } from 'antd';
|
||||||
import { useState } from 'react';
|
import _ from 'lodash';
|
||||||
import { Gpu } from '../config/types';
|
import { useEffect, useState } from 'react';
|
||||||
|
import { queryGpuDevicesList } from '../apis';
|
||||||
|
import { GPUDeviceItem } from '../config/types';
|
||||||
const { Column } = Table;
|
const { Column } = Table;
|
||||||
|
|
||||||
const dataSource: Gpu[] = [
|
const dataSource: GPUDeviceItem[] = [
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
name: 'bj-web-service-1',
|
name: 'bj-web-service-1',
|
||||||
@@ -101,23 +104,32 @@ const dataSource: Gpu[] = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
const Models: React.FC = () => {
|
const Models: React.FC = () => {
|
||||||
|
const intl = useIntl();
|
||||||
const rowSelection = useTableRowSelection();
|
const rowSelection = useTableRowSelection();
|
||||||
const { sortOrder, setSortOrder } = useTableSort({
|
const { sortOrder, setSortOrder } = useTableSort({
|
||||||
defaultSortOrder: 'descend'
|
defaultSortOrder: 'descend'
|
||||||
});
|
});
|
||||||
|
const [dataSource, setDataSource] = useState<GPUDeviceItem[]>([]);
|
||||||
const [total, setTotal] = useState(10);
|
const [total, setTotal] = useState(10);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [queryParams, setQueryParams] = useState({
|
const [queryParams, setQueryParams] = useState({
|
||||||
current: 1,
|
page: 1,
|
||||||
pageSize: 10,
|
perPage: 10,
|
||||||
name: ''
|
query: ''
|
||||||
});
|
});
|
||||||
const handleShowSizeChange = (current: number, size: number) => {
|
const handleShowSizeChange = (current: number, size: number) => {
|
||||||
console.log(current, size);
|
setQueryParams({
|
||||||
|
...queryParams,
|
||||||
|
perPage: size
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlePageChange = (page: number, pageSize: number | undefined) => {
|
const handlePageChange = (page: number, perPage: number | undefined) => {
|
||||||
console.log(page, pageSize);
|
console.log(page, perPage);
|
||||||
|
setQueryParams({
|
||||||
|
...queryParams,
|
||||||
|
page: page
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleTableChange = (pagination: any, filters: any, sorter: any) => {
|
const handleTableChange = (pagination: any, filters: any, sorter: any) => {
|
||||||
@@ -125,7 +137,20 @@ const Models: React.FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
console.log('fetchData');
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const params = {
|
||||||
|
..._.pickBy(queryParams, (val: any) => !!val)
|
||||||
|
};
|
||||||
|
const res = await queryGpuDevicesList(params);
|
||||||
|
|
||||||
|
setDataSource(res.items);
|
||||||
|
setTotal(res.pagination.total);
|
||||||
|
} catch (error) {
|
||||||
|
console.log('error', error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
const handleSearch = (e: any) => {
|
const handleSearch = (e: any) => {
|
||||||
fetchData();
|
fetchData();
|
||||||
@@ -134,10 +159,14 @@ const Models: React.FC = () => {
|
|||||||
const handleNameChange = (e: any) => {
|
const handleNameChange = (e: any) => {
|
||||||
setQueryParams({
|
setQueryParams({
|
||||||
...queryParams,
|
...queryParams,
|
||||||
name: e.target.value
|
query: e.target.value
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchData();
|
||||||
|
}, [queryParams]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PageTools
|
<PageTools
|
||||||
@@ -146,7 +175,9 @@ const Models: React.FC = () => {
|
|||||||
left={
|
left={
|
||||||
<Space>
|
<Space>
|
||||||
<Input
|
<Input
|
||||||
placeholder="名称查询"
|
placeholder={intl.formatMessage({
|
||||||
|
id: 'common.filter.name'
|
||||||
|
})}
|
||||||
style={{ width: 300 }}
|
style={{ width: 300 }}
|
||||||
onChange={handleNameChange}
|
onChange={handleNameChange}
|
||||||
></Input>
|
></Input>
|
||||||
@@ -166,46 +197,73 @@ const Models: React.FC = () => {
|
|||||||
onChange={handleTableChange}
|
onChange={handleTableChange}
|
||||||
pagination={{
|
pagination={{
|
||||||
showSizeChanger: true,
|
showSizeChanger: true,
|
||||||
pageSize: 10,
|
pageSize: queryParams.perPage,
|
||||||
current: 2,
|
current: queryParams.page,
|
||||||
total: total,
|
total: total,
|
||||||
hideOnSinglePage: true,
|
hideOnSinglePage: true,
|
||||||
onShowSizeChange: handleShowSizeChange,
|
onShowSizeChange: handleShowSizeChange,
|
||||||
onChange: handlePageChange
|
onChange: handlePageChange
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Column title="GPU Name" dataIndex="hostname" key="hostname" />
|
<Column title="Name" dataIndex="name" key="name" />
|
||||||
<Column
|
<Column
|
||||||
title="State"
|
title="Index"
|
||||||
dataIndex="state"
|
dataIndex="index"
|
||||||
key="state"
|
key="index"
|
||||||
render={(text, record) => {
|
render={(text, record: GPUDeviceItem) => {
|
||||||
return (
|
return <span>{record.index}</span>;
|
||||||
<StatusTag
|
|
||||||
statusValue={{
|
|
||||||
status: 'success',
|
|
||||||
text: 'ALIVE'
|
|
||||||
}}
|
|
||||||
></StatusTag>
|
|
||||||
);
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Column title="IP" dataIndex="address" key="address" />
|
<Column title="Worker Name" dataIndex="worker_name" key="worker_name" />
|
||||||
|
<Column title="Vendor" dataIndex="vendor" key="vendor" />
|
||||||
|
|
||||||
<Column
|
<Column
|
||||||
title="Temperature(˚C)"
|
title="Temperature(˚C)"
|
||||||
dataIndex="Temperature"
|
dataIndex="temperature"
|
||||||
key="Temperature"
|
key="Temperature"
|
||||||
|
render={(text, record: GPUDeviceItem) => {
|
||||||
|
return <span>{_.round(text, 1)}</span>;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Column
|
||||||
|
title="Core"
|
||||||
|
dataIndex="core"
|
||||||
|
key="Core"
|
||||||
|
render={(text, record: GPUDeviceItem) => {
|
||||||
|
return <span>{record.core?.total}</span>;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Column
|
||||||
|
title="GPU Utilization"
|
||||||
|
dataIndex="gpuUtil"
|
||||||
|
key="gpuUtil"
|
||||||
|
render={(text, record: GPUDeviceItem) => {
|
||||||
|
return (
|
||||||
|
<ProgressBar
|
||||||
|
percent={_.round(record.core?.utilization_rate, 2)}
|
||||||
|
></ProgressBar>
|
||||||
|
);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<Column title="Core" dataIndex="core" key="Core" />
|
|
||||||
<Column title="GPU-Util" dataIndex="gpuUtil" key="gpuUtil" />
|
|
||||||
|
|
||||||
<Column
|
<Column
|
||||||
title="VRAM"
|
title="VRAM"
|
||||||
dataIndex="GRAM"
|
dataIndex="GRAM"
|
||||||
key="VRAM"
|
key="VRAM"
|
||||||
render={(text, record: Gpu) => {
|
render={(text, record: GPUDeviceItem) => {
|
||||||
return <ProgressBar percent={0}></ProgressBar>;
|
return (
|
||||||
|
<ProgressBar
|
||||||
|
percent={_.round(record.memory.utilization_rate, 0)}
|
||||||
|
label={
|
||||||
|
<span className="flex-column">
|
||||||
|
<span>
|
||||||
|
Total: {convertFileSize(record.memory?.total, 0)}
|
||||||
|
</span>
|
||||||
|
<span>Used: {convertFileSize(record.memory?.used, 0)}</span>
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
></ProgressBar>
|
||||||
|
);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Table>
|
</Table>
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
import SealTable from '@/components/seal-table';
|
|
||||||
import SealColumn from '@/components/seal-table/components/seal-column';
|
|
||||||
import useTableRowSelection from '@/hooks/use-table-row-selection';
|
|
||||||
|
|
||||||
const Table = () => {
|
|
||||||
const dataSource = [{ name: 'test' }, { name: 'test2' }];
|
|
||||||
const rowSelection = useTableRowSelection();
|
|
||||||
return (
|
|
||||||
<SealTable
|
|
||||||
dataSource={dataSource}
|
|
||||||
rowKey="name"
|
|
||||||
loading={false}
|
|
||||||
rowSelection={rowSelection}
|
|
||||||
expandable={true}
|
|
||||||
>
|
|
||||||
<SealColumn title="Name" dataIndex="name" key="name" span={12} />
|
|
||||||
<SealColumn title="Status" dataIndex="status" key="status" span={12} />
|
|
||||||
</SealTable>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Table;
|
|
||||||
@@ -3,13 +3,14 @@ import ProgressBar from '@/components/progress-bar';
|
|||||||
import StatusTag from '@/components/status-tag';
|
import StatusTag from '@/components/status-tag';
|
||||||
import useTableRowSelection from '@/hooks/use-table-row-selection';
|
import useTableRowSelection from '@/hooks/use-table-row-selection';
|
||||||
import useTableSort from '@/hooks/use-table-sort';
|
import useTableSort from '@/hooks/use-table-sort';
|
||||||
|
import { convertFileSize } from '@/utils';
|
||||||
import { SyncOutlined } from '@ant-design/icons';
|
import { SyncOutlined } from '@ant-design/icons';
|
||||||
import { useIntl } from '@umijs/max';
|
import { useIntl } from '@umijs/max';
|
||||||
import { Button, Input, Space, Table } from 'antd';
|
import { Button, Input, Space, Table } from 'antd';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { queryWorkersList } from '../apis';
|
import { queryWorkersList } from '../apis';
|
||||||
import { ListItem } from '../config/types';
|
import { Filesystem, GPUDeviceItem, ListItem } from '../config/types';
|
||||||
const { Column } = Table;
|
const { Column } = Table;
|
||||||
|
|
||||||
const Models: React.FC = () => {
|
const Models: React.FC = () => {
|
||||||
@@ -44,7 +45,10 @@ const Models: React.FC = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
const handleShowSizeChange = (current: number, size: number) => {
|
const handleShowSizeChange = (current: number, size: number) => {
|
||||||
console.log(current, size);
|
setQueryParams({
|
||||||
|
...queryParams,
|
||||||
|
perPage: size
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlePageChange = (page: number, perPage: number | undefined) => {
|
const handlePageChange = (page: number, perPage: number | undefined) => {
|
||||||
@@ -74,7 +78,30 @@ const Models: React.FC = () => {
|
|||||||
if (!val2 || !val1) {
|
if (!val2 || !val1) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
return _.round((val1 / val2) * 100, 2);
|
return _.round((val1 / val2) * 100, 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
const calcStorage = (files: Filesystem[]) => {
|
||||||
|
const mountRoot = _.find(
|
||||||
|
files,
|
||||||
|
(item: Filesystem) => item.mount_point === '/'
|
||||||
|
);
|
||||||
|
return mountRoot ? formateUtilazation(mountRoot.used, mountRoot.total) : 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderStorageTooltip = (files: Filesystem[]) => {
|
||||||
|
const mountRoot = _.find(
|
||||||
|
files,
|
||||||
|
(item: Filesystem) => item.mount_point === '/'
|
||||||
|
);
|
||||||
|
return mountRoot ? (
|
||||||
|
<span className="flex-column">
|
||||||
|
<span>Total: {convertFileSize(mountRoot?.total, 0)}</span>
|
||||||
|
<span>Used: {convertFileSize(mountRoot?.used, 0)}</span>
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
0
|
||||||
|
);
|
||||||
};
|
};
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchData();
|
fetchData();
|
||||||
@@ -148,7 +175,7 @@ const Models: React.FC = () => {
|
|||||||
render={(text, record: ListItem) => {
|
render={(text, record: ListItem) => {
|
||||||
return (
|
return (
|
||||||
<ProgressBar
|
<ProgressBar
|
||||||
percent={_.round(record?.status?.cpu.utilization_rate, 2)}
|
percent={_.round(record?.status?.cpu.utilization_rate, 0)}
|
||||||
></ProgressBar>
|
></ProgressBar>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
@@ -164,6 +191,16 @@ const Models: React.FC = () => {
|
|||||||
record?.status?.memory.used,
|
record?.status?.memory.used,
|
||||||
record?.status?.memory.total
|
record?.status?.memory.total
|
||||||
)}
|
)}
|
||||||
|
label={
|
||||||
|
<span className="flex-column">
|
||||||
|
<span>
|
||||||
|
Total: {convertFileSize(record?.status?.memory.total, 0)}
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
Used: {convertFileSize(record?.status?.memory.used, 0)}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
}
|
||||||
></ProgressBar>
|
></ProgressBar>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
@@ -174,30 +211,19 @@ const Models: React.FC = () => {
|
|||||||
key="GPU"
|
key="GPU"
|
||||||
render={(text, record: ListItem) => {
|
render={(text, record: ListItem) => {
|
||||||
return (
|
return (
|
||||||
<Space>
|
<span className="flex-column">
|
||||||
{record?.status?.gpu.map((item) => {
|
{record?.status?.gpu_devices.map((item, index) => {
|
||||||
return (
|
return (
|
||||||
<span key={item.index} className="flex-center">
|
<span className="flex-center" key={index}>
|
||||||
<span
|
<span className="m-r-5">[{index}]</span>
|
||||||
style={{
|
<ProgressBar
|
||||||
display: 'flex',
|
key={index}
|
||||||
width: '6px',
|
percent={_.round(item.core.utilization_rate, 0)}
|
||||||
height: '6px',
|
></ProgressBar>
|
||||||
borderRadius: '50%',
|
|
||||||
backgroundColor: 'var(--ant-color-primary)'
|
|
||||||
}}
|
|
||||||
></span>
|
|
||||||
<span className="m-l-5">
|
|
||||||
{' '}
|
|
||||||
{`${item.core.total}C`} /{' '}
|
|
||||||
{item.core.utilization_rate
|
|
||||||
? `${item.core.utilization_rate}%`
|
|
||||||
: 0}
|
|
||||||
</span>
|
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</Space>
|
</span>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -208,28 +234,39 @@ const Models: React.FC = () => {
|
|||||||
key="VRAM"
|
key="VRAM"
|
||||||
render={(text, record: ListItem) => {
|
render={(text, record: ListItem) => {
|
||||||
return (
|
return (
|
||||||
<Space>
|
<span className="flex-column">
|
||||||
{record?.status?.gpu.map((item) => {
|
{record?.status?.gpu_devices.map(
|
||||||
return (
|
(item: GPUDeviceItem, index) => {
|
||||||
<span key={item.index} className="flex-center">
|
return (
|
||||||
<span
|
<span key={index}>
|
||||||
style={{
|
{item.memory.is_unified_memory ? (
|
||||||
display: 'flex',
|
'Unified Memory'
|
||||||
width: '6px',
|
) : (
|
||||||
height: '6px',
|
<span className="flex-center">
|
||||||
borderRadius: '50%',
|
<span className="m-r-5">[{index}]</span>
|
||||||
backgroundColor: 'var(--ant-color-primary)'
|
<ProgressBar
|
||||||
}}
|
key={index}
|
||||||
></span>
|
percent={_.round(item.memory.utilization_rate, 0)}
|
||||||
<span className="m-l-5">
|
label={
|
||||||
{item.memory.allocated
|
<span className="flex-column">
|
||||||
? `${formateUtilazation(item.memory.allocated, item.memory.total)}%`
|
<span>
|
||||||
: 0}
|
Total:{' '}
|
||||||
|
{convertFileSize(item.memory?.total, 0)}
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
Used:{' '}
|
||||||
|
{convertFileSize(item.memory?.used, 0)}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
></ProgressBar>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
);
|
||||||
);
|
}
|
||||||
})}
|
)}
|
||||||
</Space>
|
</span>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -238,7 +275,12 @@ const Models: React.FC = () => {
|
|||||||
dataIndex="storage"
|
dataIndex="storage"
|
||||||
key="storage"
|
key="storage"
|
||||||
render={(text, record: ListItem) => {
|
render={(text, record: ListItem) => {
|
||||||
return <ProgressBar percent={0}></ProgressBar>;
|
return (
|
||||||
|
<ProgressBar
|
||||||
|
percent={calcStorage(record.status?.filesystem)}
|
||||||
|
label={renderStorageTooltip(record.status.filesystem)}
|
||||||
|
></ProgressBar>
|
||||||
|
);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Table>
|
</Table>
|
||||||
|
|||||||
@@ -17,6 +17,29 @@ export interface Gpu {
|
|||||||
temperature: number;
|
temperature: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface GPUDeviceItem {
|
||||||
|
uuid: string;
|
||||||
|
name: string;
|
||||||
|
vendor: string;
|
||||||
|
index: number;
|
||||||
|
core: {
|
||||||
|
total: number;
|
||||||
|
utilization_rate: number;
|
||||||
|
};
|
||||||
|
memory: {
|
||||||
|
total: number;
|
||||||
|
utilization_rate: number;
|
||||||
|
is_unified_memory: boolean;
|
||||||
|
used: number;
|
||||||
|
allocated: number;
|
||||||
|
};
|
||||||
|
temperature: number;
|
||||||
|
id: string;
|
||||||
|
worker_id: number;
|
||||||
|
worker_name: string;
|
||||||
|
worker_ip: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface Filesystem {
|
export interface Filesystem {
|
||||||
name: string;
|
name: string;
|
||||||
mount_point: string;
|
mount_point: string;
|
||||||
@@ -52,7 +75,7 @@ export interface ListItem {
|
|||||||
used: number;
|
used: number;
|
||||||
allocated: number;
|
allocated: number;
|
||||||
};
|
};
|
||||||
gpu: Gpu[];
|
gpu_devices: GPUDeviceItem[];
|
||||||
swap: {
|
swap: {
|
||||||
total: number;
|
total: number;
|
||||||
used: number;
|
used: number;
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import qs from 'query-string';
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param params data: for post request, params: for get request
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const fetchChunkedData = async (params: {
|
||||||
|
data?: any;
|
||||||
|
url: string;
|
||||||
|
params?: any;
|
||||||
|
method?: string;
|
||||||
|
}) => {
|
||||||
|
const method = params.method || 'POST';
|
||||||
|
let url = params.url;
|
||||||
|
if (params.params) {
|
||||||
|
url = `${url}?${qs.stringify(params.params)}`;
|
||||||
|
}
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method,
|
||||||
|
body: method === 'POST' ? JSON.stringify(params.data) : null,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Network response was not ok');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const reader = response?.body?.getReader();
|
||||||
|
const decoder = new TextDecoder('utf-8');
|
||||||
|
return {
|
||||||
|
reader,
|
||||||
|
decoder
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const readStreamData = async (
|
||||||
|
reader: any,
|
||||||
|
decoder: TextDecoder,
|
||||||
|
callback: (data: any) => void
|
||||||
|
) => {
|
||||||
|
const { done, value } = await reader.read();
|
||||||
|
|
||||||
|
if (done) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let chunk = decoder.decode(value, { stream: true });
|
||||||
|
console.log('chunk==========', chunk);
|
||||||
|
callback(chunk);
|
||||||
|
await readStreamData(reader, decoder, callback);
|
||||||
|
};
|
||||||
+7
-6
@@ -19,20 +19,21 @@ export const handleBatchRequest = async (
|
|||||||
return Promise.all(list.map((item) => fn(item)));
|
return Promise.all(list.map((item) => fn(item)));
|
||||||
};
|
};
|
||||||
|
|
||||||
export const convertFileSize = (sizeInBytes: number) => {
|
export const convertFileSize = (sizeInBytes: number, prec?: number) => {
|
||||||
|
const precision = prec ?? 2;
|
||||||
if (!sizeInBytes) {
|
if (!sizeInBytes) {
|
||||||
return '0 B';
|
return '0 B';
|
||||||
}
|
}
|
||||||
if (sizeInBytes < 1024) {
|
if (sizeInBytes < 1024) {
|
||||||
return `${sizeInBytes.toFixed(2)} B`;
|
return `${sizeInBytes.toFixed(precision)} B`;
|
||||||
} else if (sizeInBytes < 1024 * 1024) {
|
} else if (sizeInBytes < 1024 * 1024) {
|
||||||
return `${(sizeInBytes / 1024).toFixed(2)} KB`;
|
return `${(sizeInBytes / 1024).toFixed(precision)} KiB`;
|
||||||
} else if (sizeInBytes < 1024 * 1024 * 1024) {
|
} else if (sizeInBytes < 1024 * 1024 * 1024) {
|
||||||
return `${(sizeInBytes / (1024 * 1024)).toFixed(2)} MB`;
|
return `${(sizeInBytes / (1024 * 1024)).toFixed(precision)} MiB`;
|
||||||
} else if (sizeInBytes < 1024 * 1024 * 1024 * 1024) {
|
} else if (sizeInBytes < 1024 * 1024 * 1024 * 1024) {
|
||||||
return `${(sizeInBytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
|
return `${(sizeInBytes / (1024 * 1024 * 1024)).toFixed(precision)} GiB`;
|
||||||
} else {
|
} else {
|
||||||
return `${(sizeInBytes / (1024 * 1024 * 1024 * 1024)).toFixed(2)} TB`;
|
return `${(sizeInBytes / (1024 * 1024 * 1024 * 1024)).toFixed(precision)} TiB`;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user