Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f730548c8f | ||
|
|
a185bd9463 | ||
|
|
abdc78e123 | ||
|
|
ba49fa6c88 | ||
|
|
870ffa9dc1 | ||
|
|
8afaf1a60e | ||
|
|
6974ac8e14 | ||
|
|
9f274ee17e | ||
|
|
673bcde719 | ||
|
|
6eaf5bb463 | ||
|
|
b463eedb3f | ||
|
|
90350f99ad | ||
|
|
8576015633 | ||
|
|
f5edf39027 | ||
|
|
751781e3e5 | ||
|
|
bc9129bcbe | ||
|
|
c442a33d5e | ||
|
|
902bb06353 | ||
|
|
a0a1b8b3e8 | ||
|
|
eb5cf9da8d | ||
|
|
57386eeb55 | ||
|
|
33a21f2aaa | ||
|
|
d6fd498489 | ||
|
|
386f2969eb | ||
|
|
2fea3a4b40 | ||
|
|
b9c9fe7a97 | ||
|
|
594c4cd670 | ||
|
|
e10c5e28b5 | ||
|
|
fbefb2014d | ||
|
|
909da7edb5 | ||
|
|
349676d908 | ||
|
|
a47f7cc0ce | ||
|
|
8fbdb884c7 | ||
|
|
ec68ce082e | ||
|
|
2b61e3dfd3 | ||
|
|
4c3dddc486 | ||
|
|
ecc683a6bd | ||
|
|
7b2098cf33 | ||
|
|
3784d86344 | ||
|
|
2b22481443 | ||
|
|
c041d768b2 | ||
|
|
5df6154abe | ||
|
|
1fed02d23f | ||
|
|
2d34d07da4 | ||
|
|
ea25291d75 | ||
|
|
5f77a0082c | ||
|
|
3978ec683d | ||
|
|
458f467985 | ||
|
|
69ffdd4604 | ||
|
|
5c2503b2a8 | ||
|
|
bc921ff835 | ||
|
|
197488bff6 | ||
|
|
e48d21c8cb | ||
|
|
7e992fad99 | ||
|
|
49c0467428 | ||
|
|
7e985991cd | ||
|
|
cc7d2ded6c | ||
|
|
f813f56c1f | ||
|
|
a698c65a07 | ||
|
|
3cd96ba2e8 | ||
|
|
0652f850d6 | ||
|
|
3ab29f9e01 | ||
|
|
b041cdc766 | ||
|
|
c04b9342b8 | ||
|
|
1abaa164f5 | ||
|
|
4f4f00f0c8 | ||
|
|
2e5f0d9210 | ||
|
|
af077c4706 | ||
|
|
a5cebb88ef | ||
|
|
6baf8f08c8 |
@@ -1,3 +1,7 @@
|
|||||||
|
# GPUStack UI
|
||||||
|
|
||||||
|
UI for [GPUStack](https://github.com/gpustack/gpustack).
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
1. [Nodejs](https://nodejs.org/en) 16.0+(with NPM)
|
1. [Nodejs](https://nodejs.org/en) 16.0+(with NPM)
|
||||||
|
|||||||
+1
-1
@@ -14,7 +14,7 @@ const isProduction = env === 'production';
|
|||||||
const t = Date.now();
|
const t = Date.now();
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
proxy: {
|
proxy: {
|
||||||
...proxy('http://192.168.50.166:8080')
|
...proxy()
|
||||||
},
|
},
|
||||||
history: {
|
history: {
|
||||||
type: 'hash'
|
type: 'hash'
|
||||||
|
|||||||
+9
-1
@@ -1,4 +1,12 @@
|
|||||||
const proxyTableList = ['cli', 'v1', 'auth', 'v1-openai', 'version'];
|
const proxyTableList = [
|
||||||
|
'cli',
|
||||||
|
'v1',
|
||||||
|
'auth',
|
||||||
|
'v1-openai',
|
||||||
|
'version',
|
||||||
|
'proxy',
|
||||||
|
'update'
|
||||||
|
];
|
||||||
|
|
||||||
// @ts-ingore
|
// @ts-ingore
|
||||||
export default function createProxyTable(target?: string) {
|
export default function createProxyTable(target?: string) {
|
||||||
|
|||||||
@@ -31,13 +31,6 @@ export default [
|
|||||||
access: 'canSeeAdmin',
|
access: 'canSeeAdmin',
|
||||||
component: './resources'
|
component: './resources'
|
||||||
},
|
},
|
||||||
// {
|
|
||||||
// name: 'usage',
|
|
||||||
// path: '/usage',
|
|
||||||
// key: 'usage',
|
|
||||||
// icon: 'BarChartOutlined',
|
|
||||||
// component: './usage'
|
|
||||||
// },
|
|
||||||
{
|
{
|
||||||
name: 'apikeys',
|
name: 'apikeys',
|
||||||
path: '/api-keys',
|
path: '/api-keys',
|
||||||
|
|||||||
+7
-1
@@ -28,6 +28,10 @@ export default {
|
|||||||
Message: {
|
Message: {
|
||||||
contentPadding: '12px 16px'
|
contentPadding: '12px 16px'
|
||||||
},
|
},
|
||||||
|
Tooltip: {
|
||||||
|
colorBgSpotlight: 'rgba(110,110,110,1)'
|
||||||
|
// sizePopupArrow: 0
|
||||||
|
},
|
||||||
Slider: {
|
Slider: {
|
||||||
handleSize: 8,
|
handleSize: 8,
|
||||||
handleSizeHover: 8,
|
handleSizeHover: 8,
|
||||||
@@ -42,7 +46,9 @@ export default {
|
|||||||
},
|
},
|
||||||
token: {
|
token: {
|
||||||
colorPrimary: '#007BFF',
|
colorPrimary: '#007BFF',
|
||||||
borderRadius: 8,
|
colorSuccess: '#54cc98',
|
||||||
|
borderRadius: 4,
|
||||||
|
borderRadiusSM: 2,
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
motion: true
|
motion: true
|
||||||
}
|
}
|
||||||
|
|||||||
+15
-5
@@ -1,29 +1,34 @@
|
|||||||
{
|
{
|
||||||
"private": true,
|
"private": true,
|
||||||
"author": "jialin <jialinkuang@126.com>",
|
"author": "jialin",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "max build",
|
"build": "max build",
|
||||||
"dev": "max dev",
|
"dev": "max dev",
|
||||||
"format": "prettier --cache --write .",
|
"format": "prettier --cache --write .",
|
||||||
"postinstall": "max setup",
|
"postinstall": "max setup",
|
||||||
"prepare": "husky",
|
"prepare": "husky",
|
||||||
|
"preview": "max preview",
|
||||||
"setup": "max setup",
|
"setup": "max setup",
|
||||||
"start": "npm run dev"
|
"start": "npm run dev"
|
||||||
},
|
},
|
||||||
|
"resolutions": {
|
||||||
|
"immer": "^9.0.6"
|
||||||
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ant-design/icons": "^5.3.7",
|
"@ant-design/icons": "^5.5.1",
|
||||||
"@ant-design/pro-components": "^2.7.1",
|
"@ant-design/pro-components": "^2.7.18",
|
||||||
"@huggingface/gguf": "^0.1.7",
|
"@huggingface/gguf": "^0.1.7",
|
||||||
"@huggingface/hub": "^0.15.1",
|
"@huggingface/hub": "^0.15.1",
|
||||||
"@huggingface/tasks": "^0.11.6",
|
"@huggingface/tasks": "^0.11.6",
|
||||||
"@monaco-editor/react": "^4.6.0",
|
"@monaco-editor/react": "^4.6.0",
|
||||||
|
"@orcid/bibtex-parse-js": "^0.0.25",
|
||||||
"@react-hook/resize-observer": "^2.0.2",
|
"@react-hook/resize-observer": "^2.0.2",
|
||||||
"@types/lodash": "^4.17.4",
|
"@types/lodash": "^4.17.4",
|
||||||
"@umijs/max": "^4.2.11",
|
"@umijs/max": "^4.2.11",
|
||||||
"@xterm/addon-fit": "^0.10.0",
|
"@xterm/addon-fit": "^0.10.0",
|
||||||
"@xterm/xterm": "^5.5.0",
|
"@xterm/xterm": "^5.5.0",
|
||||||
"ansi-to-html": "^0.7.2",
|
"ansi-to-html": "^0.7.2",
|
||||||
"antd": "^5.18.3",
|
"antd": "^5.20.6",
|
||||||
"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",
|
||||||
@@ -36,14 +41,19 @@
|
|||||||
"jotai": "^2.8.4",
|
"jotai": "^2.8.4",
|
||||||
"localforage": "^1.10.0",
|
"localforage": "^1.10.0",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
|
"mammoth": "^1.8.0",
|
||||||
"marked": "^14.1.0",
|
"marked": "^14.1.0",
|
||||||
"numeral": "^2.0.6",
|
"numeral": "^2.0.6",
|
||||||
|
"overlayscrollbars": "^2.10.0",
|
||||||
|
"overlayscrollbars-react": "^0.5.6",
|
||||||
"query-string": "^9.0.0",
|
"query-string": "^9.0.0",
|
||||||
|
"rc-virtual-list": "^3.14.8",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-hotkeys-hook": "^4.5.0",
|
"react-hotkeys-hook": "^4.5.0",
|
||||||
"simplebar-react": "^3.2.6",
|
"simplebar-react": "^3.2.6",
|
||||||
"umi-presets-pro": "^2.0.3"
|
"umi-presets-pro": "^2.0.3",
|
||||||
|
"xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/marked": "^6.0.0",
|
"@types/marked": "^6.0.0",
|
||||||
|
|||||||
@@ -4,11 +4,12 @@ export default (api: IApi) => {
|
|||||||
api.modifyHTML(($) => {
|
api.modifyHTML(($) => {
|
||||||
const info = JSON.parse(process.env.VERSION || '{}');
|
const info = JSON.parse(process.env.VERSION || '{}');
|
||||||
const env = process.env.NODE_ENV;
|
const env = process.env.NODE_ENV;
|
||||||
|
|
||||||
|
$('html').attr('data-env', env);
|
||||||
|
|
||||||
$('html').attr(
|
$('html').attr(
|
||||||
'data-version',
|
'data-version',
|
||||||
env === 'production'
|
env === 'production' ? info.version || info.commitId : `${info.commitId}`
|
||||||
? info.version || info.commitId
|
|
||||||
: `dev-${info.commitId}`
|
|
||||||
);
|
);
|
||||||
if (env === 'production') {
|
if (env === 'production') {
|
||||||
$('script[src^="/js/umi"]').first?.().remove?.();
|
$('script[src^="/js/umi"]').first?.().remove?.();
|
||||||
|
|||||||
Generated
+1609
-1031
File diff suppressed because it is too large
Load Diff
+16
-2
@@ -1,9 +1,10 @@
|
|||||||
import { GPUStackVersionAtom } from '@/atoms/user';
|
import { GPUStackVersionAtom, UpdateCheckAtom } from '@/atoms/user';
|
||||||
import { setAtomStorage } from '@/atoms/utils';
|
import { setAtomStorage } from '@/atoms/utils';
|
||||||
import { requestConfig } from '@/request-config';
|
import { requestConfig } from '@/request-config';
|
||||||
import {
|
import {
|
||||||
queryCurrentUserState,
|
queryCurrentUserState,
|
||||||
queryVersionInfo
|
queryVersionInfo,
|
||||||
|
updateCheck
|
||||||
} from '@/services/profile/apis';
|
} from '@/services/profile/apis';
|
||||||
import { RequestConfig, history } from '@umijs/max';
|
import { RequestConfig, history } from '@umijs/max';
|
||||||
|
|
||||||
@@ -16,11 +17,24 @@ export async function getInitialState(): Promise<{
|
|||||||
}> {
|
}> {
|
||||||
const { location } = history;
|
const { location } = history;
|
||||||
|
|
||||||
|
const getUpdateCheck = async () => {
|
||||||
|
try {
|
||||||
|
const data = await updateCheck();
|
||||||
|
setAtomStorage(UpdateCheckAtom, data);
|
||||||
|
return data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('updateCheck error', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const fetchUserInfo = async (): Promise<Global.UserInfo> => {
|
const fetchUserInfo = async (): Promise<Global.UserInfo> => {
|
||||||
try {
|
try {
|
||||||
const data = await queryCurrentUserState({
|
const data = await queryCurrentUserState({
|
||||||
skipErrorHandler: true
|
skipErrorHandler: true
|
||||||
});
|
});
|
||||||
|
if (data.is_admin) {
|
||||||
|
getUpdateCheck();
|
||||||
|
}
|
||||||
return data;
|
return data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
history.push(loginPath);
|
history.push(loginPath);
|
||||||
|
|||||||
@@ -38,6 +38,10 @@
|
|||||||
margin-left: 20px;
|
margin-left: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.m-l-40 {
|
||||||
|
margin-left: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
.m-r-10 {
|
.m-r-10 {
|
||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
}
|
}
|
||||||
@@ -66,6 +70,10 @@
|
|||||||
padding-left: 6px;
|
padding-left: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.p-l-8 {
|
||||||
|
padding-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
.flex {
|
.flex {
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
@@ -85,6 +93,10 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.flex-wrap {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
.flex-gap-2 {
|
.flex-gap-2 {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 2px;
|
gap: 2px;
|
||||||
@@ -98,6 +110,11 @@
|
|||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.flex-end {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
.flex-column {
|
.flex-column {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -119,6 +136,10 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.font-size-0 {
|
||||||
|
font-size: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.font-size-12 {
|
.font-size-12 {
|
||||||
font-size: var(--font-size-base);
|
font-size: var(--font-size-base);
|
||||||
}
|
}
|
||||||
@@ -131,6 +152,10 @@
|
|||||||
font-size: var(--font-size-large);
|
font-size: var(--font-size-large);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.font-size-20 {
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
.font-400 {
|
.font-400 {
|
||||||
font-weight: var(--font-weight-normal);
|
font-weight: var(--font-weight-normal);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,12 @@ export const GPUStackVersionAtom = atom<{
|
|||||||
git_commit: ''
|
git_commit: ''
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const UpdateCheckAtom = atom<{
|
||||||
|
latest_version: string;
|
||||||
|
}>({
|
||||||
|
latest_version: ''
|
||||||
|
});
|
||||||
|
|
||||||
export const initialPasswordAtom = atomWithStorage<string>(
|
export const initialPasswordAtom = atomWithStorage<string>(
|
||||||
'initialPassword',
|
'initialPassword',
|
||||||
''
|
''
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { Image as AntImage, ImageProps } from 'antd';
|
||||||
|
import { round } from 'lodash';
|
||||||
|
import React, { useCallback, useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
const AutoImage: React.FC<ImageProps & { height: number }> = (props) => {
|
||||||
|
const { height = 100, ...rest } = props;
|
||||||
|
const [width, setWidth] = useState(0);
|
||||||
|
|
||||||
|
const getImgRatio = useCallback((url: string): Promise<{ ratio: number }> => {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const img = new Image();
|
||||||
|
img.onload = () => {
|
||||||
|
resolve({ ratio: round(img.width / img.height, 2) });
|
||||||
|
};
|
||||||
|
img.onerror = () => {
|
||||||
|
resolve({ ratio: 1 });
|
||||||
|
};
|
||||||
|
img.src = url;
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleOnLoad = useCallback(async () => {
|
||||||
|
const { ratio } = await getImgRatio(props.src || '');
|
||||||
|
setWidth(height * ratio);
|
||||||
|
}, [getImgRatio, height, props.src]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
handleOnLoad();
|
||||||
|
}, [handleOnLoad]);
|
||||||
|
|
||||||
|
return <AntImage {...rest} height={height} width={width} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AutoImage;
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Tag, Tooltip } from 'antd';
|
import { Tag, Tooltip, type TagProps } from 'antd';
|
||||||
import debounce from 'lodash/debounce';
|
import { throttle } from 'lodash';
|
||||||
import React, {
|
import React, {
|
||||||
useCallback,
|
useCallback,
|
||||||
useEffect,
|
useEffect,
|
||||||
@@ -7,46 +7,51 @@ import React, {
|
|||||||
useRef,
|
useRef,
|
||||||
useState
|
useState
|
||||||
} from 'react';
|
} from 'react';
|
||||||
|
import TitleTip from './title-tip';
|
||||||
|
|
||||||
interface AutoTooltipProps extends React.ComponentProps<typeof Tag> {
|
// type TagProps = React.ComponentProps<typeof Tag>;
|
||||||
|
|
||||||
|
interface AutoTooltipProps extends Omit<TagProps, 'title'> {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
maxWidth?: number | string;
|
maxWidth?: number | string;
|
||||||
|
minWidth?: number | string;
|
||||||
color?: string;
|
color?: string;
|
||||||
style?: React.CSSProperties;
|
style?: React.CSSProperties;
|
||||||
ghost?: boolean;
|
ghost?: boolean;
|
||||||
|
title?: React.ReactNode;
|
||||||
showTitle?: boolean;
|
showTitle?: boolean;
|
||||||
|
tooltipProps?: React.ComponentProps<typeof Tooltip>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const AutoTooltip: React.FC<AutoTooltipProps> = ({
|
const AutoTooltip: React.FC<AutoTooltipProps> = ({
|
||||||
children,
|
children,
|
||||||
maxWidth = '100%',
|
maxWidth = '100%',
|
||||||
|
minWidth,
|
||||||
ghost = false,
|
ghost = false,
|
||||||
|
title,
|
||||||
showTitle = false,
|
showTitle = false,
|
||||||
|
tooltipProps,
|
||||||
...tagProps
|
...tagProps
|
||||||
}) => {
|
}) => {
|
||||||
const contentRef = useRef<HTMLDivElement>(null);
|
const contentRef = useRef<HTMLDivElement>(null);
|
||||||
const [isOverflowing, setIsOverflowing] = useState(false);
|
const [isOverflowing, setIsOverflowing] = useState(false);
|
||||||
|
const resizeObserver = useRef<ResizeObserver>();
|
||||||
|
|
||||||
const checkOverflow = useCallback(() => {
|
const checkOverflow = useCallback(() => {
|
||||||
if (contentRef.current) {
|
if (contentRef.current) {
|
||||||
const { scrollWidth, clientWidth } = contentRef.current;
|
const { scrollWidth, clientWidth } = contentRef.current;
|
||||||
setIsOverflowing(scrollWidth > clientWidth);
|
setIsOverflowing(scrollWidth > clientWidth);
|
||||||
}
|
}
|
||||||
}, []);
|
}, [contentRef.current]);
|
||||||
|
|
||||||
const debouncedCheckOverflow = useMemo(
|
|
||||||
() => debounce(checkOverflow, 200),
|
|
||||||
[checkOverflow]
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
checkOverflow();
|
const debouncedCheckOverflow = throttle(checkOverflow, 200);
|
||||||
window.addEventListener('resize', debouncedCheckOverflow);
|
window.addEventListener('resize', debouncedCheckOverflow);
|
||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener('resize', debouncedCheckOverflow);
|
window.removeEventListener('resize', debouncedCheckOverflow);
|
||||||
debouncedCheckOverflow.cancel();
|
debouncedCheckOverflow.cancel();
|
||||||
};
|
};
|
||||||
}, [checkOverflow, debouncedCheckOverflow]);
|
}, [checkOverflow]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
checkOverflow();
|
checkOverflow();
|
||||||
@@ -55,6 +60,7 @@ const AutoTooltip: React.FC<AutoTooltipProps> = ({
|
|||||||
const tagStyle = useMemo(
|
const tagStyle = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
maxWidth,
|
maxWidth,
|
||||||
|
minWidth,
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
textOverflow: 'ellipsis',
|
textOverflow: 'ellipsis',
|
||||||
whiteSpace: 'nowrap' as const,
|
whiteSpace: 'nowrap' as const,
|
||||||
@@ -64,7 +70,24 @@ const AutoTooltip: React.FC<AutoTooltipProps> = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip title={isOverflowing || showTitle ? children : ''}>
|
<Tooltip
|
||||||
|
overlayInnerStyle={{ paddingInline: 0 }}
|
||||||
|
destroyTooltipOnHide={false}
|
||||||
|
title={
|
||||||
|
isOverflowing || showTitle ? (
|
||||||
|
<TitleTip
|
||||||
|
isOverflowing={isOverflowing}
|
||||||
|
title={title}
|
||||||
|
showTitle={showTitle}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</TitleTip>
|
||||||
|
) : (
|
||||||
|
''
|
||||||
|
)
|
||||||
|
}
|
||||||
|
{...tooltipProps}
|
||||||
|
>
|
||||||
{ghost ? (
|
{ghost ? (
|
||||||
<div ref={contentRef} style={tagStyle}>
|
<div ref={contentRef} style={tagStyle}>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import useOverlayScroller from '@/hooks/use-overlay-scroller';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
interface TitleTipProps {
|
||||||
|
isOverflowing: boolean;
|
||||||
|
showTitle: boolean;
|
||||||
|
title: React.ReactNode;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TitleTip: React.FC<TitleTipProps> = (props) => {
|
||||||
|
const { isOverflowing, showTitle, title, children } = props;
|
||||||
|
const { initialize } = useOverlayScroller();
|
||||||
|
const scrollRef = React.useRef<any>(null);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (scrollRef.current) {
|
||||||
|
initialize(scrollRef.current);
|
||||||
|
}
|
||||||
|
}, [initialize, scrollRef.current]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ maxHeight: 200, overflowY: 'auto' }} ref={scrollRef}>
|
||||||
|
<div style={{ width: 'max-content', maxWidth: 250, paddingInline: 10 }}>
|
||||||
|
{isOverflowing || showTitle ? title || children : ''}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default React.memo(TitleTip);
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import bibtexParse from '@orcid/bibtex-parse-js';
|
||||||
|
import { Typography } from 'antd';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
/*
|
||||||
|
@inproceedings{Lysenko:2010:GMC:1839778.1839781,\
|
||||||
|
author = {Lysenko, Mikola and Nelaturi, Saigopal and Shapiro, Vadim},\
|
||||||
|
title = {Group morphology with convolution algebras},\
|
||||||
|
booktitle = {Proceedings of the 14th ACM Symposium on Solid and Physical Modeling},\
|
||||||
|
series = {SPM '10},\
|
||||||
|
year = {2010},\
|
||||||
|
isbn = {978-1-60558-984-8},\
|
||||||
|
location = {Haifa, Israel},\
|
||||||
|
pages = {11--22},\
|
||||||
|
numpages = {12},\
|
||||||
|
url = {http://doi.acm.org/10.1145/1839778.1839781},\
|
||||||
|
doi = {10.1145/1839778.1839781},\
|
||||||
|
acmid = {1839781},\
|
||||||
|
publisher = {ACM},\
|
||||||
|
address = {New York, NY, USA},\
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
const BibTeXViewer: React.FC<{ data: string }> = ({ data }) => {
|
||||||
|
if (!data) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const dataList = bibtexParse.toJSON(data);
|
||||||
|
return (
|
||||||
|
<ol>
|
||||||
|
{dataList.map((item: any, index: number) => (
|
||||||
|
<li key={index} style={{ lineHeight: 2 }}>
|
||||||
|
<Typography.Link href={item.entryTags?.url} target="_blank">
|
||||||
|
{item.entryTags?.title}.{' '}
|
||||||
|
</Typography.Link>
|
||||||
|
<Typography.Text>{item.entryTags?.author}. </Typography.Text>
|
||||||
|
<Typography.Text>[{item.entryTags?.year}] </Typography.Text>
|
||||||
|
{item.entryTags?.journal && (
|
||||||
|
<Typography.Text>.({item.entryTags?.journal})</Typography.Text>
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ol>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BibTeXViewer;
|
||||||
@@ -18,7 +18,7 @@ const CopyButton: React.FC<CopyButtonProps> = ({
|
|||||||
text,
|
text,
|
||||||
disabled,
|
disabled,
|
||||||
type = 'text',
|
type = 'text',
|
||||||
shape = 'circle',
|
shape = 'default',
|
||||||
fontSize = '14px',
|
fontSize = '14px',
|
||||||
style,
|
style,
|
||||||
size = 'middle'
|
size = 'middle'
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { throttle } from 'echarts/core';
|
import { throttle } from 'lodash';
|
||||||
import { useCallback, useEffect, useRef } from 'react';
|
import { useCallback, useEffect, useRef } from 'react';
|
||||||
import echarts, { ECOption } from '.';
|
import echarts, { ECOption } from '.';
|
||||||
|
|
||||||
@@ -9,6 +9,8 @@ const Chart: React.FC<{
|
|||||||
}> = ({ options, width, height }) => {
|
}> = ({ options, width, height }) => {
|
||||||
const container = useRef<HTMLDivElement>(null);
|
const container = useRef<HTMLDivElement>(null);
|
||||||
const chart = useRef<echarts.EChartsType>();
|
const chart = useRef<echarts.EChartsType>();
|
||||||
|
const resizeable = useRef(false);
|
||||||
|
const resizeObserver = useRef<ResizeObserver>();
|
||||||
|
|
||||||
const init = useCallback(() => {
|
const init = useCallback(() => {
|
||||||
if (container.current) {
|
if (container.current) {
|
||||||
@@ -30,27 +32,48 @@ const Chart: React.FC<{
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (container.current) {
|
||||||
init();
|
init();
|
||||||
|
}
|
||||||
return () => {
|
return () => {
|
||||||
chart.current?.dispose();
|
chart.current?.dispose();
|
||||||
};
|
};
|
||||||
}, [init]);
|
}, [init]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
resizeable.current = false;
|
||||||
resize();
|
resize();
|
||||||
setOption(options);
|
setOption(options);
|
||||||
}, [options]);
|
}, [options]);
|
||||||
|
|
||||||
// resize on window resize
|
useEffect(() => {
|
||||||
|
let timer: any = null;
|
||||||
|
timer = setTimeout(() => {
|
||||||
|
resizeable.current = true;
|
||||||
|
}, 300);
|
||||||
|
return () => {
|
||||||
|
clearTimeout(timer);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleResize = throttle(() => {
|
const handleResize = throttle(() => {
|
||||||
resize();
|
if (resizeable.current) {
|
||||||
|
chart.current?.resize();
|
||||||
|
}
|
||||||
}, 100);
|
}, 100);
|
||||||
window.addEventListener('resize', handleResize);
|
|
||||||
|
if (container.current) {
|
||||||
|
resizeObserver.current = new ResizeObserver(handleResize);
|
||||||
|
|
||||||
|
resizeObserver.current.observe(container.current);
|
||||||
|
}
|
||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener('resize', handleResize);
|
if (container.current) {
|
||||||
|
resizeObserver.current?.unobserve(container.current);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}, [resize]);
|
}, []);
|
||||||
|
|
||||||
return <div ref={container} style={{ width: width, height }}></div>;
|
return <div ref={container} style={{ width: width, height }}></div>;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import type {
|
import type {
|
||||||
// 系列类型的定义后缀都为 SeriesOption
|
|
||||||
BarSeriesOption,
|
BarSeriesOption,
|
||||||
GaugeSeriesOption,
|
GaugeSeriesOption,
|
||||||
LineSeriesOption
|
LineSeriesOption
|
||||||
@@ -8,18 +7,16 @@ import { BarChart, GaugeChart, LineChart } from 'echarts/charts';
|
|||||||
import type {
|
import type {
|
||||||
DatasetComponentOption,
|
DatasetComponentOption,
|
||||||
GridComponentOption,
|
GridComponentOption,
|
||||||
// 组件类型的定义后缀都为 ComponentOption
|
|
||||||
TitleComponentOption,
|
TitleComponentOption,
|
||||||
TooltipComponentOption
|
TooltipComponentOption
|
||||||
} from 'echarts/components';
|
} from 'echarts/components';
|
||||||
import {
|
import {
|
||||||
// 数据集组件
|
|
||||||
DatasetComponent,
|
DatasetComponent,
|
||||||
GridComponent,
|
GridComponent,
|
||||||
LegendComponent,
|
LegendComponent,
|
||||||
TitleComponent,
|
TitleComponent,
|
||||||
TooltipComponent,
|
TooltipComponent,
|
||||||
// 内置数据转换器组件 (filter, sort)
|
// (filter, sort)
|
||||||
TransformComponent
|
TransformComponent
|
||||||
} from 'echarts/components';
|
} from 'echarts/components';
|
||||||
import type { ComposeOption } from 'echarts/core';
|
import type { ComposeOption } from 'echarts/core';
|
||||||
@@ -27,7 +24,6 @@ import * as echarts from 'echarts/core';
|
|||||||
import { LabelLayout, UniversalTransition } from 'echarts/features';
|
import { LabelLayout, UniversalTransition } from 'echarts/features';
|
||||||
import { CanvasRenderer } from 'echarts/renderers';
|
import { CanvasRenderer } from 'echarts/renderers';
|
||||||
|
|
||||||
// 通过 ComposeOption 来组合出一个只有必须组件和图表的 Option 类型
|
|
||||||
type ECOption = ComposeOption<
|
type ECOption = ComposeOption<
|
||||||
| BarSeriesOption
|
| BarSeriesOption
|
||||||
| LineSeriesOption
|
| LineSeriesOption
|
||||||
@@ -38,7 +34,7 @@ type ECOption = ComposeOption<
|
|||||||
| GaugeSeriesOption
|
| GaugeSeriesOption
|
||||||
>;
|
>;
|
||||||
|
|
||||||
// 注册必须的组件
|
// register components and charts
|
||||||
echarts.use([
|
echarts.use([
|
||||||
LegendComponent,
|
LegendComponent,
|
||||||
TitleComponent,
|
TitleComponent,
|
||||||
|
|||||||
@@ -1,10 +1,16 @@
|
|||||||
.editor-wrap {
|
.editor-wrap {
|
||||||
border-radius: var(--border-radius-mini);
|
border-radius: var(--border-radius-mini);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
font-size: 0;
|
||||||
|
|
||||||
|
.code-pre {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.editor-header {
|
.editor-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
padding: 6px 10px;
|
height: 40px;
|
||||||
|
padding: 0 10px;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background-color: rgb(56, 56, 56);
|
background-color: rgb(56, 56, 56);
|
||||||
@@ -12,6 +18,7 @@
|
|||||||
.ant-select-selector {
|
.ant-select-selector {
|
||||||
background-color: rgba(255, 255, 255, 9%) !important;
|
background-color: rgba(255, 255, 255, 9%) !important;
|
||||||
color: rgba(255, 255, 255, 70%);
|
color: rgba(255, 255, 255, 70%);
|
||||||
|
border-radius: var(--border-radius-2px);
|
||||||
|
|
||||||
.ant-select-selection-item {
|
.ant-select-selection-item {
|
||||||
color: rgba(255, 255, 255, 70%);
|
color: rgba(255, 255, 255, 70%);
|
||||||
|
|||||||
@@ -10,6 +10,11 @@ interface EditorwrapProps {
|
|||||||
copyText: string;
|
copyText: string;
|
||||||
defaultValue?: string;
|
defaultValue?: string;
|
||||||
langOptions?: { label: string; value: string }[];
|
langOptions?: { label: string; value: string }[];
|
||||||
|
styles?: {
|
||||||
|
wrapper?: React.CSSProperties;
|
||||||
|
header?: React.CSSProperties;
|
||||||
|
content?: React.CSSProperties;
|
||||||
|
};
|
||||||
onChangeLang?: (value: string) => void;
|
onChangeLang?: (value: string) => void;
|
||||||
}
|
}
|
||||||
const EditorWrap: React.FC<EditorwrapProps> = ({
|
const EditorWrap: React.FC<EditorwrapProps> = ({
|
||||||
@@ -19,6 +24,7 @@ const EditorWrap: React.FC<EditorwrapProps> = ({
|
|||||||
langOptions,
|
langOptions,
|
||||||
onChangeLang,
|
onChangeLang,
|
||||||
defaultValue,
|
defaultValue,
|
||||||
|
styles = {},
|
||||||
showHeader = true
|
showHeader = true
|
||||||
}) => {
|
}) => {
|
||||||
const handleChangeLang = (value: string) => {
|
const handleChangeLang = (value: string) => {
|
||||||
@@ -52,7 +58,7 @@ const EditorWrap: React.FC<EditorwrapProps> = ({
|
|||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<div className="editor-wrap">
|
<div className="editor-wrap" style={{ ...styles.wrapper }}>
|
||||||
{renderHeader()}
|
{renderHeader()}
|
||||||
<div className="editor-content">{children}</div>
|
<div className="editor-content">{children}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
import Editor from '@monaco-editor/react';
|
||||||
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
|
import EditorWrap from '../editor-wrap';
|
||||||
|
|
||||||
|
interface ViewerProps {
|
||||||
|
lang: string;
|
||||||
|
defaultLang?: string;
|
||||||
|
langOptions?: Global.BaseOption<string>[];
|
||||||
|
config?: any;
|
||||||
|
value: string;
|
||||||
|
height?: string | number;
|
||||||
|
theme?: string;
|
||||||
|
showHeader?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ViewerEditor: React.FC<ViewerProps> = (props) => {
|
||||||
|
const editorRef = useRef<any>(null);
|
||||||
|
const {
|
||||||
|
lang,
|
||||||
|
value,
|
||||||
|
config,
|
||||||
|
langOptions,
|
||||||
|
defaultLang,
|
||||||
|
height = 380,
|
||||||
|
showHeader
|
||||||
|
} = props;
|
||||||
|
const [langType, setLangType] = useState(defaultLang);
|
||||||
|
|
||||||
|
const handleBeforeMount = (monaco: any) => {
|
||||||
|
monaco.languages.typescript.javascriptDefaults.setDiagnosticsOptions({
|
||||||
|
noSemanticValidation: false,
|
||||||
|
noSyntaxValidation: false,
|
||||||
|
diagnosticCodesToIgnore: [80001]
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEditorDidMount = (editor: any, monaco: any) => {
|
||||||
|
editorRef.current = editor;
|
||||||
|
console.log('loaded====', editor, monaco);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOnChangeLang = (value: string) => {
|
||||||
|
setLangType(value);
|
||||||
|
};
|
||||||
|
const formatCode = () => {
|
||||||
|
if (editorRef.current) {
|
||||||
|
setTimeout(() => {
|
||||||
|
editorRef.current
|
||||||
|
?.getAction?.('editor.action.formatDocument')
|
||||||
|
?.run()
|
||||||
|
.then(() => {
|
||||||
|
console.log('format success');
|
||||||
|
});
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
formatCode();
|
||||||
|
setTimeout(() => {
|
||||||
|
const lineCount = editorRef.current?.getModel().getLineCount(); // 获取总行数
|
||||||
|
editorRef.current?.revealLine(lineCount); // 滚动到最后一行
|
||||||
|
}, 100); // 可以调整延时,确保编辑器完全加载
|
||||||
|
}, [value]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<EditorWrap
|
||||||
|
copyText={value}
|
||||||
|
langOptions={langOptions}
|
||||||
|
defaultValue={lang}
|
||||||
|
showHeader={showHeader}
|
||||||
|
onChangeLang={handleOnChangeLang}
|
||||||
|
>
|
||||||
|
<Editor
|
||||||
|
height={height}
|
||||||
|
theme="vs-dark"
|
||||||
|
className="monaco-editor"
|
||||||
|
defaultLanguage={defaultLang}
|
||||||
|
language={langType}
|
||||||
|
value={value}
|
||||||
|
options={config}
|
||||||
|
beforeMount={handleBeforeMount}
|
||||||
|
onMount={handleEditorDidMount}
|
||||||
|
/>
|
||||||
|
</EditorWrap>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default React.memo(ViewerEditor);
|
||||||
@@ -12,6 +12,7 @@ const Footer: React.FC = () => {
|
|||||||
const showVersion = () => {
|
const showVersion = () => {
|
||||||
Modal.info({
|
Modal.info({
|
||||||
...modalConfig,
|
...modalConfig,
|
||||||
|
width: 460,
|
||||||
content: <VersionInfo intl={intl} />
|
content: <VersionInfo intl={intl} />
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,12 +8,21 @@ interface CodeViewerProps {
|
|||||||
autodetect?: boolean;
|
autodetect?: boolean;
|
||||||
ignoreIllegals?: boolean;
|
ignoreIllegals?: boolean;
|
||||||
copyable?: boolean;
|
copyable?: boolean;
|
||||||
|
height?: string | number;
|
||||||
}
|
}
|
||||||
const DarkViewer: React.FC<CodeViewerProps> = (props) => {
|
const DarkViewer: React.FC<CodeViewerProps> = (props) => {
|
||||||
const { code, lang, autodetect, ignoreIllegals, copyable } = props || {};
|
const {
|
||||||
|
code,
|
||||||
|
lang,
|
||||||
|
autodetect,
|
||||||
|
ignoreIllegals,
|
||||||
|
copyable,
|
||||||
|
height = 'auto'
|
||||||
|
} = props || {};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CodeViewer
|
<CodeViewer
|
||||||
|
height={height}
|
||||||
code={code}
|
code={code}
|
||||||
lang={lang}
|
lang={lang}
|
||||||
theme="dark"
|
theme="dark"
|
||||||
|
|||||||
@@ -8,12 +8,21 @@ interface CodeViewerProps {
|
|||||||
autodetect?: boolean;
|
autodetect?: boolean;
|
||||||
ignoreIllegals?: boolean;
|
ignoreIllegals?: boolean;
|
||||||
copyable?: boolean;
|
copyable?: boolean;
|
||||||
|
height?: string | number;
|
||||||
}
|
}
|
||||||
const LightViewer: React.FC<CodeViewerProps> = (props) => {
|
const LightViewer: React.FC<CodeViewerProps> = (props) => {
|
||||||
const { code, lang, autodetect, ignoreIllegals, copyable } = props || {};
|
const {
|
||||||
|
code,
|
||||||
|
lang,
|
||||||
|
autodetect,
|
||||||
|
ignoreIllegals,
|
||||||
|
copyable,
|
||||||
|
height = 'auto'
|
||||||
|
} = props || {};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CodeViewer
|
<CodeViewer
|
||||||
|
height={height}
|
||||||
code={code}
|
code={code}
|
||||||
lang={lang}
|
lang={lang}
|
||||||
theme="light"
|
theme="light"
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ interface CodeViewerProps {
|
|||||||
autodetect?: boolean;
|
autodetect?: boolean;
|
||||||
ignoreIllegals?: boolean;
|
ignoreIllegals?: boolean;
|
||||||
copyable?: boolean;
|
copyable?: boolean;
|
||||||
|
height?: string | number;
|
||||||
theme?: 'light' | 'dark';
|
theme?: 'light' | 'dark';
|
||||||
}
|
}
|
||||||
const CodeViewer: React.FC<CodeViewerProps> = (props) => {
|
const CodeViewer: React.FC<CodeViewerProps> = (props) => {
|
||||||
@@ -18,7 +19,8 @@ const CodeViewer: React.FC<CodeViewerProps> = (props) => {
|
|||||||
lang,
|
lang,
|
||||||
autodetect = true,
|
autodetect = true,
|
||||||
ignoreIllegals = true,
|
ignoreIllegals = true,
|
||||||
copyable = true
|
copyable = true,
|
||||||
|
height = 'auto'
|
||||||
} = props || {};
|
} = props || {};
|
||||||
|
|
||||||
const highlightedCode = useMemo(() => {
|
const highlightedCode = useMemo(() => {
|
||||||
@@ -58,14 +60,24 @@ const CodeViewer: React.FC<CodeViewerProps> = (props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<pre
|
<pre
|
||||||
className={classNames('code-pre', {
|
className={classNames(
|
||||||
|
'code-pre custome-scrollbar custom-scrollbar-horizontal ',
|
||||||
|
{
|
||||||
dark: props.theme === 'dark',
|
dark: props.theme === 'dark',
|
||||||
light: props.theme === 'light',
|
light: props.theme === 'light',
|
||||||
copyable: copyable
|
copyable: copyable
|
||||||
})}
|
}
|
||||||
|
)}
|
||||||
|
style={{
|
||||||
|
height: height
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<code
|
<code
|
||||||
className={highlightedCode.className}
|
style={{ minHeight: height }}
|
||||||
|
className={classNames(highlightedCode.className, {
|
||||||
|
dark: props.theme === 'dark',
|
||||||
|
light: props.theme === 'light'
|
||||||
|
})}
|
||||||
dangerouslySetInnerHTML={{
|
dangerouslySetInnerHTML={{
|
||||||
__html: highlightedCode.value
|
__html: highlightedCode.value
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import React from 'react';
|
||||||
import CodeViewerDark from './code-viewer-dark';
|
import CodeViewerDark from './code-viewer-dark';
|
||||||
import CodeViewerLight from './code-viewer-light';
|
import CodeViewerLight from './code-viewer-light';
|
||||||
import './styles/index.less';
|
import './styles/index.less';
|
||||||
@@ -7,18 +8,35 @@ const HighlightCode: React.FC<{
|
|||||||
lang?: string;
|
lang?: string;
|
||||||
copyable?: boolean;
|
copyable?: boolean;
|
||||||
theme?: 'light' | 'dark';
|
theme?: 'light' | 'dark';
|
||||||
|
height?: string | number;
|
||||||
}> = (props) => {
|
}> = (props) => {
|
||||||
const { code, lang = 'bash', copyable = true, theme = 'dark' } = props;
|
const {
|
||||||
|
code,
|
||||||
|
lang = 'bash',
|
||||||
|
copyable = true,
|
||||||
|
theme = 'dark',
|
||||||
|
height = 'auto'
|
||||||
|
} = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="high-light-wrapper">
|
<div className="high-light-wrapper hj-wrapper">
|
||||||
{theme === 'dark' ? (
|
{theme === 'dark' ? (
|
||||||
<CodeViewerDark lang={lang} code={code} copyable={copyable} />
|
<CodeViewerDark
|
||||||
|
lang={lang}
|
||||||
|
code={code}
|
||||||
|
copyable={copyable}
|
||||||
|
height={height}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<CodeViewerLight lang={lang} code={code} copyable={copyable} />
|
<CodeViewerLight
|
||||||
|
lang={lang}
|
||||||
|
code={code}
|
||||||
|
copyable={copyable}
|
||||||
|
height={height}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default HighlightCode;
|
export default React.memo(HighlightCode);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
.high-light-wrapper {
|
.high-light-wrapper {
|
||||||
text-align: left;
|
text-align: left;
|
||||||
|
font-size: var(--font-size-base);
|
||||||
|
|
||||||
.hljs {
|
.hljs {
|
||||||
font-weight: var(--font-weight-normal);
|
font-weight: var(--font-weight-normal);
|
||||||
@@ -7,21 +8,35 @@
|
|||||||
padding-block: 1.2em;
|
padding-block: 1.2em;
|
||||||
|
|
||||||
&::-webkit-scrollbar {
|
&::-webkit-scrollbar {
|
||||||
height: 8px;
|
height: var(--scrollbar-size);
|
||||||
}
|
}
|
||||||
|
|
||||||
&::-webkit-scrollbar-thumb {
|
&::-webkit-scrollbar-thumb {
|
||||||
background-color: rgba(211, 211, 220, 50%);
|
background-color: transparent;
|
||||||
border-radius: 6px;
|
border-radius: 4px;
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: rgba(190, 190, 190, 100%);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&::-webkit-scrollbar-track {
|
&::-webkit-scrollbar-track {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.light {
|
||||||
|
&:hover {
|
||||||
|
&::-webkit-scrollbar-thumb {
|
||||||
|
background-color: var(--color-scrollbar-thumb);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.dark {
|
||||||
|
&:hover {
|
||||||
|
&::-webkit-scrollbar-thumb {
|
||||||
|
background-color: var(--scrollbar-handle-light-bg);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.code-pre {
|
.code-pre {
|
||||||
@@ -29,6 +44,10 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
border-radius: var(--border-radius-mini);
|
border-radius: var(--border-radius-mini);
|
||||||
|
|
||||||
|
code {
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
&.copyable {
|
&.copyable {
|
||||||
padding-inline: 12px 32px;
|
padding-inline: 12px 32px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { createFromIconfontCN } from '@ant-design/icons';
|
import { createFromIconfontCN } from '@ant-design/icons';
|
||||||
|
|
||||||
const IconFont = createFromIconfontCN({
|
const IconFont = createFromIconfontCN({
|
||||||
scriptUrl: '//at.alicdn.com/t/c/font_4613488_3n3aubqzylv.js'
|
scriptUrl: '//at.alicdn.com/t/c/font_4613488_l9554igzbh.js'
|
||||||
});
|
});
|
||||||
|
|
||||||
export default IconFont;
|
export default IconFont;
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ const LabelItem: React.FC<LabelItemProps> = ({
|
|||||||
// has duplicate key
|
// has duplicate key
|
||||||
const duplicates = _.filter(
|
const duplicates = _.filter(
|
||||||
labelList,
|
labelList,
|
||||||
(item: Global.BaseListItem) => val && val === item.key
|
(item: Global.BaseListItem<string>) => val && val === item.key
|
||||||
);
|
);
|
||||||
if (duplicates.length > 1) {
|
if (duplicates.length > 1) {
|
||||||
setOpen(true);
|
setOpen(true);
|
||||||
@@ -77,7 +77,8 @@ const LabelItem: React.FC<LabelItemProps> = ({
|
|||||||
title={intl.formatMessage({ id: 'resources.table.key.tips' })}
|
title={intl.formatMessage({ id: 'resources.table.key.tips' })}
|
||||||
>
|
>
|
||||||
<SealInput.Input
|
<SealInput.Input
|
||||||
label="Key"
|
checkStatus="success"
|
||||||
|
label={intl.formatMessage({ id: 'common.input.key' })}
|
||||||
value={label.key}
|
value={label.key}
|
||||||
onChange={handleOnKeyChange}
|
onChange={handleOnKeyChange}
|
||||||
onBlur={handleKeyOnBlur}
|
onBlur={handleKeyOnBlur}
|
||||||
@@ -89,7 +90,8 @@ const LabelItem: React.FC<LabelItemProps> = ({
|
|||||||
<div className="label-value">
|
<div className="label-value">
|
||||||
{valueAddon ?? (
|
{valueAddon ?? (
|
||||||
<SealInput.Input
|
<SealInput.Input
|
||||||
label="Value"
|
checkStatus={label.value ? 'success' : ''}
|
||||||
|
label={intl.formatMessage({ id: 'common.input.value' })}
|
||||||
value={label.value}
|
value={label.value}
|
||||||
onChange={handleOnValueChange}
|
onChange={handleOnValueChange}
|
||||||
></SealInput.Input>
|
></SealInput.Input>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
border: 1px solid var(--ant-color-border);
|
border: 1px solid var(--ant-color-border);
|
||||||
border-radius: var(--border-radius-base);
|
border-radius: var(--border-radius-base);
|
||||||
display: flex;
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
||||||
:global {
|
:global {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ const Wrapper: React.FC<{
|
|||||||
label?: string;
|
label?: string;
|
||||||
description?: React.ReactNode;
|
description?: React.ReactNode;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}> = ({ children, label, description }) => {
|
}> = ({ children, label, description, ...rest }) => {
|
||||||
return (
|
return (
|
||||||
<div className={styles['wrapper']}>
|
<div className={styles['wrapper']}>
|
||||||
{label && (
|
{label && (
|
||||||
@@ -14,7 +14,9 @@ const Wrapper: React.FC<{
|
|||||||
<LabelInfo label={label} description={description}></LabelInfo>
|
<LabelInfo label={label} description={description}></LabelInfo>
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{children}
|
{React.isValidElement(children)
|
||||||
|
? React.cloneElement(children, { ...rest })
|
||||||
|
: children}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,99 @@
|
|||||||
|
import AutoComplete from '@/components/seal-form/auto-complete';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
interface HintInputProps {
|
||||||
|
value: string;
|
||||||
|
label?: string;
|
||||||
|
onChange: (value: string) => void;
|
||||||
|
placeholder?: string;
|
||||||
|
sourceOptions?: Global.HintOptions[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const matchReg = /[^=]+=[^=]*$/;
|
||||||
|
|
||||||
|
const HintInput: React.FC<HintInputProps> = (props) => {
|
||||||
|
const { value, label, onChange, sourceOptions } = props;
|
||||||
|
const cursorPosRef = React.useRef(0);
|
||||||
|
const contextBeforeCursorRef = React.useRef('');
|
||||||
|
const [options, setOptions] = React.useState<
|
||||||
|
Array<Global.BaseOption<string>>
|
||||||
|
>([]);
|
||||||
|
|
||||||
|
const generateOptions = (context: string) => {
|
||||||
|
if (!context) {
|
||||||
|
setOptions(sourceOptions || []);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const match = context.match(matchReg);
|
||||||
|
if (!match) {
|
||||||
|
const list = _.filter(sourceOptions, (item: Global.HintOptions) =>
|
||||||
|
item.label.includes(context)
|
||||||
|
);
|
||||||
|
setOptions(list);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const [key, value] = _.split(match[0], '=');
|
||||||
|
const data = _.find(
|
||||||
|
sourceOptions,
|
||||||
|
(item: Global.HintOptions) => item.label === key
|
||||||
|
);
|
||||||
|
if (!data) {
|
||||||
|
setOptions([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const list = _.filter(data.opts, (item: Global.BaseOption<string>) =>
|
||||||
|
item.label.includes(value)
|
||||||
|
);
|
||||||
|
setOptions(list);
|
||||||
|
};
|
||||||
|
|
||||||
|
const replaceLastEqual = (value: string) => {
|
||||||
|
const matchStr = contextBeforeCursorRef.current.match(matchReg);
|
||||||
|
if (matchStr) {
|
||||||
|
const arr = _.split(matchStr[0], '=');
|
||||||
|
onChange(`${arr[0]}=${value}`);
|
||||||
|
} else {
|
||||||
|
onChange(value?.trim());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getContextBeforeCursor = _.debounce((e: any) => {
|
||||||
|
cursorPosRef.current = e.target.selectionStart;
|
||||||
|
contextBeforeCursorRef.current = e.target.value.slice(
|
||||||
|
0,
|
||||||
|
cursorPosRef.current
|
||||||
|
);
|
||||||
|
generateOptions(contextBeforeCursorRef.current);
|
||||||
|
}, 100);
|
||||||
|
|
||||||
|
const handleInput = (e: any) => {
|
||||||
|
getContextBeforeCursor(e);
|
||||||
|
onChange(e.target.value?.trim());
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOnChange = (value: string) => {
|
||||||
|
onChange(value?.trim());
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOnSelect = (value: string) => {
|
||||||
|
replaceLastEqual(value);
|
||||||
|
setOptions([]);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AutoComplete
|
||||||
|
placeholder={props.placeholder}
|
||||||
|
defaultActiveFirstOption={true}
|
||||||
|
value={value}
|
||||||
|
onInput={handleInput}
|
||||||
|
onSelect={handleOnSelect}
|
||||||
|
onFocus={getContextBeforeCursor}
|
||||||
|
label={label}
|
||||||
|
options={options}
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default React.memo(HintInput);
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
import { PlusOutlined } from '@ant-design/icons';
|
||||||
|
import { useIntl } from '@umijs/max';
|
||||||
|
import { Button } from 'antd';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import React from 'react';
|
||||||
|
import Wrapper from '../label-selector/wrapper';
|
||||||
|
import ListItem from './list-item';
|
||||||
|
|
||||||
|
interface ListInputProps {
|
||||||
|
dataList: string[];
|
||||||
|
label: string;
|
||||||
|
description?: React.ReactNode;
|
||||||
|
btnText?: string;
|
||||||
|
options?: Global.HintOptions[];
|
||||||
|
placeholder?: string;
|
||||||
|
onChange: (data: string[]) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ListInput: React.FC<ListInputProps> = (props) => {
|
||||||
|
const intl = useIntl();
|
||||||
|
const { dataList, label, description, onChange, btnText, options } = props;
|
||||||
|
const [list, setList] = React.useState<{ value: string; uid: number }[]>([]);
|
||||||
|
const countRef = React.useRef(0);
|
||||||
|
const buttonRef = React.useRef<HTMLButtonElement>(null);
|
||||||
|
|
||||||
|
const updateCountRef = () => {
|
||||||
|
countRef.current = countRef.current + 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOnRemove = (index: number) => {
|
||||||
|
const values = _.cloneDeep(list);
|
||||||
|
values.splice(index, 1);
|
||||||
|
const valueList = _.map(values, 'value').filter((val: string) => !!val);
|
||||||
|
setList(values);
|
||||||
|
onChange(valueList);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOnChange = (value: string, index: number) => {
|
||||||
|
const values = _.cloneDeep(list);
|
||||||
|
values[index].value = value;
|
||||||
|
const valueList = _.map(values, 'value').filter((val: string) => !!val);
|
||||||
|
setList(values);
|
||||||
|
onChange(valueList);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOnAdd = () => {
|
||||||
|
updateCountRef();
|
||||||
|
const values = _.cloneDeep(list);
|
||||||
|
values.push({
|
||||||
|
value: '',
|
||||||
|
uid: countRef.current
|
||||||
|
});
|
||||||
|
setList(values);
|
||||||
|
setTimeout(() => {
|
||||||
|
buttonRef.current?.scrollIntoView?.({ behavior: 'smooth' });
|
||||||
|
}, 100);
|
||||||
|
};
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const valueList = _.map(list, 'value').filter((val: string) => !!val);
|
||||||
|
if (!_.isEqual(valueList, dataList)) {
|
||||||
|
const values = _.map(dataList, (value: string) => {
|
||||||
|
updateCountRef();
|
||||||
|
return {
|
||||||
|
value,
|
||||||
|
uid: countRef.current
|
||||||
|
};
|
||||||
|
});
|
||||||
|
setList(values);
|
||||||
|
}
|
||||||
|
}, [dataList]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Wrapper label={label} description={description}>
|
||||||
|
<>
|
||||||
|
{_.map(list, (item: any, index: number) => {
|
||||||
|
return (
|
||||||
|
<ListItem
|
||||||
|
placeholder={props.placeholder}
|
||||||
|
options={options}
|
||||||
|
key={item.uid}
|
||||||
|
value={item.value}
|
||||||
|
onRemove={() => handleOnRemove(index)}
|
||||||
|
onChange={(val) => handleOnChange(val, index)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
<div className="flex justify-center">
|
||||||
|
<Button
|
||||||
|
ref={buttonRef}
|
||||||
|
type="text"
|
||||||
|
block
|
||||||
|
style={{
|
||||||
|
marginTop: 16,
|
||||||
|
backgroundColor: 'var(--ant-color-fill-secondary)'
|
||||||
|
}}
|
||||||
|
onClick={handleOnAdd}
|
||||||
|
>
|
||||||
|
<PlusOutlined className="font-size-14" />{' '}
|
||||||
|
{intl.formatMessage({
|
||||||
|
id: btnText
|
||||||
|
})}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
</Wrapper>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default React.memo(ListInput);
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
// import AutoComplete from '@/components/seal-form/auto-complete';
|
||||||
|
import { MinusOutlined } from '@ant-design/icons';
|
||||||
|
import { Button } from 'antd';
|
||||||
|
import React from 'react';
|
||||||
|
import HintInput from './hint-input';
|
||||||
|
import './styles/list-item.less';
|
||||||
|
|
||||||
|
interface LabelItemProps {
|
||||||
|
onRemove: () => void;
|
||||||
|
onChange: (value: string) => void;
|
||||||
|
value: string;
|
||||||
|
label?: string;
|
||||||
|
placeholder?: string;
|
||||||
|
options?: Global.HintOptions[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const ListItem: React.FC<LabelItemProps> = (props) => {
|
||||||
|
const { onRemove, onChange, label, value, options } = props;
|
||||||
|
|
||||||
|
const handleOnChange = (value: any) => {
|
||||||
|
onChange(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="list-item">
|
||||||
|
<HintInput
|
||||||
|
value={value}
|
||||||
|
onChange={handleOnChange}
|
||||||
|
label={label}
|
||||||
|
sourceOptions={options}
|
||||||
|
placeholder={props.placeholder}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
className="btn"
|
||||||
|
type="default"
|
||||||
|
shape="circle"
|
||||||
|
icon={<MinusOutlined />}
|
||||||
|
onClick={onRemove}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default React.memo(ListItem);
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
.list-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-start;
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
|
||||||
|
.field-wrapper {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
.list-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-start;
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
|
||||||
|
.field-wrapper {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,30 @@
|
|||||||
.logs-viewer-wrap-w2 {
|
.logs-viewer-wrap-w2 {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.copy {
|
||||||
|
position: absolute;
|
||||||
|
top: 10px;
|
||||||
|
right: 10px;
|
||||||
|
z-index: 100;
|
||||||
|
|
||||||
|
button {
|
||||||
|
color: rgba(255, 255, 255, 70%);
|
||||||
|
background-color: rgba(71, 71, 71, 100%);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: rgba(255, 255, 255, 90%) !important;
|
||||||
|
background-color: rgba(71, 71, 71, 100%) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.wrap {
|
.wrap {
|
||||||
padding: 5px 0 5px 10px;
|
padding: 5px 0 5px 10px;
|
||||||
background-color: var(--color-logs-bg);
|
background-color: var(--color-logs-bg);
|
||||||
border-radius: var(--border-radius-mini);
|
border-radius: var(--border-radius-mini);
|
||||||
overflow: hidden;
|
overflow: auto;
|
||||||
|
font-family: monospace, Menlo, Courier, 'Courier New', Consolas, Monaco,
|
||||||
|
'Liberation Mono' !important;
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
@@ -14,7 +35,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.text {
|
.text {
|
||||||
height: 100%;
|
min-height: 22px;
|
||||||
}
|
}
|
||||||
|
|
||||||
color: var(--color-logs-text);
|
color: var(--color-logs-text);
|
||||||
@@ -26,14 +47,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.xterm {
|
.xterm {
|
||||||
height: 100% !important;
|
|
||||||
|
|
||||||
.xterm-viewport {
|
.xterm-viewport {
|
||||||
overflow-y: auto !important;
|
overflow-y: auto !important;
|
||||||
// custom scrollbar
|
|
||||||
&::-webkit-scrollbar {
|
&::-webkit-scrollbar {
|
||||||
width: 8px;
|
width: var(--scrollbar-size);
|
||||||
height: 6px;
|
height: var(--scrollbar-size);
|
||||||
}
|
}
|
||||||
|
|
||||||
&::-webkit-scrollbar-thumb {
|
&::-webkit-scrollbar-thumb {
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
import useSetChunkRequest from '@/hooks/use-chunk-request';
|
import useSetChunkFetch from '@/hooks/use-chunk-fetch';
|
||||||
import { FitAddon } from '@xterm/addon-fit';
|
import useOverlayScroller from '@/hooks/use-overlay-scroller';
|
||||||
import { Terminal } from '@xterm/xterm';
|
|
||||||
import '@xterm/xterm/css/xterm.css';
|
import '@xterm/xterm/css/xterm.css';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { memo, useCallback, useEffect, useRef, useState } from 'react';
|
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import './index.less';
|
import './index.less';
|
||||||
import useSize from './use-size';
|
import useParseAnsi from './parse-ansi';
|
||||||
|
|
||||||
interface LogsViewerProps {
|
interface LogsViewerProps {
|
||||||
height: number;
|
height: number;
|
||||||
@@ -15,38 +14,47 @@ interface LogsViewerProps {
|
|||||||
params?: object;
|
params?: object;
|
||||||
}
|
}
|
||||||
const LogsViewer: React.FC<LogsViewerProps> = (props) => {
|
const LogsViewer: React.FC<LogsViewerProps> = (props) => {
|
||||||
const { height, content, url } = props;
|
const { height, url } = props;
|
||||||
const { setChunkRequest } = useSetChunkRequest();
|
const {
|
||||||
|
initialize,
|
||||||
|
updateScrollerPosition,
|
||||||
|
generateInstance,
|
||||||
|
scrollEventElement,
|
||||||
|
instance,
|
||||||
|
initialized
|
||||||
|
} = useOverlayScroller({
|
||||||
|
theme: 'os-theme-light'
|
||||||
|
});
|
||||||
|
const { isClean, parseAnsi } = useParseAnsi();
|
||||||
|
const { setChunkFetch } = useSetChunkFetch();
|
||||||
const chunkRequedtRef = useRef<any>(null);
|
const chunkRequedtRef = useRef<any>(null);
|
||||||
const scroller = useRef<any>({});
|
const scroller = useRef<any>({});
|
||||||
const termRef = useRef<any>({});
|
const cacheDataRef = useRef<any>('');
|
||||||
const termwrapRef = useRef<any>({});
|
const uidRef = useRef<any>(0);
|
||||||
const fitAddonRef = useRef<any>({});
|
const [logs, setLogs] = useState<any[]>([]);
|
||||||
const cacheDataRef = useRef<any>(null);
|
const stopScroll = useRef(false);
|
||||||
const [logs, setLogs] = useState('');
|
|
||||||
const size = useSize(scroller);
|
|
||||||
|
|
||||||
const throttleScroll = _.debounce(() => {
|
const setId = () => {
|
||||||
termRef.current?.scrollToBottom?.();
|
uidRef.current += 1;
|
||||||
}, 100);
|
return uidRef.current;
|
||||||
|
|
||||||
const updateContent = useCallback(
|
|
||||||
_.throttle((data: string) => {
|
|
||||||
cacheDataRef.current = '';
|
|
||||||
termRef.current?.clear?.();
|
|
||||||
cacheDataRef.current = data;
|
|
||||||
termRef.current?.write?.(data);
|
|
||||||
}, 100),
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
const fitTerm = () => {
|
|
||||||
fitAddonRef.current?.fit?.();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const updateContent = useCallback(
|
||||||
|
(data: string) => {
|
||||||
|
if (isClean(data)) {
|
||||||
|
cacheDataRef.current = data;
|
||||||
|
} else {
|
||||||
|
cacheDataRef.current += data;
|
||||||
|
}
|
||||||
|
const res = parseAnsi(cacheDataRef.current, setId);
|
||||||
|
setLogs(res);
|
||||||
|
},
|
||||||
|
[setLogs, setId]
|
||||||
|
);
|
||||||
|
|
||||||
const createChunkConnection = async () => {
|
const createChunkConnection = async () => {
|
||||||
chunkRequedtRef.current?.current?.cancel?.();
|
chunkRequedtRef.current?.current?.abort?.();
|
||||||
chunkRequedtRef.current = setChunkRequest({
|
chunkRequedtRef.current = setChunkFetch({
|
||||||
url,
|
url,
|
||||||
params: {
|
params: {
|
||||||
...props.params,
|
...props.params,
|
||||||
@@ -57,65 +65,84 @@ const LogsViewer: React.FC<LogsViewerProps> = (props) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const initTerm = () => {
|
const debounceResetStopScroll = _.debounce(() => {
|
||||||
termRef.current?.dispose?.();
|
stopScroll.current = false;
|
||||||
termRef.current = new Terminal({
|
}, 30000);
|
||||||
lineHeight: 1.2,
|
|
||||||
fontSize: 13,
|
|
||||||
fontFamily:
|
|
||||||
"monospace,Menlo,Courier,'Courier New',Consolas,Monaco, 'Liberation Mono'",
|
|
||||||
disableStdin: true,
|
|
||||||
convertEol: true,
|
|
||||||
theme: {
|
|
||||||
background: '#1e1e1e',
|
|
||||||
foreground: 'rgba(255,255,255,0.8)'
|
|
||||||
},
|
|
||||||
cursorInactiveStyle: 'none',
|
|
||||||
smoothScrollDuration: 0
|
|
||||||
});
|
|
||||||
fitAddonRef.current = new FitAddon();
|
|
||||||
termRef.current.loadAddon(fitAddonRef.current);
|
|
||||||
termRef.current.open(termwrapRef.current);
|
|
||||||
|
|
||||||
// add event
|
const handleOnWheel = useCallback(
|
||||||
termRef.current.onLineFeed((e: any) => {
|
(e: any) => {
|
||||||
if (cacheDataRef.current) {
|
const scrollTop = scrollEventElement?.scrollTop;
|
||||||
throttleScroll();
|
const scrollHeight = scrollEventElement?.scrollHeight;
|
||||||
|
const clientHeight = scrollEventElement?.clientHeight;
|
||||||
|
if (scrollTop + clientHeight >= scrollHeight) {
|
||||||
|
stopScroll.current = false;
|
||||||
|
} else {
|
||||||
|
stopScroll.current = true;
|
||||||
}
|
}
|
||||||
});
|
debounceResetStopScroll();
|
||||||
};
|
},
|
||||||
|
[debounceResetStopScroll, scrollEventElement]
|
||||||
|
);
|
||||||
|
|
||||||
const handleResize = _.throttle(() => {
|
const debounceUpdateScrollerPosition = _.debounce(() => {
|
||||||
fitTerm();
|
generateInstance();
|
||||||
}, 100);
|
updateScrollerPosition(0);
|
||||||
|
}, 200);
|
||||||
|
|
||||||
|
const copyText = useMemo(() => {
|
||||||
|
if (!logs.length) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return logs?.map((item) => item.content).join('\n');
|
||||||
|
}, [logs]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
createChunkConnection();
|
createChunkConnection();
|
||||||
return () => {
|
return () => {
|
||||||
chunkRequedtRef.current?.current?.cancel?.();
|
chunkRequedtRef.current?.current?.abort?.();
|
||||||
};
|
};
|
||||||
}, [url, props.params]);
|
}, [url, props.params]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (termwrapRef.current) {
|
if (scroller.current) {
|
||||||
initTerm();
|
initialize(scroller.current);
|
||||||
}
|
}
|
||||||
return () => {
|
}, [scroller.current, initialize]);
|
||||||
termRef.current?.dispose?.();
|
|
||||||
};
|
|
||||||
}, [termwrapRef.current]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (size) {
|
if (logs.length && !stopScroll.current && instance) {
|
||||||
handleResize();
|
updateScrollerPosition(0);
|
||||||
|
} else if (logs.length && !stopScroll.current && scroller.current) {
|
||||||
|
if (!initialized) {
|
||||||
|
initialize(scroller.current);
|
||||||
}
|
}
|
||||||
}, [size]);
|
if (!instance) {
|
||||||
|
debounceUpdateScrollerPosition();
|
||||||
|
} else {
|
||||||
|
updateScrollerPosition(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [logs, stopScroll.current, instance, scroller.current]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="logs-viewer-wrap-w2">
|
<div className="logs-viewer-wrap-w2">
|
||||||
<div className="wrap" style={{ height: height }} ref={scroller}>
|
{/* <span className="copy">
|
||||||
|
<CopyButton text={copyText} type="text" size="small"></CopyButton>
|
||||||
|
</span> */}
|
||||||
|
<div
|
||||||
|
className="wrap"
|
||||||
|
style={{ height: height }}
|
||||||
|
ref={scroller}
|
||||||
|
onWheel={handleOnWheel}
|
||||||
|
>
|
||||||
<div className={classNames('content')}>
|
<div className={classNames('content')}>
|
||||||
<div className="text" ref={termwrapRef}></div>
|
{_.map(logs, (item: any, index: number) => {
|
||||||
|
return (
|
||||||
|
<div key={item.uid} className="text">
|
||||||
|
{item.content}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,153 @@
|
|||||||
|
import { useCallback, useRef } from 'react';
|
||||||
|
|
||||||
|
const controlSeqRegex = /\x1b\[(\d*);?(\d*)?([A-DJKHfm])/g;
|
||||||
|
|
||||||
|
const useParseAnsi = () => {
|
||||||
|
const lastIndex = useRef(0);
|
||||||
|
|
||||||
|
const removeBrackets = useCallback((str: string) => {
|
||||||
|
return str?.replace?.(/^\(…\)/, '');
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const isClean = useCallback((ansiStr: string) => {
|
||||||
|
let input = ansiStr.replace(/\r\n/g, '\n');
|
||||||
|
let match = controlSeqRegex.exec(input) || [];
|
||||||
|
const command = match?.[3];
|
||||||
|
const n = parseInt(match?.[1], 10) || 1;
|
||||||
|
return command === 'J' && n === 2;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const parseAnsi = useCallback((inputStr: string, setId: () => number) => {
|
||||||
|
let cursorRow = 0; // current row
|
||||||
|
let cursorCol = 0; // current column
|
||||||
|
// screen content array
|
||||||
|
let screen = [['']];
|
||||||
|
// replace carriage return and newline characters in the text
|
||||||
|
let input = inputStr.replace(/\r\n/g, '\n');
|
||||||
|
|
||||||
|
lastIndex.current = 0;
|
||||||
|
|
||||||
|
// handle the \r and \n characters in the text
|
||||||
|
const handleText = (text: string) => {
|
||||||
|
let processed = '';
|
||||||
|
for (let char of text) {
|
||||||
|
if (char === '\r') {
|
||||||
|
cursorCol = 0; // move to the beginning of the line
|
||||||
|
} else if (char === '\n') {
|
||||||
|
cursorRow++; // move to the next line
|
||||||
|
cursorCol = 0; // move to the beginning of the line
|
||||||
|
screen[cursorRow] = screen[cursorRow] || ['']; // create a new line if it does not exist
|
||||||
|
} else {
|
||||||
|
// add the character to the screen content array
|
||||||
|
screen[cursorRow][cursorCol] = char;
|
||||||
|
cursorCol++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return processed;
|
||||||
|
};
|
||||||
|
|
||||||
|
let output = ''; // output text
|
||||||
|
|
||||||
|
let match;
|
||||||
|
|
||||||
|
// ANSI color map
|
||||||
|
const colorMap: Record<string, string> = {
|
||||||
|
'30': 'black',
|
||||||
|
'31': 'red',
|
||||||
|
'32': 'green',
|
||||||
|
'33': 'yellow',
|
||||||
|
'34': 'blue',
|
||||||
|
'35': 'magenta',
|
||||||
|
'36': 'cyan',
|
||||||
|
'37': 'white'
|
||||||
|
};
|
||||||
|
|
||||||
|
let currentStyle = ''; // current text style
|
||||||
|
|
||||||
|
// match ANSI control characters
|
||||||
|
while ((match = controlSeqRegex.exec(input)) !== null) {
|
||||||
|
// handle text before the control character
|
||||||
|
let textBeforeControl = input.slice(lastIndex.current, match.index);
|
||||||
|
output += handleText(textBeforeControl); // add the processed text to the output
|
||||||
|
lastIndex.current = controlSeqRegex.lastIndex; // update the last index
|
||||||
|
|
||||||
|
const n = parseInt(match[1], 10) || 1;
|
||||||
|
const m = parseInt(match[2], 10) || 1;
|
||||||
|
const command = match[3];
|
||||||
|
console.log('command', {
|
||||||
|
command,
|
||||||
|
cursorRow,
|
||||||
|
n
|
||||||
|
});
|
||||||
|
|
||||||
|
// handle ANSI control characters
|
||||||
|
switch (command) {
|
||||||
|
case 'A': // up
|
||||||
|
cursorRow = Math.max(0, cursorRow - n);
|
||||||
|
if (cursorRow === 0) {
|
||||||
|
// screen = [['']];
|
||||||
|
// cursorCol = 0;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'B': // down
|
||||||
|
cursorRow += n;
|
||||||
|
break;
|
||||||
|
case 'C': // right
|
||||||
|
cursorCol += n;
|
||||||
|
break;
|
||||||
|
case 'D': // left
|
||||||
|
cursorCol = Math.max(0, cursorCol - n);
|
||||||
|
break;
|
||||||
|
case 'H': // move the cursor to the specified position (n, m)
|
||||||
|
cursorRow = Math.max(0, n - 1);
|
||||||
|
cursorCol = Math.max(0, m - 1);
|
||||||
|
break;
|
||||||
|
case 'J': // clear the screen
|
||||||
|
if (n === 2) {
|
||||||
|
console.log('clear====');
|
||||||
|
screen = [['']];
|
||||||
|
cursorRow = 0;
|
||||||
|
cursorCol = 0;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'm': // color
|
||||||
|
if (match[1] === '0') {
|
||||||
|
currentStyle = '';
|
||||||
|
} else if (colorMap[match[1]]) {
|
||||||
|
currentStyle = `color: ${colorMap[match[1]]};`;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if the row and column are within the screen content array
|
||||||
|
while (screen.length <= cursorRow) {
|
||||||
|
screen.push(['']);
|
||||||
|
}
|
||||||
|
while (screen[cursorRow].length <= cursorCol) {
|
||||||
|
screen[cursorRow].push('');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle the remaining text
|
||||||
|
output += handleText(input.slice(lastIndex.current));
|
||||||
|
|
||||||
|
let result = [];
|
||||||
|
for (let row = 0; row < screen.length; row++) {
|
||||||
|
let rowContent = screen[row].join('');
|
||||||
|
result.push({
|
||||||
|
content: removeBrackets(rowContent),
|
||||||
|
uid: setId()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
result.push({
|
||||||
|
content: output,
|
||||||
|
uid: setId()
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return { parseAnsi, isClean };
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useParseAnsi;
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
import { useRef, useState } from 'react';
|
||||||
|
|
||||||
|
const usePaseByLine = () => {
|
||||||
|
const [result, setResult] = useState<any[]>([]);
|
||||||
|
const uidRef = useRef(0);
|
||||||
|
|
||||||
|
const cursorRow = useRef(0); // current row
|
||||||
|
const cursorCol = useRef(0); // current column
|
||||||
|
// screen.current content array
|
||||||
|
const screen = useRef([['']]);
|
||||||
|
// replace carriage return and newline characters in the text
|
||||||
|
// let input = inputStr.replace(/\r\n/g, '\n')
|
||||||
|
|
||||||
|
const removeBrackets = (str: string) => {
|
||||||
|
return str?.replace?.(/^\(\.*?\)/, '');
|
||||||
|
};
|
||||||
|
|
||||||
|
const setId = () => {
|
||||||
|
uidRef.current += 1;
|
||||||
|
return uidRef.current;
|
||||||
|
};
|
||||||
|
|
||||||
|
// handle the \r and \n characters in the text
|
||||||
|
const handleText = (text: string) => {
|
||||||
|
for (let char of text) {
|
||||||
|
if (char === '\r') {
|
||||||
|
cursorCol.current = 0; // move to the beginning of the line
|
||||||
|
} else if (char === '\n') {
|
||||||
|
cursorRow.current += 1; // move to the next line
|
||||||
|
cursorCol.current = 0; // move to the beginning of the line
|
||||||
|
screen.current[cursorRow.current] = screen.current[
|
||||||
|
cursorRow.current
|
||||||
|
] || ['']; // create a new line if it does not exist
|
||||||
|
} else {
|
||||||
|
// add the character to the screen.current content array
|
||||||
|
screen.current[cursorRow.current][cursorCol.current] = char;
|
||||||
|
cursorCol.current += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ANSI
|
||||||
|
const controlSeqRegex = /\x1b\[(\d*);?(\d*)?([A-DJKHfm])/g;
|
||||||
|
|
||||||
|
// ANSI color map
|
||||||
|
const colorMap: Record<string, string> = {
|
||||||
|
'30': 'black',
|
||||||
|
'31': 'red',
|
||||||
|
'32': 'green',
|
||||||
|
'33': 'yellow',
|
||||||
|
'34': 'blue',
|
||||||
|
'35': 'magenta',
|
||||||
|
'36': 'cyan',
|
||||||
|
'37': 'white'
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateResult = () => {
|
||||||
|
let res = [];
|
||||||
|
for (let row = 0; row < screen.current.length; row++) {
|
||||||
|
console.log('screen.current[row]', screen.current[row]);
|
||||||
|
let rowContent = screen.current[row].join('');
|
||||||
|
res.push({
|
||||||
|
content: removeBrackets(rowContent),
|
||||||
|
uid: setId()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
console.log('res', res);
|
||||||
|
setResult(res);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLine = (line: string) => {
|
||||||
|
let match;
|
||||||
|
let lastIndex = 0;
|
||||||
|
let currentStyle = ''; // current text style
|
||||||
|
// match ANSI control characters
|
||||||
|
while ((match = controlSeqRegex.exec(line)) !== null) {
|
||||||
|
// handle text before the control character
|
||||||
|
let textBeforeControl = line.slice(lastIndex, match.index);
|
||||||
|
|
||||||
|
handleText(textBeforeControl); // add the processed text to the output
|
||||||
|
lastIndex = controlSeqRegex.lastIndex; // update the last index
|
||||||
|
|
||||||
|
const n = parseInt(match[1], 10) || 1;
|
||||||
|
const m = parseInt(match[2], 10) || 1;
|
||||||
|
const command = match[3];
|
||||||
|
|
||||||
|
// handle ANSI control characters
|
||||||
|
switch (command) {
|
||||||
|
case 'A': // up
|
||||||
|
cursorRow.current = Math.max(0, cursorRow.current - n);
|
||||||
|
break;
|
||||||
|
case 'B': // down
|
||||||
|
cursorRow.current += n;
|
||||||
|
break;
|
||||||
|
case 'C': // right
|
||||||
|
cursorCol.current += n;
|
||||||
|
break;
|
||||||
|
case 'D': // left
|
||||||
|
cursorCol.current = Math.max(0, cursorCol.current - n);
|
||||||
|
break;
|
||||||
|
case 'H': // move the cursor to the specified position (n, m)
|
||||||
|
cursorRow.current = Math.max(0, n - 1);
|
||||||
|
cursorCol.current = Math.max(0, m - 1);
|
||||||
|
break;
|
||||||
|
case 'J': // clear the screen.current
|
||||||
|
if (n === 2) {
|
||||||
|
screen.current = [['']];
|
||||||
|
cursorRow.current = 0;
|
||||||
|
cursorCol.current = 0;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'm': // color
|
||||||
|
if (match[1] === '0') {
|
||||||
|
currentStyle = '';
|
||||||
|
} else if (colorMap[match[1]]) {
|
||||||
|
currentStyle = `color: ${colorMap[match[1]]};`;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if the row and column are within the screen.current content array
|
||||||
|
while (screen.current.length <= cursorRow.current) {
|
||||||
|
screen.current.push(['']);
|
||||||
|
}
|
||||||
|
while (screen.current[cursorRow.current].length <= cursorCol.current) {
|
||||||
|
screen.current[cursorRow.current].push('');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
handleText(line.slice(lastIndex));
|
||||||
|
updateResult();
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
handleLine,
|
||||||
|
result
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default usePaseByLine;
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
.markdown-viewer {
|
||||||
|
font-family: var(--font-family) !important;
|
||||||
|
|
||||||
|
.hr {
|
||||||
|
border: none;
|
||||||
|
height: 1px;
|
||||||
|
background-color: var(--ant-color-split);
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4 {
|
||||||
|
font-weight: 700;
|
||||||
|
font-weight: var(--font-weight-bold);
|
||||||
|
font-size: var(--font-size-small);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hj-wrapper {
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
strong {
|
||||||
|
font-weight: var(--font-weight-bold);
|
||||||
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
margin-bottom: 0;
|
||||||
|
padding-left: 20px;
|
||||||
|
line-height: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
ol {
|
||||||
|
margin-bottom: 0;
|
||||||
|
padding-left: 20px;
|
||||||
|
line-height: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
a * {
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 1.2em;
|
||||||
|
|
||||||
|
th {
|
||||||
|
font-weight: var(--font-weight-bold);
|
||||||
|
line-height: 1.5;
|
||||||
|
padding-inline: 6px;
|
||||||
|
border-bottom: 1px solid var(--ant-color-split);
|
||||||
|
word-break: break-word;
|
||||||
|
text-align: left !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
td {
|
||||||
|
line-height: 1.5;
|
||||||
|
padding-inline: 6px;
|
||||||
|
border-bottom: 1px solid var(--ant-color-split);
|
||||||
|
word-break: break-word;
|
||||||
|
text-align: left !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
div {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
div {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
video {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
border-radius: var(--border-radius-base);
|
||||||
|
}
|
||||||
|
|
||||||
|
audio {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
border-radius: var(--border-radius-base);
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-token {
|
||||||
|
color: inherit;
|
||||||
|
font-size: inherit;
|
||||||
|
font-weight: inherit;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,26 +1,208 @@
|
|||||||
import { marked, Tokens } from 'marked';
|
import { EyeOutlined } from '@ant-design/icons';
|
||||||
import React from 'react';
|
import { Checkbox, Image, Typography } from 'antd';
|
||||||
|
import { unescape } from 'lodash';
|
||||||
|
import { TokensList, marked } from 'marked';
|
||||||
|
import React, { Fragment, useCallback, useEffect } from 'react';
|
||||||
|
import HighlightCode from '../highlight-code';
|
||||||
|
import './index.less';
|
||||||
|
|
||||||
|
const { Text, Link, Paragraph } = Typography;
|
||||||
|
|
||||||
interface MarkdownViewerProps {
|
interface MarkdownViewerProps {
|
||||||
content: string;
|
content: string;
|
||||||
height?: string;
|
height?: string;
|
||||||
|
theme?: 'light' | 'dark';
|
||||||
|
generateImgLink?: (src: string) => string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const MarkdownViewer: React.FC<MarkdownViewerProps> = ({
|
const MarkdownViewer: React.FC<MarkdownViewerProps> = ({
|
||||||
content,
|
content,
|
||||||
height = 'auto'
|
generateImgLink,
|
||||||
|
height = 'auto',
|
||||||
|
theme = 'light'
|
||||||
}) => {
|
}) => {
|
||||||
const renderer = new marked.Renderer();
|
const renderer = new marked.Renderer();
|
||||||
|
const tokens = marked.lexer(content);
|
||||||
|
const reDefineTypes = [
|
||||||
|
'code',
|
||||||
|
'link',
|
||||||
|
'hr',
|
||||||
|
'heading',
|
||||||
|
'paragraph',
|
||||||
|
'codespan',
|
||||||
|
'strong',
|
||||||
|
'text',
|
||||||
|
'image',
|
||||||
|
'em',
|
||||||
|
'list',
|
||||||
|
'list_item',
|
||||||
|
'br',
|
||||||
|
'html',
|
||||||
|
'escape',
|
||||||
|
'del',
|
||||||
|
'blockquote',
|
||||||
|
'checkbox'
|
||||||
|
// 'bibtex'
|
||||||
|
];
|
||||||
|
|
||||||
renderer.link = ({ href, title, text }: Tokens.Link) => {
|
// renderer.link = ({ href, title, text }) => {
|
||||||
return `<a href="${href}" title="${title || ''}" target="_blank" rel="noopener noreferrer">${text}</a>`;
|
// return `<a href="${href}" title="${title || ''}" target="_blank" rel="noopener noreferrer">${text}</a>`;
|
||||||
|
// };
|
||||||
|
|
||||||
|
const isValidURL = useCallback((url: string) => {
|
||||||
|
const pattern = /^(https?:\/\/|\/\/)([^\s/$.?#].[^\s]*)$/;
|
||||||
|
|
||||||
|
return pattern.test(url);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const generateImgSrc = useCallback(
|
||||||
|
(src: string | null) => {
|
||||||
|
if (!src) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
if (generateImgLink) {
|
||||||
|
return isValidURL(src) ? src : generateImgLink(src);
|
||||||
|
}
|
||||||
|
return src;
|
||||||
|
},
|
||||||
|
[generateImgLink]
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderItem = useCallback(
|
||||||
|
(token: any, render: any) => {
|
||||||
|
console.log('token====', token);
|
||||||
|
if (!reDefineTypes.includes(token.type)) {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: marked.parser([token], { renderer })
|
||||||
|
}}
|
||||||
|
></span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let htmlstr: any = null;
|
||||||
|
let child: any = null;
|
||||||
|
if (token.tokens?.length) {
|
||||||
|
child = render?.(token.tokens as TokensList, render);
|
||||||
|
}
|
||||||
|
const text = child ? child : unescape(token.text);
|
||||||
|
|
||||||
|
if (token.type === 'escape') {
|
||||||
|
htmlstr = text;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (token.type === 'html') {
|
||||||
|
htmlstr = <div dangerouslySetInnerHTML={{ __html: token.text }} />;
|
||||||
|
}
|
||||||
|
if (token.type === 'list') {
|
||||||
|
htmlstr = token.order ? (
|
||||||
|
<ol>{render?.(token.items, render)}</ol>
|
||||||
|
) : (
|
||||||
|
<ul>{render?.(token.items, render)}</ul>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (token.type === 'list_item') {
|
||||||
|
htmlstr = <li>{text}</li>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (token.type === 'del') {
|
||||||
|
htmlstr = <del>{text}</del>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (token.type === 'blockquote') {
|
||||||
|
htmlstr = <blockquote>{text}</blockquote>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (token.type === 'checkbox') {
|
||||||
|
htmlstr = <Checkbox value={token.checked} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (token.type === 'br') {
|
||||||
|
htmlstr = <br />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (token.type === 'em') {
|
||||||
|
htmlstr = <em>{text}</em>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (token.type === 'image') {
|
||||||
|
let href = generateImgSrc(token.href);
|
||||||
|
htmlstr = (
|
||||||
|
<Image
|
||||||
|
src={href}
|
||||||
|
alt={token.text}
|
||||||
|
preview={{
|
||||||
|
mask: <EyeOutlined />
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (token.type === 'text') {
|
||||||
|
htmlstr = text;
|
||||||
|
}
|
||||||
|
if (token.type === 'codespan') {
|
||||||
|
htmlstr = <Text code>{text}</Text>;
|
||||||
|
}
|
||||||
|
if (token.type === 'strong') {
|
||||||
|
htmlstr = <Text strong>{text}</Text>;
|
||||||
|
}
|
||||||
|
if (token.type === 'heading') {
|
||||||
|
htmlstr = <Typography.Title level={4}>{text}</Typography.Title>;
|
||||||
|
}
|
||||||
|
if (token.type === 'paragraph') {
|
||||||
|
htmlstr = <Paragraph> {text}</Paragraph>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (token.type === 'code') {
|
||||||
|
htmlstr = (
|
||||||
|
<HighlightCode theme={theme} code={token.text} lang={token.lang} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (token.type === 'link') {
|
||||||
|
htmlstr = (
|
||||||
|
<Link
|
||||||
|
href={token.href}
|
||||||
|
title={token.title || ''}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
{text}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (token.type === 'hr') {
|
||||||
|
htmlstr = <hr className="hr" />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return htmlstr;
|
||||||
|
},
|
||||||
|
[generateImgSrc]
|
||||||
|
);
|
||||||
|
const renderTokens = (tokens: TokensList): any => {
|
||||||
|
return tokens?.map((token: any, index: number) => {
|
||||||
|
return <Fragment key={index}>{renderItem(token, renderTokens)}</Fragment>;
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!content) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const imgs = document.querySelectorAll('.markdown-viewer img');
|
||||||
|
imgs.forEach((img) => {
|
||||||
|
const src = img.getAttribute('src');
|
||||||
|
img.setAttribute('src', generateImgSrc(src));
|
||||||
|
});
|
||||||
|
}, [content, generateImgSrc]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ height, overflow: 'auto' }}>
|
|
||||||
<div
|
<div
|
||||||
dangerouslySetInnerHTML={{ __html: marked(content, { renderer }) }}
|
style={{ height }}
|
||||||
/>
|
className="markdown-viewer custom-scrollbar-horizontal"
|
||||||
|
>
|
||||||
|
{renderTokens(tokens)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
export default {};
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
const htmlUnescapes: Record<string, string> = {
|
||||||
|
'&': '&',
|
||||||
|
'<': '<',
|
||||||
|
'>': '>',
|
||||||
|
'"': '"',
|
||||||
|
''': "'"
|
||||||
|
};
|
||||||
|
|
||||||
|
const reEscapedHtml = /&(?:amp|lt|gt|quot|#(?:0+)?39);/g;
|
||||||
|
const reHasEscapedHtml = RegExp(reEscapedHtml.source);
|
||||||
|
|
||||||
|
export const unescape = (str = '') => {
|
||||||
|
return reHasEscapedHtml.test(str)
|
||||||
|
? str.replace(reEscapedHtml, (entity) => htmlUnescapes[entity] || "'")
|
||||||
|
: str;
|
||||||
|
};
|
||||||
@@ -4,9 +4,9 @@ import { useEffect, useRef, useState } from 'react';
|
|||||||
import Wrapper from './components/wrapper';
|
import Wrapper from './components/wrapper';
|
||||||
import { SealFormItemProps } from './types';
|
import { SealFormItemProps } from './types';
|
||||||
|
|
||||||
const SealAutoComplete: React.FC<AutoCompleteProps & SealFormItemProps> = (
|
const SealAutoComplete: React.FC<
|
||||||
props
|
AutoCompleteProps & SealFormItemProps & { onInput?: (e: Event) => void }
|
||||||
) => {
|
> = (props) => {
|
||||||
const {
|
const {
|
||||||
label,
|
label,
|
||||||
placeholder,
|
placeholder,
|
||||||
@@ -75,7 +75,7 @@ const SealAutoComplete: React.FC<AutoCompleteProps & SealFormItemProps> = (
|
|||||||
<Wrapper
|
<Wrapper
|
||||||
status={status}
|
status={status}
|
||||||
extra={extra}
|
extra={extra}
|
||||||
label={label || (placeholder as string)}
|
label={label}
|
||||||
isFocus={isFocus}
|
isFocus={isFocus}
|
||||||
required={required}
|
required={required}
|
||||||
description={description}
|
description={description}
|
||||||
@@ -87,7 +87,7 @@ const SealAutoComplete: React.FC<AutoCompleteProps & SealFormItemProps> = (
|
|||||||
{...rest}
|
{...rest}
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
placeholder={
|
placeholder={
|
||||||
isFocus ? (
|
isFocus || !label ? (
|
||||||
<span style={{ paddingLeft: '12px' }}>{placeholder}</span>
|
<span style={{ paddingLeft: '12px' }}>{placeholder}</span>
|
||||||
) : (
|
) : (
|
||||||
''
|
''
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
:local(.wrapper-box) {
|
:local(.wrapper-box) {
|
||||||
@borderRadius: 8px;
|
@borderRadius: var(--border-radius-base);
|
||||||
|
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -95,6 +95,7 @@
|
|||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
padding-block: 20px 0;
|
padding-block: 20px 0;
|
||||||
|
max-width: 100%;
|
||||||
|
|
||||||
&.no-wrapper-style {
|
&.no-wrapper-style {
|
||||||
padding-block: 0;
|
padding-block: 0;
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ interface WrapperProps {
|
|||||||
className?: string;
|
className?: string;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
required?: boolean;
|
required?: boolean;
|
||||||
description?: string;
|
description?: React.ReactNode;
|
||||||
variant?: string;
|
variant?: string;
|
||||||
style?: React.CSSProperties;
|
style?: React.CSSProperties;
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
@@ -28,7 +28,11 @@ const Wrapper: React.FC<WrapperProps> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
style={{ ...style }}
|
style={{
|
||||||
|
padding: '0 calc(var(--ant-padding-sm) - 5px)',
|
||||||
|
width: '100%',
|
||||||
|
...style
|
||||||
|
}}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
wrapperStyle.wrapper,
|
wrapperStyle.wrapper,
|
||||||
wrapperStyle[`validate-status-${status}`],
|
wrapperStyle[`validate-status-${status}`],
|
||||||
@@ -51,8 +55,8 @@ const Wrapper: React.FC<WrapperProps> = ({
|
|||||||
></LabelInfo>
|
></LabelInfo>
|
||||||
</label>
|
</label>
|
||||||
<div
|
<div
|
||||||
|
className="child-inner"
|
||||||
style={{
|
style={{
|
||||||
padding: '0 calc(var(--ant-padding-sm) - 5px)',
|
|
||||||
width: '100%'
|
width: '100%'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ const SealInputNumber: React.FC<InputNumberProps & SealFormItemProps> = (
|
|||||||
return (
|
return (
|
||||||
<Wrapper
|
<Wrapper
|
||||||
status={status}
|
status={status}
|
||||||
label={label || (placeholder as string)}
|
label={label}
|
||||||
isFocus={isFocus}
|
isFocus={isFocus}
|
||||||
required={required}
|
required={required}
|
||||||
description={description}
|
description={description}
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ const SealInputSearch: React.FC<SearchProps & SealFormItemProps> = (props) => {
|
|||||||
return (
|
return (
|
||||||
<Wrapper
|
<Wrapper
|
||||||
status={status}
|
status={status}
|
||||||
label={label || (placeholder as string)}
|
label={label}
|
||||||
isFocus={isFocus}
|
isFocus={isFocus}
|
||||||
required={required}
|
required={required}
|
||||||
description={description}
|
description={description}
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ const SealPassword: React.FC<InputProps & SealFormItemProps> = (props) => {
|
|||||||
return (
|
return (
|
||||||
<Wrapper
|
<Wrapper
|
||||||
status={status}
|
status={status}
|
||||||
label={label || (placeholder as string)}
|
label={label}
|
||||||
isFocus={isFocus}
|
isFocus={isFocus}
|
||||||
required={required}
|
required={required}
|
||||||
description={description}
|
description={description}
|
||||||
|
|||||||
@@ -0,0 +1,115 @@
|
|||||||
|
import { CloseOutlined } from '@ant-design/icons';
|
||||||
|
import { useIntl } from '@umijs/max';
|
||||||
|
import { Button, Input, Tooltip } from 'antd';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import './styles/row-textarea.less';
|
||||||
|
|
||||||
|
interface SystemMessageProps {
|
||||||
|
style?: React.CSSProperties;
|
||||||
|
value: string;
|
||||||
|
placeholder?: string;
|
||||||
|
label?: React.ReactNode;
|
||||||
|
onChange: (e: any) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const RowTextarea: React.FC<SystemMessageProps> = (props) => {
|
||||||
|
const { value, onChange, style, label, placeholder } = props;
|
||||||
|
const intl = useIntl();
|
||||||
|
const rowTextAreaRef = React.useRef<any>(null);
|
||||||
|
const [autoSize, setAutoSize] = useState<{
|
||||||
|
minRows: number;
|
||||||
|
maxRows: number;
|
||||||
|
focus: boolean;
|
||||||
|
}>({ minRows: 1, maxRows: 1, focus: false });
|
||||||
|
|
||||||
|
const handleFocus = () => {
|
||||||
|
setAutoSize({
|
||||||
|
minRows: 3,
|
||||||
|
maxRows: 3,
|
||||||
|
focus: true
|
||||||
|
});
|
||||||
|
setTimeout(() => {
|
||||||
|
rowTextAreaRef.current?.focus?.({
|
||||||
|
cursor: 'end'
|
||||||
|
});
|
||||||
|
}, 50);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBlur = (e: any) => {
|
||||||
|
setAutoSize({
|
||||||
|
minRows: 2,
|
||||||
|
maxRows: 2,
|
||||||
|
focus: false
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOnChange = (e: any) => {
|
||||||
|
onChange?.(e);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClear = () => {
|
||||||
|
onChange?.({ target: { value: '' } });
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={classNames('row-textarea', {
|
||||||
|
focus: autoSize.focus
|
||||||
|
})}
|
||||||
|
style={{ ...style }}
|
||||||
|
>
|
||||||
|
{
|
||||||
|
<div
|
||||||
|
style={{ display: autoSize.focus ? 'block' : 'none' }}
|
||||||
|
className="textarea-wrapper"
|
||||||
|
>
|
||||||
|
<span className="textarea-label">{label}</span>
|
||||||
|
<Input.TextArea
|
||||||
|
className="custome-scrollbar"
|
||||||
|
ref={rowTextAreaRef}
|
||||||
|
placeholder={placeholder}
|
||||||
|
style={{
|
||||||
|
borderRadius: '0',
|
||||||
|
border: 'none'
|
||||||
|
}}
|
||||||
|
value={value}
|
||||||
|
autoSize={{
|
||||||
|
minRows: autoSize.minRows,
|
||||||
|
maxRows: autoSize.maxRows
|
||||||
|
}}
|
||||||
|
onFocus={handleFocus}
|
||||||
|
onBlur={handleBlur}
|
||||||
|
allowClear={false}
|
||||||
|
onChange={handleOnChange}
|
||||||
|
></Input.TextArea>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
{!autoSize.focus && (
|
||||||
|
<div className="content-wrap" onClick={handleFocus}>
|
||||||
|
<div className="content">
|
||||||
|
<span className="title">{label}</span>
|
||||||
|
{value || (
|
||||||
|
<span style={{ color: 'var(--ant-color-text-tertiary)' }}>
|
||||||
|
{placeholder}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{value && (
|
||||||
|
<Tooltip title={intl.formatMessage({ id: 'common.button.clear' })}>
|
||||||
|
<Button
|
||||||
|
className="clear-btn"
|
||||||
|
type="text"
|
||||||
|
icon={<CloseOutlined />}
|
||||||
|
size="small"
|
||||||
|
onClick={handleClear}
|
||||||
|
></Button>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default React.memo(RowTextarea);
|
||||||
@@ -17,6 +17,7 @@ const SealInput: React.FC<InputProps & SealFormItemProps> = (props) => {
|
|||||||
isInFormItems = true,
|
isInFormItems = true,
|
||||||
variant,
|
variant,
|
||||||
addAfter,
|
addAfter,
|
||||||
|
checkStatus,
|
||||||
trim = true,
|
trim = true,
|
||||||
...rest
|
...rest
|
||||||
} = props;
|
} = props;
|
||||||
@@ -70,8 +71,8 @@ const SealInput: React.FC<InputProps & SealFormItemProps> = (props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Wrapper
|
<Wrapper
|
||||||
status={status}
|
status={checkStatus || status}
|
||||||
label={label || (placeholder as string)}
|
label={label}
|
||||||
isFocus={isFocus}
|
isFocus={isFocus}
|
||||||
required={required}
|
required={required}
|
||||||
description={description}
|
description={description}
|
||||||
@@ -82,7 +83,7 @@ const SealInput: React.FC<InputProps & SealFormItemProps> = (props) => {
|
|||||||
>
|
>
|
||||||
<Input
|
<Input
|
||||||
{...rest}
|
{...rest}
|
||||||
placeholder={isFocus ? placeholder : ''}
|
placeholder={isFocus || !label ? placeholder : ''}
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
onInput={handleInput}
|
onInput={handleInput}
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ const SealSelect: React.FC<SelectProps & SealFormItemProps> = (props) => {
|
|||||||
return (
|
return (
|
||||||
<Wrapper
|
<Wrapper
|
||||||
status={status}
|
status={status}
|
||||||
label={label || (placeholder as string)}
|
label={label}
|
||||||
isFocus={isFocus}
|
isFocus={isFocus}
|
||||||
required={required}
|
required={required}
|
||||||
description={description}
|
description={description}
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ const SealTextArea: React.FC<TextAreaProps & SealFormItemProps> = (props) => {
|
|||||||
return (
|
return (
|
||||||
<Wrapper
|
<Wrapper
|
||||||
status={status}
|
status={status}
|
||||||
label={label || (placeholder as string)}
|
label={label}
|
||||||
isFocus={isFocus}
|
isFocus={isFocus}
|
||||||
required={required}
|
required={required}
|
||||||
description={description}
|
description={description}
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
.row-textarea {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&.focus {
|
||||||
|
padding-top: 9px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-wrap {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding-right: 20px;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
.clear-btn {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea.ant-input {
|
||||||
|
background-color: transparent;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clear-btn {
|
||||||
|
display: none;
|
||||||
|
position: absolute;
|
||||||
|
right: 6px;
|
||||||
|
top: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.textarea-label {
|
||||||
|
position: relative;
|
||||||
|
top: 1px;
|
||||||
|
font-weight: var(--font-weight-bold);
|
||||||
|
padding-left: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
flex: 1;
|
||||||
|
width: 100px;
|
||||||
|
height: 40px;
|
||||||
|
line-height: 24px;
|
||||||
|
padding: 8px 14px;
|
||||||
|
padding-right: 4px;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
color: var(--ant-color-text-secondary);
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-weight: var(--font-weight-normal);
|
||||||
|
padding-right: 10px;
|
||||||
|
color: var(--ant-color-text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,4 +9,5 @@ export interface SealFormItemProps {
|
|||||||
addAfter?: React.ReactNode;
|
addAfter?: React.ReactNode;
|
||||||
loading?: React.ReactNode;
|
loading?: React.ReactNode;
|
||||||
trim?: boolean;
|
trim?: boolean;
|
||||||
|
checkStatus?: 'success' | 'error' | 'warning' | '';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ const TableRow: React.FC<
|
|||||||
|
|
||||||
const { updateChunkedList, cacheDataListRef } = useUpdateChunkedList({
|
const { updateChunkedList, cacheDataListRef } = useUpdateChunkedList({
|
||||||
dataList: childrenData,
|
dataList: childrenData,
|
||||||
|
limit: 100,
|
||||||
setDataList: setChildrenData
|
setDataList: setChildrenData
|
||||||
// callback: (list) => renderChildren?.(list)
|
// callback: (list) => renderChildren?.(list)
|
||||||
});
|
});
|
||||||
@@ -79,7 +80,7 @@ const TableRow: React.FC<
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const renderChildrenData = () => {
|
const renderChildrenData = () => {
|
||||||
return renderChildren?.(childrenData);
|
return renderChildren?.(childrenData, record);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlePolling = async () => {
|
const handlePolling = async () => {
|
||||||
@@ -118,7 +119,7 @@ const TableRow: React.FC<
|
|||||||
|
|
||||||
const updateChildrenHandler = (list: any) => {
|
const updateChildrenHandler = (list: any) => {
|
||||||
_.each(list, (data: any) => {
|
_.each(list, (data: any) => {
|
||||||
updateChunkedList(data, childrenDataRef.current);
|
updateChunkedList(data);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -46,7 +46,10 @@ const SealTable: React.FC<SealTableProps & { pagination: PaginationProps }> = (
|
|||||||
if (selectedRowKeys?.length === 0) {
|
if (selectedRowKeys?.length === 0) {
|
||||||
setSelectAll(false);
|
setSelectAll(false);
|
||||||
setIndeterminate(false);
|
setIndeterminate(false);
|
||||||
} else if (selectedRowKeys?.length === props.dataSource.length) {
|
} else if (
|
||||||
|
selectedRowKeys?.length === props.dataSource.length &&
|
||||||
|
selectedRowKeys.length > 0
|
||||||
|
) {
|
||||||
setSelectAll(true);
|
setSelectAll(true);
|
||||||
setIndeterminate(false);
|
setIndeterminate(false);
|
||||||
} else {
|
} else {
|
||||||
@@ -54,7 +57,7 @@ const SealTable: React.FC<SealTableProps & { pagination: PaginationProps }> = (
|
|||||||
setIndeterminate(true);
|
setIndeterminate(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [rowSelection]);
|
}, [rowSelection, props.dataSource]);
|
||||||
|
|
||||||
const handleSelectAllChange = (e: any) => {
|
const handleSelectAllChange = (e: any) => {
|
||||||
if (e.target.checked) {
|
if (e.target.checked) {
|
||||||
|
|||||||
@@ -4,7 +4,15 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
height: 68px;
|
min-height: 68px;
|
||||||
|
word-break: break-word;
|
||||||
|
min-width: 20px;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
.cell-content {
|
||||||
|
max-width: 100%;
|
||||||
|
line-height: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
&-left {
|
&-left {
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
|
|||||||
@@ -63,6 +63,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.row-prefix-wrapper {
|
.row-prefix-wrapper {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
padding-left: var(--ant-table-cell-padding-inline);
|
padding-left: var(--ant-table-cell-padding-inline);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ export interface SealTableProps {
|
|||||||
onCell?: (record: any, dataIndex: string) => void;
|
onCell?: (record: any, dataIndex: string) => void;
|
||||||
onSort?: (dataIndex: string, order: 'ascend' | 'descend') => void;
|
onSort?: (dataIndex: string, order: 'ascend' | 'descend') => void;
|
||||||
onExpand?: (expanded: boolean, record: any, rowKey: any) => void;
|
onExpand?: (expanded: boolean, record: any, rowKey: any) => void;
|
||||||
renderChildren?: (data: any) => React.ReactNode;
|
renderChildren?: (data: any, parent?: any) => React.ReactNode;
|
||||||
loadChildren?: (record: any) => Promise<any[]>;
|
loadChildren?: (record: any) => Promise<any[]>;
|
||||||
loadChildrenAPI?: (record: any) => string;
|
loadChildrenAPI?: (record: any) => string;
|
||||||
contentRendered?: () => void;
|
contentRendered?: () => void;
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ const StatusTag: React.FC<StatusTagProps> = ({
|
|||||||
const renderContent = () => {
|
const renderContent = () => {
|
||||||
const percent = download?.percent || 0;
|
const percent = download?.percent || 0;
|
||||||
|
|
||||||
if (download && percent > 0 && percent < 100) {
|
if (download && percent > 0 && percent <= 100) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<span className="progress">{download?.percent || 0}%</span>
|
<span className="progress">{download?.percent || 0}%</span>
|
||||||
|
|||||||
@@ -37,7 +37,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.ant-input {
|
.ant-input {
|
||||||
/* 只隐藏垂直滚动条轨道 */
|
|
||||||
&::-webkit-scrollbar-track {
|
&::-webkit-scrollbar-track {
|
||||||
width: 0 !important;
|
width: 0 !important;
|
||||||
color: transparent;
|
color: transparent;
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
.version-box {
|
.version-box {
|
||||||
display: flex;
|
display: flex;
|
||||||
margin-bottom: 40px;
|
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
@@ -23,20 +22,42 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.ver {
|
.ver {
|
||||||
|
margin-bottom: 20px;
|
||||||
line-height: 32px;
|
line-height: 32px;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
font-size: var(--font-size-middle);
|
font-size: var(--font-size-middle);
|
||||||
|
color: var(--ant-color-text-secondary);
|
||||||
|
gap: 10px;
|
||||||
|
|
||||||
.label {
|
.label {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
width: 60px;
|
|
||||||
font-size: var(--font-size-middle);
|
font-size: var(--font-size-middle);
|
||||||
font-weight: var(--font-weight-medium);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.val {
|
.dev.val {
|
||||||
color: var(--ant-color-text-secondary);
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.item {
|
||||||
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tl {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-start;
|
||||||
|
width: 60px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.upgrade {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding-block: 20px 4px;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
border-top: 1px solid var(--ant-color-split);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,34 +1,80 @@
|
|||||||
import Logo from '@/assets/images/gpustack-logo.png';
|
import Logo from '@/assets/images/gpustack-logo.png';
|
||||||
import { GPUStackVersionAtom } from '@/atoms/user';
|
import { GPUStackVersionAtom, UpdateCheckAtom, userAtom } from '@/atoms/user';
|
||||||
import { getAtomStorage } from '@/atoms/utils';
|
import { getAtomStorage } from '@/atoms/utils';
|
||||||
|
import externalLinks from '@/constants/external-links';
|
||||||
|
import { Button } from 'antd';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import './index.less';
|
import './index.less';
|
||||||
|
|
||||||
const VersionInfo: React.FC<{ intl: any }> = ({ intl }) => {
|
const VersionInfo: React.FC<{ intl: any }> = ({ intl }) => {
|
||||||
// get the data attr from html
|
const latestVersion = getAtomStorage(UpdateCheckAtom).latest_version;
|
||||||
const version = document.documentElement.getAttribute('data-version');
|
const currentVersion = getAtomStorage(GPUStackVersionAtom)?.version;
|
||||||
|
|
||||||
|
const isProd =
|
||||||
|
currentVersion !== '0.0.0' && currentVersion?.indexOf('rc') === -1;
|
||||||
|
|
||||||
|
const uiVersion = document.documentElement.getAttribute('data-version');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="version-box">
|
<div className="version-box">
|
||||||
<div className="img">
|
<div className="img">
|
||||||
<img src={Logo} alt="logo" />
|
<img src={Logo} alt="logo" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<div className="ver">
|
<div className="ver">
|
||||||
|
{isProd && (
|
||||||
<span className="label">
|
<span className="label">
|
||||||
{' '}
|
{intl.formatMessage({ id: 'common.footer.version' })}
|
||||||
{intl.formatMessage({ id: 'common.footer.version.server' })}
|
|
||||||
</span>
|
</span>
|
||||||
|
)}
|
||||||
|
{isProd ? (
|
||||||
<span className="val">
|
<span className="val">
|
||||||
{getAtomStorage(GPUStackVersionAtom)?.version ||
|
{getAtomStorage(GPUStackVersionAtom)?.version ||
|
||||||
getAtomStorage(GPUStackVersionAtom)?.git_commit}
|
getAtomStorage(GPUStackVersionAtom)?.git_commit}
|
||||||
</span>
|
</span>
|
||||||
|
) : (
|
||||||
|
<span className="val dev">
|
||||||
|
<span className="item">
|
||||||
|
<span className="tl">
|
||||||
|
{' '}
|
||||||
|
{intl.formatMessage({ id: 'common.footer.version.server' })}
|
||||||
|
</span>
|
||||||
|
{currentVersion === '0.0.0'
|
||||||
|
? getAtomStorage(GPUStackVersionAtom)?.git_commit
|
||||||
|
: getAtomStorage(GPUStackVersionAtom)?.version}
|
||||||
|
</span>
|
||||||
|
<span className="item">
|
||||||
|
<span className="tl">UI</span>
|
||||||
|
{uiVersion}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="ver">
|
{getAtomStorage(userAtom)?.is_admin && isProd && (
|
||||||
<span className="label">UI </span>
|
<div className="upgrade">
|
||||||
<span className="val"> {version}</span>
|
<span className="m-l-5">
|
||||||
</div>
|
{latestVersion &&
|
||||||
|
latestVersion !== currentVersion &&
|
||||||
|
latestVersion !== '0.0.0'
|
||||||
|
? intl.formatMessage(
|
||||||
|
{ id: 'users.version.update' },
|
||||||
|
{ version: latestVersion }
|
||||||
|
)
|
||||||
|
: intl.formatMessage(
|
||||||
|
{ id: 'users.version.islatest' },
|
||||||
|
{ version: currentVersion }
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
<Button
|
||||||
|
type="link"
|
||||||
|
href={externalLinks.release}
|
||||||
|
target="_blank"
|
||||||
|
style={{ paddingInline: 0 }}
|
||||||
|
>
|
||||||
|
{intl.formatMessage({ id: 'common.text.changelog' })}
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Vendored
+10
-4
@@ -22,14 +22,20 @@ declare namespace Global {
|
|||||||
id: number;
|
id: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface BaseListItem {
|
interface BaseListItem<T> {
|
||||||
key: string;
|
key: string;
|
||||||
value: string | number;
|
value: T;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface BaseOption {
|
interface BaseOption<T> {
|
||||||
label: string;
|
label: string;
|
||||||
value: string | number;
|
value: T;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface HintOptions {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
opts?: Array<BaseOption<string>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
type SearchParams = Pagination & { search?: string };
|
type SearchParams = Pagination & { search?: string };
|
||||||
|
|||||||
+24
-20
@@ -1,23 +1,27 @@
|
|||||||
import { platformCall } from '@/utils';
|
import { platformCall } from '@/utils';
|
||||||
|
|
||||||
const platform = platformCall();
|
const platform = platformCall();
|
||||||
const KeybindingsMap = {
|
const KeybindingsMap = {
|
||||||
CREATE: ['alt+ctrl+N', 'alt+meta+N'],
|
CREATE: ['Alt+Ctrl+N', 'Alt+Meta+N'],
|
||||||
CLEAR: ['alt+ctrl+W', 'alt+meta+W'],
|
CLEAR: ['Alt+Ctrl+K', 'Alt+Meta+K'],
|
||||||
RIGHT: ['ctrl+RIGHT', 'meta+RIGHT'],
|
RIGHT: ['Ctrl+RIGHT', 'Meta+RIGHT'],
|
||||||
SAVE: ['ctrl+S', 'meta+S'],
|
SAVE: ['Ctrl+S', 'Meta+S'],
|
||||||
SUBMIT: ['ctrl+enter', 'meta+enter'],
|
SUBMIT: ['Ctrl+Enter', 'Meta+Enter'],
|
||||||
SAVEAS: ['alt+ctrl+S', 'alt+meta+S'],
|
SAVEAS: ['Alt+Ctrl+S', 'Alt+Meta+S'],
|
||||||
OPEN: ['alt+ctrl+O', 'alt+meta+O'],
|
OPEN: ['Alt+Ctrl+O', 'Alt+Meta+O'],
|
||||||
CANCEL: ['ctrl+W', 'meta+W'],
|
CANCEL: ['Ctrl+W', 'Meta+W'],
|
||||||
DELETE: ['delete'],
|
DELETE: ['delete'],
|
||||||
COPY: ['ctrl+C', 'meta+C'],
|
COPY: ['Ctrl+C', 'Meta+C'],
|
||||||
REFRESH: ['ctrl+R', 'meta+R'],
|
REFRESH: ['Ctrl+R', 'Meta+R'],
|
||||||
EDIT: ['ctrl+E', 'meta+E'],
|
EDIT: ['Ctrl+E', 'Meta+E'],
|
||||||
SEARCH: ['ctrl+K', 'meta+K'],
|
SEARCH: ['Ctrl+K', 'Meta+K'],
|
||||||
RESET: ['alt+ctrl+R', 'alt+meta+R'],
|
RESET: ['Alt+Ctrl+R', 'Alt+Meta+R'],
|
||||||
INPUT: ['ctrl+K', 'meta+K'],
|
INPUT: ['Ctrl+K', 'Meta+K'],
|
||||||
NEW1: ['ctrl+1', 'meta+1'],
|
NEW1: ['Ctrl+1', 'Meta+1'],
|
||||||
NEW2: ['ctrl+2', 'meta+2']
|
NEW2: ['Ctrl+2', 'Meta+2'],
|
||||||
|
NEW3: ['Ctrl+3', 'Meta+3'],
|
||||||
|
FOCUS: ['/', '/'],
|
||||||
|
ADD: ['Alt+Ctrl+Enter', 'Alt+Meta+Enter']
|
||||||
};
|
};
|
||||||
|
|
||||||
type KeyBindingType = keyof typeof KeybindingsMap;
|
type KeyBindingType = keyof typeof KeybindingsMap;
|
||||||
@@ -36,11 +40,11 @@ const KeybiningList: KeybindingValue[] = Object.entries(KeybindingsMap).map(
|
|||||||
keybinding: keybinding,
|
keybinding: keybinding,
|
||||||
command: key,
|
command: key,
|
||||||
textKeybinding: platform.isMac
|
textKeybinding: platform.isMac
|
||||||
? keybinding.replace('meta', 'Command').replace('alt', 'Option')
|
? keybinding.replace('Meta', 'Cmd').replace('Alt', 'Option')
|
||||||
: keybinding.replace('ctrl', 'Ctrl'),
|
: keybinding.replace('Ctrl', 'Ctrl'),
|
||||||
iconKeybinding: platform.isMac
|
iconKeybinding: platform.isMac
|
||||||
? keybinding.replace('meta', '⌘').replace('alt', '⌥')
|
? keybinding.replace('Meta', '⌘').replace('Alt', '⌥')
|
||||||
: keybinding.replace('ctrl', 'Ctrl')
|
: keybinding.replace('Ctrl', 'Ctrl')
|
||||||
} as KeybindingValue;
|
} as KeybindingValue;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,5 +2,6 @@ export default {
|
|||||||
documentation: 'https://docs.gpustack.ai/',
|
documentation: 'https://docs.gpustack.ai/',
|
||||||
github: 'https://github.com/gpustack/gpustack',
|
github: 'https://github.com/gpustack/gpustack',
|
||||||
discord: 'https://discord.gg/VXYJzuaqwD',
|
discord: 'https://discord.gg/VXYJzuaqwD',
|
||||||
site: 'https://gpustack.ai/'
|
site: 'https://gpustack.ai/',
|
||||||
|
release: 'https://github.com/gpustack/gpustack/releases'
|
||||||
};
|
};
|
||||||
|
|||||||
+201
-15
@@ -2,15 +2,23 @@
|
|||||||
@import url('src/assets/styles/menu.less');
|
@import url('src/assets/styles/menu.less');
|
||||||
|
|
||||||
html {
|
html {
|
||||||
|
--font-family: 'noto sans', sans-serif;
|
||||||
--ant-color-text-secondary: rgba(0, 0, 0, 65%);
|
--ant-color-text-secondary: rgba(0, 0, 0, 65%);
|
||||||
--ant-color-text-tertiary: rgba(0, 0, 0, 45%);
|
--ant-color-text-tertiary: rgba(0, 0, 0, 45%);
|
||||||
--ant-color-text-quaternary: rgba(0, 0, 0, 25%);
|
--ant-color-text-quaternary: rgba(0, 0, 0, 25%);
|
||||||
--ant-color-fill: rgba(0, 0, 0, 15%);
|
--ant-color-fill: rgba(223, 168, 168, 15%);
|
||||||
--ant-color-fill-secondary: rgba(0, 0, 0, 6%);
|
--ant-color-fill-secondary: rgba(0, 0, 0, 6%);
|
||||||
--ant-color-fill-tertiary: rgba(0, 0, 0, 4%);
|
--ant-color-fill-tertiary: rgba(0, 0, 0, 4%);
|
||||||
--ant-color-fill-quaternary: rgba(0, 0, 0, 2%);
|
--ant-color-fill-quaternary: rgba(0, 0, 0, 2%);
|
||||||
|
--color-text-light-1: rgba(255, 255, 255, 90%);
|
||||||
--color-fill-1: var(--ant-color-fill-tertiary);
|
--color-fill-1: var(--ant-color-fill-tertiary);
|
||||||
--color-scrollbar-thumb: rgba(193, 193, 193, 80%);
|
--color-scrollbar-thumb: rgba(193, 193, 193, 80%);
|
||||||
|
--scrollbar-size: 6px;
|
||||||
|
--scrollbar-handle-bg: rgba(0, 0, 0, 44%);
|
||||||
|
--scrollbar-handle-light-bg: rgba(255, 255, 255, 44%);
|
||||||
|
--scrollbar-handle-hover-bg: rgba(0, 0, 0, 55%);
|
||||||
|
--color-editor-dark: #282c34;
|
||||||
|
--color-editor-light: #fafafa;
|
||||||
--color-scrollbar-track: var(--ant-color-fill-tertiary);
|
--color-scrollbar-track: var(--ant-color-fill-tertiary);
|
||||||
// --color-fill-1: #fff;
|
// --color-fill-1: #fff;
|
||||||
--ant-color-text: #000;
|
--ant-color-text: #000;
|
||||||
@@ -21,12 +29,14 @@ html {
|
|||||||
--color-logs-text: #d4d4d4;
|
--color-logs-text: #d4d4d4;
|
||||||
--layout-content-blockpadding: 32px;
|
--layout-content-blockpadding: 32px;
|
||||||
--layout-content-inlinepadding: 32px;
|
--layout-content-inlinepadding: 32px;
|
||||||
--menu-border-radius-base: 8px;
|
--border-radius-modal: 6px;
|
||||||
--border-radius-base: 8px;
|
--menu-border-radius-base: 4px;
|
||||||
|
--border-radius-base: 4px;
|
||||||
--border-radius-middle: 20px;
|
--border-radius-middle: 20px;
|
||||||
--border-radius-small: 8px;
|
--border-radius-small: 4px;
|
||||||
--border-radius-mdium: 6px;
|
--border-radius-mdium: 4px;
|
||||||
--border-radius-mini: 4px;
|
--border-radius-mini: 4px;
|
||||||
|
--border-radius-2px: 2px;
|
||||||
--color-white-1: rgba(255, 255, 255, 100%);
|
--color-white-1: rgba(255, 255, 255, 100%);
|
||||||
--color-fill-sider: #f4f5f4;
|
--color-fill-sider: #f4f5f4;
|
||||||
--font-weight-normal: 500;
|
--font-weight-normal: 500;
|
||||||
@@ -36,14 +46,15 @@ html {
|
|||||||
--color-text-3: rgba(0, 0, 0, 45%);
|
--color-text-3: rgba(0, 0, 0, 45%);
|
||||||
--color-text-2: rgba(0, 0, 0, 65%);
|
--color-text-2: rgba(0, 0, 0, 65%);
|
||||||
--color-bg-light-1: #e6f6ff;
|
--color-bg-light-1: #e6f6ff;
|
||||||
|
--font-size-small: 13px;
|
||||||
--font-size-base: 12px;
|
--font-size-base: 12px;
|
||||||
--font-size-large: 16px;
|
--font-size-large: 16px;
|
||||||
--font-size-middle: 14px;
|
--font-size-middle: 14px;
|
||||||
--table-td-radius: 8px;
|
--table-td-radius: 4px;
|
||||||
--checkbox-border-radius: 4px;
|
--checkbox-border-radius: 2px;
|
||||||
--ant-table-cell-padding-inline: 16px;
|
--ant-table-cell-padding-inline: 16px;
|
||||||
--ant-table-cell-padding-block: 8px;
|
--ant-table-cell-padding-block: 6px;
|
||||||
--ant-table-header-border-radius: 8px;
|
--ant-table-header-border-radius: 4px;
|
||||||
--ant-table-header-split-color: #f0f0f0;
|
--ant-table-header-split-color: #f0f0f0;
|
||||||
--ant-table-row-selected-bg: #e6f6ff;
|
--ant-table-row-selected-bg: #e6f6ff;
|
||||||
--ant-table-row-selected-hover-bg: #e6f6ff;
|
--ant-table-row-selected-hover-bg: #e6f6ff;
|
||||||
@@ -54,12 +65,14 @@ html {
|
|||||||
--seal-transition-func: cubic-bezier(0, 0, 1, 1);
|
--seal-transition-func: cubic-bezier(0, 0, 1, 1);
|
||||||
--color-progress-green: rgba(84, 204, 152, 100%);
|
--color-progress-green: rgba(84, 204, 152, 100%);
|
||||||
--color-border-1: rgba(217, 217, 217, 100%);
|
--color-border-1: rgba(217, 217, 217, 100%);
|
||||||
|
--ant-rate-star-color: #fadb14;
|
||||||
|
--color-fill-mask: rgba(255, 255, 255, 60%);
|
||||||
// ======== input ============
|
// ======== input ============
|
||||||
--ant-input-active-shadow: 0 0 0 2px rgba(5, 255, 105, 6%);
|
--ant-input-active-shadow: 0 0 0 2px rgba(5, 255, 105, 6%);
|
||||||
--ant-input-active-border-color: #007bff;
|
--ant-input-active-border-color: #007bff;
|
||||||
--ant-input-hover-border-color: #2997ff;
|
--ant-input-hover-border-color: #2997ff;
|
||||||
--box-shadow-base: 0 4px 6px rgba(227, 232, 240, 70%);
|
--box-shadow-base: 0 4px 6px rgba(227, 232, 240, 70%);
|
||||||
--ant-border-radius-lg: 8px;
|
--ant-border-radius-lg: 4px;
|
||||||
--ant-menu-item-color: var(--color-text-1);
|
--ant-menu-item-color: var(--color-text-1);
|
||||||
--color-selected-bg: rgba(230, 230, 230, 88%);
|
--color-selected-bg: rgba(230, 230, 230, 88%);
|
||||||
// --box-shadow-base: none;
|
// --box-shadow-base: none;
|
||||||
@@ -75,7 +88,7 @@ html {
|
|||||||
|
|
||||||
&.ant-menu-css-var {
|
&.ant-menu-css-var {
|
||||||
// --ant-menu-item-selected-bg: var(--color-white-1);
|
// --ant-menu-item-selected-bg: var(--color-white-1);
|
||||||
--ant-menu-item-border-radius: 8px;
|
--ant-menu-item-border-radius: 4px;
|
||||||
--ant-menu-item-selected-color: var(--ant-color-primary);
|
--ant-menu-item-selected-color: var(--ant-color-primary);
|
||||||
--ant-menu-item-color: var(--color-text-1);
|
--ant-menu-item-color: var(--color-text-1);
|
||||||
}
|
}
|
||||||
@@ -92,7 +105,7 @@ html {
|
|||||||
--ant-font-size-xl: 20px;
|
--ant-font-size-xl: 20px;
|
||||||
--ant-font-size: var(--font-size-base);
|
--ant-font-size: var(--font-size-base);
|
||||||
--ant-padding-sm: 14px;
|
--ant-padding-sm: 14px;
|
||||||
--ant-border-radius-lg: 8px;
|
--ant-border-radius-lg: 4px;
|
||||||
--ant-color-text: #000;
|
--ant-color-text: #000;
|
||||||
--ant-color-error: #ff4d4f;
|
--ant-color-error: #ff4d4f;
|
||||||
--ant-color-bg-mask: rgba(0, 0, 0, 35%);
|
--ant-color-bg-mask: rgba(0, 0, 0, 35%);
|
||||||
@@ -113,6 +126,10 @@ html {
|
|||||||
--ant-table-row-hover-bg: #e6e6e6;
|
--ant-table-row-hover-bg: #e6e6e6;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.ant-progress {
|
||||||
|
--ant-progress-line-border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
&.ant-modal-css-var {
|
&.ant-modal-css-var {
|
||||||
--ant-modal-title-font-size: 14px;
|
--ant-modal-title-font-size: 14px;
|
||||||
--ant-modal-header-margin-bottom: 20px;
|
--ant-modal-header-margin-bottom: 20px;
|
||||||
@@ -126,7 +143,7 @@ html {
|
|||||||
.css-var-ri.ant-menu-css-var,
|
.css-var-ri.ant-menu-css-var,
|
||||||
.css-var-rh.ant-menu-css-var {
|
.css-var-rh.ant-menu-css-var {
|
||||||
--ant-menu-item-height: 46px;
|
--ant-menu-item-height: 46px;
|
||||||
--ant-menu-item-border-radius: 8px;
|
--ant-menu-item-border-radius: 4px;
|
||||||
--ant-menu-item-selected-color: var(--ant-color-primary);
|
--ant-menu-item-selected-color: var(--ant-color-primary);
|
||||||
--ant-menu-item-color: var(--color-text-1);
|
--ant-menu-item-color: var(--color-text-1);
|
||||||
--ant-menu-item-active-bg: rgba(0, 0, 0, 4%);
|
--ant-menu-item-active-bg: rgba(0, 0, 0, 4%);
|
||||||
@@ -155,6 +172,10 @@ body {
|
|||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
|
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.ant-modal-confirm-title {
|
.ant-modal-confirm-title {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
@@ -269,7 +290,7 @@ body {
|
|||||||
|
|
||||||
// form item help
|
// form item help
|
||||||
.ant-form-item-with-help .ant-form-item-explain {
|
.ant-form-item-with-help .ant-form-item-explain {
|
||||||
padding-left: 24px;
|
padding-left: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
// icon
|
// icon
|
||||||
@@ -293,12 +314,26 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.ant-pro-sider-logo {
|
.ant-pro-sider-logo {
|
||||||
|
position: relative;
|
||||||
padding-left: 18px;
|
padding-left: 18px;
|
||||||
|
padding-block: 12px;
|
||||||
border-block-end: none;
|
border-block-end: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ant-pro-sider-logo-collapsed {
|
.ant-pro-sider-logo-collapsed {
|
||||||
padding-left: 12px;
|
padding-left: 12px;
|
||||||
|
|
||||||
|
.collapse-wrap {
|
||||||
|
display: none;
|
||||||
|
position: absolute;
|
||||||
|
top: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
.collapse-wrap {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.ant-pro-sider .ant-layout-sider-children {
|
.ant-pro-sider .ant-layout-sider-children {
|
||||||
@@ -353,6 +388,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.ant-pro-layout-container {
|
.ant-pro-layout-container {
|
||||||
|
overflow-x: auto;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
// background-color: var(--color-fill-2);
|
// background-color: var(--color-fill-2);
|
||||||
}
|
}
|
||||||
@@ -404,6 +440,26 @@ body {
|
|||||||
font-size: var(--font-size-middle);
|
font-size: var(--font-size-middle);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.new-icon {
|
||||||
|
position: relative;
|
||||||
|
bottom: 3px;
|
||||||
|
display: flex;
|
||||||
|
color: #fff;
|
||||||
|
width: 36px;
|
||||||
|
height: 15px;
|
||||||
|
margin-left: 5px;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
background-color: var(--ant-orange-5);
|
||||||
|
border-radius: 8px 8px 8px 0;
|
||||||
|
transform: scale(0.9);
|
||||||
|
|
||||||
|
.text {
|
||||||
|
transform: scale(0.8);
|
||||||
|
line-height: 1em;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -416,6 +472,16 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
&.user-menu-collapsed {
|
&.user-menu-collapsed {
|
||||||
|
.ant-menu-submenu-title {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
color: var(--color-text-1) !important;
|
||||||
|
padding-inline: unset !important;
|
||||||
|
width: 100%;
|
||||||
|
margin-inline: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
.user-avatar.ant-menu-submenu {
|
.user-avatar.ant-menu-submenu {
|
||||||
.ant-menu-submenu-title {
|
.ant-menu-submenu-title {
|
||||||
padding: 0 !important;
|
padding: 0 !important;
|
||||||
@@ -427,6 +493,10 @@ body {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sub-title {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.ant-menu-submenu-title {
|
.ant-menu-submenu-title {
|
||||||
@@ -442,7 +512,105 @@ body {
|
|||||||
}
|
}
|
||||||
// ======== monaco editor style ============
|
// ======== monaco editor style ============
|
||||||
.monaco-editor {
|
.monaco-editor {
|
||||||
border-radius: 16px;
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-modal-centered.ant-modal-wrap {
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
width: var(--scrollbar-size);
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-thumb {
|
||||||
|
background-color: transparent;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-track {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
&::-webkit-scrollbar-thumb {
|
||||||
|
background-color: var(--scrollbar-handle-bg);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.custome-scrollbar {
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
width: var(--scrollbar-size);
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-thumb {
|
||||||
|
background-color: transparent;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-track {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
&::-webkit-scrollbar-thumb {
|
||||||
|
background-color: var(--color-scrollbar-thumb);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-scrollbar-horizontal {
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
height: var(--scrollbar-size);
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-thumb {
|
||||||
|
background-color: transparent;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-track {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
&::-webkit-scrollbar-thumb {
|
||||||
|
background-color: var(--color-scrollbar-thumb);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.simplebar-scrollbar.simplebar-visible::before {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.simplebar-scrollbar::before {
|
||||||
|
background: var(--scrollbar-handle-bg);
|
||||||
|
width: var(--scrollbar-size);
|
||||||
|
}
|
||||||
|
|
||||||
|
.simplebar-scrollbar.simplebar-visible.simplebar-hover {
|
||||||
|
&::before {
|
||||||
|
background: var(--scrollbar-handle-hover-bg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.rc-virtual-list-scrollbar {
|
||||||
|
width: var(--scrollbar-size) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-dropdown-menu
|
||||||
|
.ant-dropdown-menu-item.ant-dropdown-menu-item-danger:not(
|
||||||
|
.ant-dropdown-menu-item-disabled
|
||||||
|
):hover {
|
||||||
|
background-color: var(--ant-color-error-bg) !important;
|
||||||
|
color: var(--ant-color-error) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-image {
|
||||||
|
border-radius: var(--border-radius-base);
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ant-message-notice-wrapper {
|
.ant-message-notice-wrapper {
|
||||||
@@ -500,6 +668,12 @@ body {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =============== segment start==============
|
||||||
|
.ant-segmented .ant-segmented-item::after {
|
||||||
|
border-radius: var(--border-radius-mini);
|
||||||
|
}
|
||||||
|
// =============== segment end ===============
|
||||||
|
|
||||||
.background {
|
.background {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
@@ -535,6 +709,10 @@ body {
|
|||||||
|
|
||||||
.ant-pro-page-container {
|
.ant-pro-page-container {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
|
|
||||||
|
.ant-page-header-heading {
|
||||||
|
min-width: max-content;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes skeleton-loading {
|
@keyframes skeleton-loading {
|
||||||
@@ -560,6 +738,14 @@ body {
|
|||||||
|
|
||||||
.ant-modal {
|
.ant-modal {
|
||||||
max-width: 100vw;
|
max-width: 100vw;
|
||||||
|
|
||||||
|
.ant-modal-title {
|
||||||
|
font-weight: var(--font-weight-bold);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-drawer .ant-drawer-title {
|
||||||
|
font-weight: var(--font-weight-bold);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tooltip-wrapper {
|
.tooltip-wrapper {
|
||||||
|
|||||||
@@ -0,0 +1,137 @@
|
|||||||
|
import { split } from 'lodash';
|
||||||
|
import qs from 'query-string';
|
||||||
|
import { useEffect, useRef } from 'react';
|
||||||
|
|
||||||
|
interface RequestConfig {
|
||||||
|
url: string;
|
||||||
|
handler: (data: any) => any;
|
||||||
|
beforeReconnect?: () => void;
|
||||||
|
params?: object;
|
||||||
|
byLine?: boolean;
|
||||||
|
contentType?: 'json' | 'text';
|
||||||
|
}
|
||||||
|
|
||||||
|
const useSetChunkFetch = () => {
|
||||||
|
const axiosToken = useRef<any>(null);
|
||||||
|
const requestConfig = useRef<any>({});
|
||||||
|
const chunkDataRef = useRef<any>([]);
|
||||||
|
const bufferCacheRef = useRef<any>('');
|
||||||
|
|
||||||
|
const readTextEventStreamData = async (
|
||||||
|
reader: ReadableStreamDefaultReader<Uint8Array>,
|
||||||
|
decoder: TextDecoder,
|
||||||
|
callback: (data: any) => void
|
||||||
|
) => {
|
||||||
|
const { done, value } = await reader.read();
|
||||||
|
if (done) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const chunk = decoder.decode(value, { stream: true });
|
||||||
|
|
||||||
|
callback(chunk);
|
||||||
|
// console.log('chunkDataRef.current===2', chunkDataRef.current);
|
||||||
|
|
||||||
|
await readTextEventStreamData(reader, decoder, callback);
|
||||||
|
};
|
||||||
|
|
||||||
|
const readTextEventStreamDataByLine = async (
|
||||||
|
reader: ReadableStreamDefaultReader<Uint8Array>,
|
||||||
|
decoder: TextDecoder,
|
||||||
|
callback: (data: any) => void
|
||||||
|
) => {
|
||||||
|
const { done, value } = await reader.read();
|
||||||
|
if (done) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
bufferCacheRef.current += decoder.decode(value, { stream: true });
|
||||||
|
const lines = split(bufferCacheRef.current, /\r?\n/);
|
||||||
|
bufferCacheRef.current = lines.pop();
|
||||||
|
for (const line of lines) {
|
||||||
|
callback(line);
|
||||||
|
}
|
||||||
|
|
||||||
|
await readTextEventStreamDataByLine(reader, decoder, callback);
|
||||||
|
};
|
||||||
|
|
||||||
|
const readTextEventStreamDataByLineWithBuffer = async (
|
||||||
|
reader: ReadableStreamDefaultReader<Uint8Array>,
|
||||||
|
decoder: TextDecoder,
|
||||||
|
callback: (data: any) => void
|
||||||
|
) => {
|
||||||
|
await readTextEventStreamDataByLine(reader, decoder, callback);
|
||||||
|
|
||||||
|
if (bufferCacheRef.current.length > 0) {
|
||||||
|
callback(bufferCacheRef.current);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchChunkRequest = async ({
|
||||||
|
url,
|
||||||
|
handler,
|
||||||
|
byLine = false,
|
||||||
|
params = {}
|
||||||
|
}: RequestConfig) => {
|
||||||
|
axiosToken.current?.abort?.();
|
||||||
|
axiosToken.current = new AbortController();
|
||||||
|
try {
|
||||||
|
const response = await fetch(
|
||||||
|
`v1${url}?${qs.stringify({
|
||||||
|
...params,
|
||||||
|
watch: true
|
||||||
|
})}`,
|
||||||
|
{
|
||||||
|
method: 'GET',
|
||||||
|
body: null,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/octet-stream'
|
||||||
|
},
|
||||||
|
signal: axiosToken.current.signal
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const reader =
|
||||||
|
response?.body?.getReader() as ReadableStreamDefaultReader<Uint8Array>;
|
||||||
|
const decoder = new TextDecoder('utf-8');
|
||||||
|
|
||||||
|
await readTextEventStreamData(reader, decoder, handler);
|
||||||
|
|
||||||
|
console.log('chunkDataRef.current===1', chunkDataRef.current);
|
||||||
|
} catch (error) {
|
||||||
|
// handle error
|
||||||
|
console.log('error============', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return axiosToken.current;
|
||||||
|
};
|
||||||
|
|
||||||
|
const setChunkFetch = (config: RequestConfig) => {
|
||||||
|
requestConfig.current = { ...config };
|
||||||
|
fetchChunkRequest(requestConfig.current);
|
||||||
|
return axiosToken;
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleUnload = () => {
|
||||||
|
axiosToken.current?.abort?.();
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('beforeunload', handleUnload);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
axiosToken.current?.abort?.();
|
||||||
|
window.removeEventListener('beforeunload', handleUnload);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return {
|
||||||
|
setChunkFetch
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useSetChunkFetch;
|
||||||
@@ -172,7 +172,7 @@ const useSetChunkRequest = () => {
|
|||||||
|
|
||||||
let result = response;
|
let result = response;
|
||||||
let cres = '';
|
let cres = '';
|
||||||
console.log('chunkrequest============e==', e);
|
console.log('chunkrequest============e==', result);
|
||||||
if (contentType === 'json') {
|
if (contentType === 'json') {
|
||||||
const currentRes = sliceData(response, e.loaded, loadedSize);
|
const currentRes = sliceData(response, e.loaded, loadedSize);
|
||||||
result = parseData(currentRes);
|
result = parseData(currentRes);
|
||||||
@@ -194,7 +194,6 @@ const useSetChunkRequest = () => {
|
|||||||
retryCount.current -= 1;
|
retryCount.current -= 1;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log('error=============', error);
|
|
||||||
if (!axios.isCancel(error)) {
|
if (!axios.isCancel(error)) {
|
||||||
setRequestReadyState(4);
|
setRequestReadyState(4);
|
||||||
if (retryCount.current > 0) {
|
if (retryCount.current > 0) {
|
||||||
|
|||||||
@@ -0,0 +1,119 @@
|
|||||||
|
import { throttle } from 'lodash';
|
||||||
|
import {
|
||||||
|
UseOverlayScrollbarsParams,
|
||||||
|
useOverlayScrollbars
|
||||||
|
} from 'overlayscrollbars-react';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
export const overlaySollerOptions: UseOverlayScrollbarsParams = {
|
||||||
|
options: {
|
||||||
|
update: {
|
||||||
|
debounce: 0
|
||||||
|
},
|
||||||
|
overflow: {
|
||||||
|
x: 'hidden'
|
||||||
|
},
|
||||||
|
scrollbars: {
|
||||||
|
theme: 'os-theme-light',
|
||||||
|
autoHide: 'scroll',
|
||||||
|
autoHideDelay: 600,
|
||||||
|
clickScroll: 'instant'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
defer: true
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function useOverlayScroller(options?: any) {
|
||||||
|
const scrollEventElement = React.useRef<any>(null);
|
||||||
|
const instanceRef = React.useRef<any>(null);
|
||||||
|
const initialized = React.useRef(false);
|
||||||
|
const [initialize, instance] = useOverlayScrollbars({
|
||||||
|
options: {
|
||||||
|
update: {
|
||||||
|
debounce: 0
|
||||||
|
},
|
||||||
|
overflow: {
|
||||||
|
x: 'hidden'
|
||||||
|
},
|
||||||
|
scrollbars: {
|
||||||
|
theme: options?.theme || 'os-theme-dark',
|
||||||
|
autoHide: 'scroll',
|
||||||
|
autoHideDelay: 600,
|
||||||
|
clickScroll: 'instant'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
defer: true
|
||||||
|
});
|
||||||
|
|
||||||
|
instanceRef.current = instance?.();
|
||||||
|
scrollEventElement.current =
|
||||||
|
instanceRef.current?.elements()?.scrollEventElement;
|
||||||
|
|
||||||
|
const throttledScroll = React.useCallback(
|
||||||
|
throttle(() => {
|
||||||
|
scrollEventElement.current?.scrollTo?.({
|
||||||
|
top: scrollEventElement.current.scrollHeight,
|
||||||
|
behavior: 'smooth'
|
||||||
|
});
|
||||||
|
instanceRef.current?.update?.();
|
||||||
|
}, 100),
|
||||||
|
[(scrollEventElement.current, instanceRef.current)]
|
||||||
|
);
|
||||||
|
|
||||||
|
const scrollauto = React.useCallback(() => {
|
||||||
|
scrollEventElement.current?.scrollTo?.({
|
||||||
|
top: scrollEventElement.current.scrollHeight,
|
||||||
|
behavior: 'auto'
|
||||||
|
});
|
||||||
|
instanceRef.current?.update?.();
|
||||||
|
}, [scrollEventElement.current, instanceRef.current]);
|
||||||
|
|
||||||
|
const throttledUpdateScrollerPosition = React.useCallback(
|
||||||
|
(delay?: number) => {
|
||||||
|
if (delay === 0) {
|
||||||
|
scrollauto();
|
||||||
|
} else {
|
||||||
|
throttledScroll();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[throttledScroll, scrollauto]
|
||||||
|
);
|
||||||
|
|
||||||
|
const generateInstance = () => {
|
||||||
|
instanceRef.current = instance?.();
|
||||||
|
scrollEventElement.current =
|
||||||
|
instanceRef.current?.elements()?.scrollEventElement;
|
||||||
|
};
|
||||||
|
|
||||||
|
const createInstance = React.useCallback(
|
||||||
|
(el: any) => {
|
||||||
|
if (instanceRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (el) {
|
||||||
|
initialize(el);
|
||||||
|
initialized.current = true;
|
||||||
|
console.log(
|
||||||
|
'createInstance===2',
|
||||||
|
initialized.current,
|
||||||
|
instanceRef.current,
|
||||||
|
instance?.(),
|
||||||
|
instance
|
||||||
|
);
|
||||||
|
instanceRef.current = instance?.();
|
||||||
|
scrollEventElement.current =
|
||||||
|
instanceRef.current?.elements()?.scrollEventElement;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[initialize, instance]
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
initialize: createInstance,
|
||||||
|
instance: instanceRef.current,
|
||||||
|
scrollEventElement: scrollEventElement.current,
|
||||||
|
initialized: initialized.current,
|
||||||
|
generateInstance,
|
||||||
|
updateScrollerPosition: throttledUpdateScrollerPosition
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -11,10 +11,15 @@ export default function useTableRowSelection() {
|
|||||||
setSelectedRowKeys([]);
|
setSelectedRowKeys([]);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const removeSelectedKey = (rowKey: React.Key) => {
|
||||||
|
setSelectedRowKeys((keys) => keys.filter((key) => key !== rowKey));
|
||||||
|
};
|
||||||
|
|
||||||
const rowSelection = {
|
const rowSelection = {
|
||||||
selectedRowKeys,
|
selectedRowKeys,
|
||||||
clearSelections,
|
clearSelections,
|
||||||
onChange: onSelectChange
|
onChange: onSelectChange,
|
||||||
|
removeSelectedKey
|
||||||
};
|
};
|
||||||
|
|
||||||
return rowSelection;
|
return rowSelection;
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { WatchEventType } from '@/config';
|
import { WatchEventType } from '@/config';
|
||||||
import _ from 'lodash';
|
|
||||||
import { useEffect, useRef } from 'react';
|
import { useEffect, useRef } from 'react';
|
||||||
|
|
||||||
interface ChunkedCollection {
|
interface ChunkedCollection {
|
||||||
@@ -11,6 +10,7 @@ interface ChunkedCollection {
|
|||||||
// Only used to update lists without nested state
|
// Only used to update lists without nested state
|
||||||
export function useUpdateChunkedList(options: {
|
export function useUpdateChunkedList(options: {
|
||||||
dataList?: any[];
|
dataList?: any[];
|
||||||
|
limit?: number;
|
||||||
setDataList: (args: any) => void;
|
setDataList: (args: any) => void;
|
||||||
callback?: (args: any) => void;
|
callback?: (args: any) => void;
|
||||||
filterFun?: (args: any) => boolean;
|
filterFun?: (args: any) => boolean;
|
||||||
@@ -19,6 +19,8 @@ export function useUpdateChunkedList(options: {
|
|||||||
}) {
|
}) {
|
||||||
const cacheDataListRef = useRef<any[]>(options.dataList || []);
|
const cacheDataListRef = useRef<any[]>(options.dataList || []);
|
||||||
const timerRef = useRef<any>(null);
|
const timerRef = useRef<any>(null);
|
||||||
|
const countRef = useRef<number>(0);
|
||||||
|
const limit = options.limit || 10;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
cacheDataListRef.current = [...(options.dataList || [])];
|
cacheDataListRef.current = [...(options.dataList || [])];
|
||||||
@@ -30,74 +32,71 @@ export function useUpdateChunkedList(options: {
|
|||||||
options.setDataList?.([...cacheDataListRef.current]);
|
options.setDataList?.([...cacheDataListRef.current]);
|
||||||
}, 80);
|
}, 80);
|
||||||
};
|
};
|
||||||
const updateChunkedList = (
|
const updateChunkedList = (data: ChunkedCollection) => {
|
||||||
data: ChunkedCollection,
|
|
||||||
dataList?: { id: string | number }[]
|
|
||||||
) => {
|
|
||||||
console.log('updateChunkedList=====', {
|
console.log('updateChunkedList=====', {
|
||||||
ids: data?.ids,
|
ids: data?.ids,
|
||||||
type: data?.type,
|
type: data?.type,
|
||||||
collection: data?.collection.length,
|
collection: data?.collection
|
||||||
dataList: _.map(dataList, (o: any) => o.id)
|
|
||||||
});
|
});
|
||||||
let collections = data?.collection || [];
|
let collections = data?.collection || [];
|
||||||
if (options?.computedID) {
|
if (options?.computedID) {
|
||||||
collections = _.map(collections, (item: any) => {
|
collections = collections?.map((item: any) => {
|
||||||
item.id = options?.computedID?.(item);
|
item.id = options?.computedID?.(item);
|
||||||
return item;
|
return item;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (options?.filterFun) {
|
if (options?.filterFun) {
|
||||||
collections = _.filter(data?.collection, options?.filterFun);
|
collections = data?.collection?.filter(options?.filterFun);
|
||||||
}
|
}
|
||||||
if (options?.mapFun) {
|
if (options?.mapFun) {
|
||||||
collections = _.map(data?.collection, options?.mapFun);
|
collections = data?.collection?.map(options?.mapFun);
|
||||||
}
|
}
|
||||||
const ids = data?.ids || [];
|
const ids = data?.ids || [];
|
||||||
// CREATE
|
// CREATE
|
||||||
if (data?.type === WatchEventType.CREATE) {
|
if (data?.type === WatchEventType.CREATE) {
|
||||||
const newDataList: any[] = [];
|
const newDataList = collections.reduce((acc: any[], item: any) => {
|
||||||
_.each(collections, (item: any) => {
|
const updateIndex = cacheDataListRef.current?.findIndex(
|
||||||
const updateIndex = _.findIndex(
|
|
||||||
cacheDataListRef.current,
|
|
||||||
(sItem: any) => sItem.id === item.id
|
(sItem: any) => sItem.id === item.id
|
||||||
);
|
);
|
||||||
|
const updateItem = { ...item };
|
||||||
if (updateIndex === -1) {
|
if (updateIndex === -1) {
|
||||||
const updateItem = _.cloneDeep(item);
|
acc.push(updateItem);
|
||||||
newDataList.push(updateItem);
|
} else {
|
||||||
|
cacheDataListRef.current[updateIndex] = updateItem;
|
||||||
}
|
}
|
||||||
console.log('create=========', updateIndex, collections);
|
|
||||||
});
|
return acc;
|
||||||
cacheDataListRef.current = [...newDataList, ...cacheDataListRef.current];
|
}, []);
|
||||||
// options.setDataList?.([...cacheDataListRef.current]);
|
cacheDataListRef.current = [
|
||||||
|
...newDataList,
|
||||||
|
...cacheDataListRef.current
|
||||||
|
].slice(0, limit);
|
||||||
}
|
}
|
||||||
// DELETE
|
// DELETE
|
||||||
if (data?.type === WatchEventType.DELETE) {
|
if (data?.type === WatchEventType.DELETE) {
|
||||||
cacheDataListRef.current = _.filter(
|
cacheDataListRef.current = cacheDataListRef.current?.filter(
|
||||||
cacheDataListRef.current,
|
|
||||||
(item: any) => {
|
(item: any) => {
|
||||||
return !_.includes(ids, item.id);
|
return !ids?.includes(item.id);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
options.setDataList?.([...cacheDataListRef.current]);
|
options.setDataList?.([...cacheDataListRef.current]);
|
||||||
}
|
}
|
||||||
// UPDATE
|
// UPDATE
|
||||||
if (data?.type === WatchEventType.UPDATE) {
|
if (data?.type === WatchEventType.UPDATE) {
|
||||||
_.each(collections, (item: any) => {
|
collections?.forEach((item: any) => {
|
||||||
const updateIndex = _.findIndex(
|
const updateIndex = cacheDataListRef.current?.findIndex(
|
||||||
cacheDataListRef.current,
|
|
||||||
(sItem: any) => sItem.id === item.id
|
(sItem: any) => sItem.id === item.id
|
||||||
);
|
);
|
||||||
|
const updateItem = { ...item };
|
||||||
if (updateIndex > -1) {
|
if (updateIndex > -1) {
|
||||||
const updateItem = _.cloneDeep(item);
|
|
||||||
cacheDataListRef.current[updateIndex] = updateItem;
|
cacheDataListRef.current[updateIndex] = updateItem;
|
||||||
} else if (updateIndex === -1) {
|
} else if (updateIndex === -1) {
|
||||||
const updateItem = _.cloneDeep(item);
|
cacheDataListRef.current = [
|
||||||
cacheDataListRef.current.push(updateItem);
|
updateItem,
|
||||||
|
...cacheDataListRef.current.slice(0, limit - 1)
|
||||||
|
];
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
console.log('updateChunkedList=====update', cacheDataListRef.current);
|
|
||||||
// options.setDataList?.([...cacheDataListRef.current]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
debounceUpdateChunckedList();
|
debounceUpdateChunckedList();
|
||||||
|
|||||||
+94
-23
@@ -1,13 +1,15 @@
|
|||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
|
|
||||||
import { userAtom } from '@/atoms/user';
|
import { GPUStackVersionAtom, UpdateCheckAtom, userAtom } from '@/atoms/user';
|
||||||
import ShortCuts, {
|
import ShortCuts, {
|
||||||
modalConfig as ShortCutsConfig
|
modalConfig as ShortCutsConfig
|
||||||
} from '@/components/short-cuts';
|
} from '@/components/short-cuts';
|
||||||
import VersionInfo, { modalConfig } from '@/components/version-info';
|
import VersionInfo, { modalConfig } from '@/components/version-info';
|
||||||
|
import useOverlayScroller from '@/hooks/use-overlay-scroller';
|
||||||
import { logout } from '@/pages/login/apis';
|
import { logout } from '@/pages/login/apis';
|
||||||
import { useAccessMarkedRoutes } from '@@/plugin-access';
|
import { useAccessMarkedRoutes } from '@@/plugin-access';
|
||||||
import { useModel } from '@@/plugin-model';
|
import { useModel } from '@@/plugin-model';
|
||||||
|
import { MenuFoldOutlined, MenuUnfoldOutlined } from '@ant-design/icons';
|
||||||
import { ProLayout } from '@ant-design/pro-components';
|
import { ProLayout } from '@ant-design/pro-components';
|
||||||
import {
|
import {
|
||||||
Link,
|
Link,
|
||||||
@@ -20,14 +22,16 @@ import {
|
|||||||
useNavigate,
|
useNavigate,
|
||||||
type IRoute
|
type IRoute
|
||||||
} from '@umijs/max';
|
} from '@umijs/max';
|
||||||
import { Modal } from 'antd';
|
import { Button, Modal } from 'antd';
|
||||||
import { useAtom } from 'jotai';
|
import { useAtom } from 'jotai';
|
||||||
import { useMemo, useState } from 'react';
|
import 'overlayscrollbars/overlayscrollbars.css';
|
||||||
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import Exception from './Exception';
|
import Exception from './Exception';
|
||||||
import './Layout.css';
|
import './Layout.css';
|
||||||
import { LogoIcon, SLogoIcon } from './Logo';
|
import { LogoIcon, SLogoIcon } from './Logo';
|
||||||
import { getRightRenderContent } from './rightRender';
|
import { getRightRenderContent } from './rightRender';
|
||||||
import { patchRoutes } from './runtime';
|
import { patchRoutes } from './runtime';
|
||||||
|
|
||||||
const loginPath = '/login';
|
const loginPath = '/login';
|
||||||
|
|
||||||
type InitialStateType = {
|
type InitialStateType = {
|
||||||
@@ -86,12 +90,17 @@ const mapRoutes = (routes: IRoute[], role: string) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default (props: any) => {
|
export default (props: any) => {
|
||||||
|
const { initialize: initialize } = useOverlayScroller();
|
||||||
|
const { initialize: initializeMenu } = useOverlayScroller();
|
||||||
const [userInfo] = useAtom(userAtom);
|
const [userInfo] = useAtom(userAtom);
|
||||||
|
const [version] = useAtom(GPUStackVersionAtom);
|
||||||
|
const [updateCheck] = useAtom(UpdateCheckAtom);
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const { clientRoutes, pluginManager } = useAppData();
|
const { clientRoutes, pluginManager } = useAppData();
|
||||||
const [collapsed, setCollapsed] = useState(false);
|
const [collapsed, setCollapsed] = useState(false);
|
||||||
|
const [collapseValue, setCollapseValue] = useState(false);
|
||||||
|
|
||||||
const initialInfo = (useModel && useModel('@@initialState')) || {
|
const initialInfo = (useModel && useModel('@@initialState')) || {
|
||||||
initialState: undefined,
|
initialState: undefined,
|
||||||
@@ -113,6 +122,7 @@ export default (props: any) => {
|
|||||||
const showVersion = () => {
|
const showVersion = () => {
|
||||||
Modal.info({
|
Modal.info({
|
||||||
...modalConfig,
|
...modalConfig,
|
||||||
|
width: 460,
|
||||||
content: <VersionInfo intl={intl} />
|
content: <VersionInfo intl={intl} />
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -139,6 +149,11 @@ export default (props: any) => {
|
|||||||
notFound: <span>404 not found</span>
|
notFound: <span>404 not found</span>
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleToggleCollapse = (e: any) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setCollapsed(!collapsed);
|
||||||
|
};
|
||||||
|
|
||||||
const newRoutes = filterRoutes(
|
const newRoutes = filterRoutes(
|
||||||
clientRoutes.filter((route) => route.id === '@@/global-layout'),
|
clientRoutes.filter((route) => route.id === '@@/global-layout'),
|
||||||
(route) => {
|
(route) => {
|
||||||
@@ -151,7 +166,6 @@ export default (props: any) => {
|
|||||||
|
|
||||||
const role = initialState?.currentUser?.is_admin ? 'admin' : 'user';
|
const role = initialState?.currentUser?.is_admin ? 'admin' : 'user';
|
||||||
const [route] = useAccessMarkedRoutes(mapRoutes(newRoutes, role));
|
const [route] = useAccessMarkedRoutes(mapRoutes(newRoutes, role));
|
||||||
console.log('clientRoutes===========', route, clientRoutes, newRoutes);
|
|
||||||
|
|
||||||
patchRoutes({
|
patchRoutes({
|
||||||
routes: route.children,
|
routes: route.children,
|
||||||
@@ -162,7 +176,77 @@ export default (props: any) => {
|
|||||||
() => matchRoutes(route?.children || [], location.pathname)?.pop?.()?.route,
|
() => matchRoutes(route?.children || [], location.pathname)?.pop?.()?.route,
|
||||||
[location.pathname]
|
[location.pathname]
|
||||||
);
|
);
|
||||||
console.log('route===========', matchedRoute, route);
|
|
||||||
|
const showUpgrade = useMemo(() => {
|
||||||
|
return (
|
||||||
|
initialState?.currentUser?.is_admin &&
|
||||||
|
updateCheck.latest_version &&
|
||||||
|
updateCheck.latest_version !== version?.version
|
||||||
|
);
|
||||||
|
}, [updateCheck, version, initialState]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const body = document.querySelector('body');
|
||||||
|
if (body) {
|
||||||
|
initialize(body);
|
||||||
|
}
|
||||||
|
}, [initialize]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const menuWrap = document.querySelector(
|
||||||
|
'.ant-menu.ant-menu-root'
|
||||||
|
)?.parentElement;
|
||||||
|
if (menuWrap) {
|
||||||
|
initializeMenu(menuWrap);
|
||||||
|
}
|
||||||
|
}, [initializeMenu]);
|
||||||
|
|
||||||
|
const renderMenuHeader = useCallback(
|
||||||
|
(logo, title) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{logo}
|
||||||
|
<div className="collapse-wrap">
|
||||||
|
<Button
|
||||||
|
style={{ marginRight: collapsed ? 0 : -14 }}
|
||||||
|
size="small"
|
||||||
|
type={collapsed ? 'default' : 'text'}
|
||||||
|
onClick={handleToggleCollapse}
|
||||||
|
>
|
||||||
|
<>
|
||||||
|
<MenuUnfoldOutlined
|
||||||
|
style={{ display: collapsed ? 'block' : 'none' }}
|
||||||
|
/>
|
||||||
|
<MenuFoldOutlined
|
||||||
|
style={{ display: !collapsed ? 'block' : 'none' }}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[collapsed]
|
||||||
|
);
|
||||||
|
|
||||||
|
const actionRender = useCallback(
|
||||||
|
(layoutProps) => {
|
||||||
|
const dom = getRightRenderContent({
|
||||||
|
runtimeConfig,
|
||||||
|
loading,
|
||||||
|
initialState,
|
||||||
|
setInitialState,
|
||||||
|
intl,
|
||||||
|
siderWidth: layoutProps.siderWidth,
|
||||||
|
collapsed: layoutProps.collapsed,
|
||||||
|
showUpgrade
|
||||||
|
});
|
||||||
|
|
||||||
|
return dom;
|
||||||
|
},
|
||||||
|
[intl, version, updateCheck]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="background"></div>
|
<div className="background"></div>
|
||||||
@@ -174,20 +258,19 @@ export default (props: any) => {
|
|||||||
layout="side"
|
layout="side"
|
||||||
disableMobile={true}
|
disableMobile={true}
|
||||||
siderWidth={220}
|
siderWidth={220}
|
||||||
|
onCollapse={(collapsed) => {
|
||||||
|
setCollapsed(collapsed);
|
||||||
|
}}
|
||||||
onMenuHeaderClick={(e) => {
|
onMenuHeaderClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
navigate('/');
|
navigate('/');
|
||||||
}}
|
}}
|
||||||
|
menuHeaderRender={renderMenuHeader}
|
||||||
collapsed={collapsed}
|
collapsed={collapsed}
|
||||||
onCollapse={(collapsed) => {
|
|
||||||
setCollapsed(collapsed);
|
|
||||||
}}
|
|
||||||
onPageChange={(route) => {
|
onPageChange={(route) => {
|
||||||
const { location } = history;
|
const { location } = history;
|
||||||
|
|
||||||
console.log('onPageChange', userInfo, initialState);
|
|
||||||
|
|
||||||
// if user is not change password, redirect to change password page
|
// if user is not change password, redirect to change password page
|
||||||
if (
|
if (
|
||||||
location.pathname !== loginPath &&
|
location.pathname !== loginPath &&
|
||||||
@@ -242,19 +325,7 @@ export default (props: any) => {
|
|||||||
fixSiderbar
|
fixSiderbar
|
||||||
fixedHeader
|
fixedHeader
|
||||||
{...runtimeConfig}
|
{...runtimeConfig}
|
||||||
actionsRender={(layoutProps) => {
|
actionsRender={actionRender}
|
||||||
const dom = getRightRenderContent({
|
|
||||||
runtimeConfig,
|
|
||||||
loading,
|
|
||||||
initialState,
|
|
||||||
setInitialState,
|
|
||||||
intl,
|
|
||||||
siderWidth: layoutProps.siderWidth,
|
|
||||||
collapsed: layoutProps.collapsed
|
|
||||||
});
|
|
||||||
|
|
||||||
return dom;
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<Exception
|
<Exception
|
||||||
route={matchedRoute}
|
route={matchedRoute}
|
||||||
|
|||||||
+36
-17
@@ -1,5 +1,4 @@
|
|||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
|
|
||||||
import avatarImg from '@/assets/images/avatar.png';
|
import avatarImg from '@/assets/images/avatar.png';
|
||||||
import externalLinks from '@/constants/external-links';
|
import externalLinks from '@/constants/external-links';
|
||||||
import langConfigMap from '@/locales/lang-config-map';
|
import langConfigMap from '@/locales/lang-config-map';
|
||||||
@@ -18,7 +17,7 @@ import { getAllLocales, history, setLocale } from '@umijs/max';
|
|||||||
import { Avatar, Menu, Spin } from 'antd';
|
import { Avatar, Menu, Spin } from 'antd';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
|
|
||||||
export function getRightRenderContent(opts: {
|
export const getRightRenderContent = (opts: {
|
||||||
runtimeConfig: any;
|
runtimeConfig: any;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
initialState: any;
|
initialState: any;
|
||||||
@@ -26,8 +25,8 @@ export function getRightRenderContent(opts: {
|
|||||||
setInitialState: any;
|
setInitialState: any;
|
||||||
siderWidth: number;
|
siderWidth: number;
|
||||||
intl: any;
|
intl: any;
|
||||||
}) {
|
}) => {
|
||||||
const { intl, collapsed, siderWidth } = opts;
|
const { intl, collapsed, siderWidth, showUpgrade } = opts;
|
||||||
|
|
||||||
const allLocals = getAllLocales();
|
const allLocals = getAllLocales();
|
||||||
if (opts.runtimeConfig.rightRender) {
|
if (opts.runtimeConfig.rightRender) {
|
||||||
@@ -100,11 +99,6 @@ export function getRightRenderContent(opts: {
|
|||||||
icon: <InfoCircleOutlined />,
|
icon: <InfoCircleOutlined />,
|
||||||
label: intl.formatMessage({ id: 'common.button.version' })
|
label: intl.formatMessage({ id: 'common.button.version' })
|
||||||
}
|
}
|
||||||
// {
|
|
||||||
// key: 'shortcuts',
|
|
||||||
// icon: <IconFont type="icon-keyboard"></IconFont>,
|
|
||||||
// label: intl.formatMessage({ id: 'common.button.shortcut' })
|
|
||||||
// }
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const helpMenu = {
|
const helpMenu = {
|
||||||
@@ -114,14 +108,30 @@ export function getRightRenderContent(opts: {
|
|||||||
: 'user-menu-container',
|
: 'user-menu-container',
|
||||||
mode: 'vertical',
|
mode: 'vertical',
|
||||||
expandIcon: false,
|
expandIcon: false,
|
||||||
inlineCollapsed: collapsed,
|
// inlineCollapsed: collapsed,
|
||||||
triggerSubMenuAction: 'hover',
|
triggerSubMenuAction: 'hover',
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
key: 'help',
|
key: 'help',
|
||||||
icon: <QuestionCircleOutlined />,
|
icon: <QuestionCircleOutlined />,
|
||||||
label: (
|
label: (
|
||||||
|
<span className="sub-title ">
|
||||||
|
<span className="flex-center">
|
||||||
<span>{intl?.formatMessage?.({ id: 'common.button.help' })}</span>
|
<span>{intl?.formatMessage?.({ id: 'common.button.help' })}</span>
|
||||||
|
{showUpgrade && (
|
||||||
|
<span
|
||||||
|
className="m-l-5"
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
width: 8,
|
||||||
|
height: 8,
|
||||||
|
borderRadius: 4,
|
||||||
|
backgroundColor: 'var(--ant-orange-5)'
|
||||||
|
}}
|
||||||
|
></span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
),
|
),
|
||||||
children: helpList.map((item) => ({
|
children: helpList.map((item) => ({
|
||||||
key: item.key,
|
key: item.key,
|
||||||
@@ -129,7 +139,16 @@ export function getRightRenderContent(opts: {
|
|||||||
<span className="flex flex-center">
|
<span className="flex flex-center">
|
||||||
{item.icon}
|
{item.icon}
|
||||||
{item.key === 'version' ? (
|
{item.key === 'version' ? (
|
||||||
|
<>
|
||||||
<a className="m-l-8">{item.label}</a>
|
<a className="m-l-8">{item.label}</a>
|
||||||
|
{showUpgrade && (
|
||||||
|
<span className="new-icon">
|
||||||
|
<span className="text">
|
||||||
|
{intl.formatMessage({ id: 'common.text.new' })}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
) : (
|
) : (
|
||||||
<a
|
<a
|
||||||
className="m-l-8"
|
className="m-l-8"
|
||||||
@@ -162,14 +181,14 @@ export function getRightRenderContent(opts: {
|
|||||||
: 'user-menu-container',
|
: 'user-menu-container',
|
||||||
mode: 'vertical',
|
mode: 'vertical',
|
||||||
expandIcon: false,
|
expandIcon: false,
|
||||||
inlineCollapsed: collapsed,
|
// inlineCollapsed: collapsed,
|
||||||
triggerSubMenuAction: 'hover',
|
triggerSubMenuAction: 'hover',
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
key: 'lang',
|
key: 'lang',
|
||||||
icon: <GlobalOutlined />,
|
icon: <GlobalOutlined />,
|
||||||
label: (
|
label: (
|
||||||
<span>
|
<span className="sub-title">
|
||||||
{intl?.formatMessage?.({ id: 'common.settings.language' })}
|
{intl?.formatMessage?.({ id: 'common.settings.language' })}
|
||||||
</span>
|
</span>
|
||||||
),
|
),
|
||||||
@@ -195,7 +214,7 @@ export function getRightRenderContent(opts: {
|
|||||||
: 'user-menu-container',
|
: 'user-menu-container',
|
||||||
mode: 'vertical',
|
mode: 'vertical',
|
||||||
expandIcon: false,
|
expandIcon: false,
|
||||||
inlineCollapsed: collapsed,
|
// inlineCollapsed: collapsed,
|
||||||
triggerSubMenuAction: 'hover',
|
triggerSubMenuAction: 'hover',
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
@@ -262,22 +281,22 @@ export function getRightRenderContent(opts: {
|
|||||||
<Menu
|
<Menu
|
||||||
{...helpMenu}
|
{...helpMenu}
|
||||||
style={{
|
style={{
|
||||||
width: collapsed ? 64 : `calc(${siderWidth}px - 16px)`
|
width: collapsed ? 40 : `calc(${siderWidth}px - 16px)`
|
||||||
}}
|
}}
|
||||||
></Menu>
|
></Menu>
|
||||||
<Menu
|
<Menu
|
||||||
{...langMenu}
|
{...langMenu}
|
||||||
style={{
|
style={{
|
||||||
width: collapsed ? 64 : `calc(${siderWidth}px - 16px)`
|
width: collapsed ? 40 : `calc(${siderWidth}px - 16px)`
|
||||||
}}
|
}}
|
||||||
></Menu>
|
></Menu>
|
||||||
<Menu
|
<Menu
|
||||||
{...userMenu}
|
{...userMenu}
|
||||||
style={{
|
style={{
|
||||||
width: collapsed ? 64 : `calc(${siderWidth}px - 16px)`,
|
width: collapsed ? 40 : `calc(${siderWidth}px - 16px)`,
|
||||||
marginTop: 20
|
marginTop: 20
|
||||||
}}
|
}}
|
||||||
></Menu>
|
></Menu>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
+17
-22
@@ -1,23 +1,18 @@
|
|||||||
import apikeys from './en-US/apikeys';
|
/**
|
||||||
import common from './en-US/common';
|
* @description The directory name used in require.context must match the language configuration names defined in lang-config-map.ts.
|
||||||
import dashboard from './en-US/dashboard';
|
* @example Directories like 'en-US' or 'zh-CN' should correspond exactly to the configuration names in lang-config-map.ts.
|
||||||
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 shortcuts from './en-US/shortcuts';
|
|
||||||
import usage from './en-US/usage';
|
|
||||||
import users from './en-US/users';
|
|
||||||
|
|
||||||
export default {
|
const requireContext = require.context(`./en-US`, false, /\.ts$/);
|
||||||
...common,
|
|
||||||
...menu,
|
let languageConfig: Record<string, string> = {};
|
||||||
...models,
|
|
||||||
...playground,
|
requireContext.keys().forEach((fileName: any) => {
|
||||||
...resources,
|
const moduleConfig = requireContext(fileName).default;
|
||||||
...apikeys,
|
languageConfig = {
|
||||||
...users,
|
...languageConfig,
|
||||||
...dashboard,
|
...moduleConfig
|
||||||
...usage,
|
};
|
||||||
...shortcuts
|
});
|
||||||
};
|
|
||||||
|
export default languageConfig;
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ export default {
|
|||||||
'common.tips.title': 'Tips:',
|
'common.tips.title': 'Tips:',
|
||||||
'common.input.range.start': 'start value',
|
'common.input.range.start': 'start value',
|
||||||
'common.input.range.end': 'end value',
|
'common.input.range.end': 'end value',
|
||||||
'common.footer.version': 'Versions',
|
'common.footer.version': 'Version',
|
||||||
'common.footer.version.title': 'Version Information',
|
'common.footer.version.title': 'Version Information',
|
||||||
'common.footer.version.web': 'UI',
|
'common.footer.version.web': 'UI',
|
||||||
'common.footer.version.server': 'Server',
|
'common.footer.version.server': 'Server',
|
||||||
@@ -113,9 +113,9 @@ export default {
|
|||||||
'common.button.logs': 'Logs',
|
'common.button.logs': 'Logs',
|
||||||
'common.ws.close': 'The connection was lost, do you want to reconnect?',
|
'common.ws.close': 'The connection was lost, do you want to reconnect?',
|
||||||
'common.ws.reconnect': 'Reconnect',
|
'common.ws.reconnect': 'Reconnect',
|
||||||
'common.input.key': 'key',
|
'common.input.key': 'Key',
|
||||||
'common.input.value': 'value',
|
'common.input.value': 'Value',
|
||||||
'common.input.type': 'type',
|
'common.input.type': 'Type',
|
||||||
'common.input.visible': 'visibility',
|
'common.input.visible': 'visibility',
|
||||||
'common.input.description': 'description',
|
'common.input.description': 'description',
|
||||||
'common.time.day': 'day',
|
'common.time.day': 'day',
|
||||||
@@ -205,5 +205,10 @@ export default {
|
|||||||
'common.button.version': 'Version',
|
'common.button.version': 'Version',
|
||||||
'common.title.delete.confirm': 'Confirm delete',
|
'common.title.delete.confirm': 'Confirm delete',
|
||||||
'common.button.addLabel': 'Add Labels',
|
'common.button.addLabel': 'Add Labels',
|
||||||
'common.button.addSelector': 'Add Selectors'
|
'common.button.addSelector': 'Add Selectors',
|
||||||
|
'common.button.addParams': 'Add Parameter',
|
||||||
|
'common.text.here': 'here',
|
||||||
|
'common.text.latest': 'Latest',
|
||||||
|
'common.text.new': 'New',
|
||||||
|
'common.text.changelog': 'Release Notes'
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
export default {
|
export default {
|
||||||
'menu.dashboard': 'Dashboard',
|
'menu.dashboard': 'Dashboard',
|
||||||
'menu.playground': 'Playground',
|
'menu.playground': 'Playground',
|
||||||
|
'menu.chat': 'Chat',
|
||||||
|
'menu.compare': 'Compare',
|
||||||
'menu.models': 'Models',
|
'menu.models': 'Models',
|
||||||
'menu.resources': 'Resources',
|
'menu.resources': 'Resources',
|
||||||
'menu.apikeys': 'API Keys',
|
'menu.apikeys': 'API Keys',
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ export default {
|
|||||||
'model.form.ollama.model': 'Ollama Model',
|
'model.form.ollama.model': 'Ollama Model',
|
||||||
'model.form.ollamaholder': 'Please select or input model name',
|
'model.form.ollamaholder': 'Please select or input model name',
|
||||||
'model.deploy.sort': 'Sort',
|
'model.deploy.sort': 'Sort',
|
||||||
'model.deploy.search.placeholder': 'Search models from Hugging Face',
|
'model.deploy.search.placeholder': 'Search models from {source}',
|
||||||
'model.form.ollamatips':
|
'model.form.ollamatips':
|
||||||
'Tip: The following are the preconfigured Ollama models in GPUStack. Please select the model you want, or directly enter the model you wish to deploy in the 【{name}】 input box on the right.',
|
'Tip: The following are the preconfigured Ollama models in GPUStack. Please select the model you want, or directly enter the model you wish to deploy in the 【{name}】 input box on the right.',
|
||||||
'models.sort.name': 'Name',
|
'models.sort.name': 'Name',
|
||||||
@@ -35,6 +35,7 @@ export default {
|
|||||||
'models.data.card': 'Model Card',
|
'models.data.card': 'Model Card',
|
||||||
'models.available.files': 'Available Files',
|
'models.available.files': 'Available Files',
|
||||||
'models.viewin.hf': 'View in Hugging Face',
|
'models.viewin.hf': 'View in Hugging Face',
|
||||||
|
'models.viewin.modelscope': 'View in ModelScope',
|
||||||
'models.architecture': 'Architecture',
|
'models.architecture': 'Architecture',
|
||||||
'models.search.noresult': 'No related models found',
|
'models.search.noresult': 'No related models found',
|
||||||
'models.search.nofiles': 'No available files',
|
'models.search.nofiles': 'No available files',
|
||||||
@@ -52,7 +53,17 @@ export default {
|
|||||||
'models.form.manual.schedule': 'Manual Schedule',
|
'models.form.manual.schedule': 'Manual Schedule',
|
||||||
'models.table.gpuindex': 'GPU Index',
|
'models.table.gpuindex': 'GPU Index',
|
||||||
'models.table.backend': 'Backends',
|
'models.table.backend': 'Backends',
|
||||||
'models.table.acrossworker': 'Distribution Across Workers',
|
'models.table.acrossworker': 'Distributed Across Workers',
|
||||||
'models.table.cpuoffload': 'CPU Offload',
|
'models.table.cpuoffload': 'CPU Offload',
|
||||||
'models.table.layers': 'Layers'
|
'models.table.layers': 'Layers',
|
||||||
|
'models.form.backend': 'Backend',
|
||||||
|
'models.form.backend_parameters': 'Backend Parameters',
|
||||||
|
'models.search.gguf.tips': '1. GGUF model backend is llama-box(llama.cpp).',
|
||||||
|
'models.search.vllm.tips': '2. Non-GGUF model backend is vLLM.',
|
||||||
|
'models.form.ollamalink': 'Find More in Ollama Library',
|
||||||
|
'models.form.backend_parameters.llamabox.placeholder':
|
||||||
|
'e.g., --ctx-size=8192',
|
||||||
|
'models.form.backend_parameters.vllm.placeholder':
|
||||||
|
'e.g., --max-model-len=8192',
|
||||||
|
'models.form.backend_parameters.vllm.tips': 'More {backend} parameter details'
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
export default {
|
export default {
|
||||||
'playground.system.tips': 'Enter system message here',
|
'playground.system.tips': 'Type system instructions here',
|
||||||
'playground.title': 'Playground',
|
'playground.title': 'Playground',
|
||||||
'playground.system': 'System',
|
'playground.system': 'System',
|
||||||
|
'playground.systemMessage': 'System instructions',
|
||||||
'playground.user': 'User',
|
'playground.user': 'User',
|
||||||
'playground.assistant': 'Assistant',
|
'playground.assistant': 'Assistant',
|
||||||
'playground.newMessage': 'New Message',
|
'playground.newMessage': 'New Message',
|
||||||
@@ -29,5 +30,22 @@ export default {
|
|||||||
'A stop sequence is a predefined or user-specified text string that signals the AI to stop generating further tokens when these sequences appear.',
|
'A stop sequence is a predefined or user-specified text string that signals the AI to stop generating further tokens when these sequences appear.',
|
||||||
'playground.viewcode.tips':
|
'playground.viewcode.tips':
|
||||||
'Your API Key can be found {here}.You should use environment variables or a secret management tool to expose your key to your applications.',
|
'Your API Key can be found {here}.You should use environment variables or a secret management tool to expose your key to your applications.',
|
||||||
'playground.viewcode.here': 'here'
|
'playground.viewcode.here': 'here',
|
||||||
|
'playground.delete.img': 'Delete Image',
|
||||||
|
'playground.img.upload': 'Upload Image',
|
||||||
|
'playground.img.upload.success': 'Upload Success',
|
||||||
|
'playground.img.upload.error': 'Upload Error',
|
||||||
|
'playground.toolbar.clearmsg': 'Clear Messages',
|
||||||
|
'playground.toolbar.prompts': 'Prompts',
|
||||||
|
'playground.toolbar.compare2Model': '2-Model Comparison',
|
||||||
|
'playground.toolbar.compare3Model': '3-Model Comparison',
|
||||||
|
'playground.toolbar.compare4Model': '4-Model Comparison',
|
||||||
|
'playground.toolbar.compare6Model': '6-Model Comparison',
|
||||||
|
'playground.input.holder': 'Type <kbd>/</kbd> to input message',
|
||||||
|
'playground.input.keyword.holder': 'Type <kbd>/</kbd> to input your query',
|
||||||
|
'playground.compare.apply': 'Apply',
|
||||||
|
'playground.compare.applytoall': 'Apply to all models',
|
||||||
|
'playground.model.noavailable': 'No available models',
|
||||||
|
'playground.model.noavailable.tips':
|
||||||
|
'Please deploy a model first, and it should not be an embedding-only model.'
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -29,14 +29,15 @@ export default {
|
|||||||
'resources.table.vram': 'VRAM',
|
'resources.table.vram': 'VRAM',
|
||||||
'resources.table.index': 'Index',
|
'resources.table.index': 'Index',
|
||||||
'resources.table.workername': 'Worker Name',
|
'resources.table.workername': 'Worker Name',
|
||||||
'resources.table.vender': 'Vender',
|
'resources.table.vender': 'Vendor',
|
||||||
'resources.table.temperature': 'Temperature',
|
'resources.table.temperature': 'Temperature',
|
||||||
'resources.table.core': 'Core',
|
'resources.table.core': 'Cores',
|
||||||
'resources.table.utilization': 'Utilization',
|
'resources.table.utilization': 'Utilization',
|
||||||
'resources.table.gpuutilization': 'GPU Utilization',
|
'resources.table.gpuutilization': 'GPU Utilization',
|
||||||
'resources.table.vramutilization': 'VRAM Utilization',
|
'resources.table.vramutilization': 'VRAM Utilization',
|
||||||
'resources.table.total': 'Total',
|
'resources.table.total': 'Total',
|
||||||
'resources.table.used': 'Used',
|
'resources.table.used': 'Used',
|
||||||
|
'resources.table.allocated': 'Allocated',
|
||||||
'resources.table.wokers': 'workers',
|
'resources.table.wokers': 'workers',
|
||||||
'resources.worker.linuxormaxos': 'Linux or MacOS',
|
'resources.worker.linuxormaxos': 'Linux or MacOS',
|
||||||
'resources.table.unified': 'Unified Memory',
|
'resources.table.unified': 'Unified Memory',
|
||||||
|
|||||||
@@ -25,5 +25,7 @@ export default {
|
|||||||
'users.password.confirm': 'Confirm New Password',
|
'users.password.confirm': 'Confirm New Password',
|
||||||
'users.password.confirm.empty': 'Please confirm the new password.',
|
'users.password.confirm.empty': 'Please confirm the new password.',
|
||||||
'users.password.confirm.error': 'The two passwords entered do not match.',
|
'users.password.confirm.error': 'The two passwords entered do not match.',
|
||||||
'users.login.title': 'Log in to {name}'
|
'users.login.title': 'Log in to {name}',
|
||||||
|
'users.version.islatest': 'GPUStack {version} is the latest version',
|
||||||
|
'users.version.update': 'GPUStack {version} is available'
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
import IconFont from '@/components/icon-font';
|
import IconFont from '@/components/icon-font';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Language configuration map
|
||||||
|
* Warning: The key of the map must be the same as the directory name in the locales directory
|
||||||
|
* Do not modify the key of the map
|
||||||
|
*
|
||||||
|
*/
|
||||||
const langConfigMap = {
|
const langConfigMap = {
|
||||||
'ar-EG': {
|
'ar-EG': {
|
||||||
lang: 'ar-EG',
|
lang: 'ar-EG',
|
||||||
|
|||||||
+17
-22
@@ -1,23 +1,18 @@
|
|||||||
import apikeys from './zh-CN/apikeys';
|
/**
|
||||||
import common from './zh-CN/common';
|
* @description The directory name used in require.context must match the language configuration names defined in lang-config-map.ts.
|
||||||
import dashboard from './zh-CN/dashboard';
|
* @example Directories like 'en-US' or 'zh-CN' should correspond exactly to the configuration names in lang-config-map.ts.
|
||||||
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 shortcuts from './zh-CN/shortcuts';
|
|
||||||
import usage from './zh-CN/usage';
|
|
||||||
import users from './zh-CN/users';
|
|
||||||
|
|
||||||
export default {
|
const requireContext = require.context(`./zh-CN`, false, /\.ts$/);
|
||||||
...common,
|
|
||||||
...menu,
|
let languageConfig: Record<string, string> = {};
|
||||||
...models,
|
|
||||||
...playground,
|
requireContext.keys().forEach((fileName: any) => {
|
||||||
...resources,
|
const moduleConfig = requireContext(fileName).default;
|
||||||
...apikeys,
|
languageConfig = {
|
||||||
...users,
|
...languageConfig,
|
||||||
...dashboard,
|
...moduleConfig
|
||||||
...usage,
|
};
|
||||||
...shortcuts
|
});
|
||||||
};
|
|
||||||
|
export default languageConfig;
|
||||||
|
|||||||
@@ -198,5 +198,10 @@ export default {
|
|||||||
'common.button.version': '版本',
|
'common.button.version': '版本',
|
||||||
'common.title.delete.confirm': '确认删除',
|
'common.title.delete.confirm': '确认删除',
|
||||||
'common.button.addLabel': '添加标签',
|
'common.button.addLabel': '添加标签',
|
||||||
'common.button.addSelector': '添加选择器'
|
'common.button.addSelector': '添加选择器',
|
||||||
|
'common.button.addParams': '添加参数',
|
||||||
|
'common.text.here': '这里',
|
||||||
|
'common.text.latest': '最新',
|
||||||
|
'common.text.new': '新',
|
||||||
|
'common.text.changelog': '更新日志'
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
export default {
|
export default {
|
||||||
'menu.dashboard': '概览',
|
'menu.dashboard': '概览',
|
||||||
'menu.playground': '试验场',
|
'menu.playground': '试验场',
|
||||||
|
'menu.chat': '聊天',
|
||||||
|
'menu.compare': '多模型对比',
|
||||||
'menu.models': '模型',
|
'menu.models': '模型',
|
||||||
'menu.resources': '资源',
|
'menu.resources': '资源',
|
||||||
'menu.apikeys': 'API 密钥',
|
'menu.apikeys': 'API 密钥',
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ export default {
|
|||||||
'model.form.ollama.model': 'Ollama 模型',
|
'model.form.ollama.model': 'Ollama 模型',
|
||||||
'model.form.ollamaholder': '请选择或输入模型名称',
|
'model.form.ollamaholder': '请选择或输入模型名称',
|
||||||
'model.deploy.sort': '排序',
|
'model.deploy.sort': '排序',
|
||||||
'model.deploy.search.placeholder': '从 Hugging Face 搜索模型',
|
'model.deploy.search.placeholder': '从 {source} 搜索模型',
|
||||||
'model.form.ollamatips':
|
'model.form.ollamatips':
|
||||||
'提示:以下为 GPUStack 预设的 Ollama 模型,请选择你想要的模型或者直接在右侧表单 【{name}】 输入框中输入你要部署的模型。',
|
'提示:以下为 GPUStack 预设的 Ollama 模型,请选择你想要的模型或者直接在右侧表单 【{name}】 输入框中输入你要部署的模型。',
|
||||||
'models.sort.name': '名称',
|
'models.sort.name': '名称',
|
||||||
@@ -35,6 +35,7 @@ export default {
|
|||||||
'models.data.card': '模型简介',
|
'models.data.card': '模型简介',
|
||||||
'models.available.files': '可用文件',
|
'models.available.files': '可用文件',
|
||||||
'models.viewin.hf': '在 Hugging Face 中查看',
|
'models.viewin.hf': '在 Hugging Face 中查看',
|
||||||
|
'models.viewin.modelscope': '在 ModelScope 中查看',
|
||||||
'models.architecture': '架构',
|
'models.architecture': '架构',
|
||||||
'models.search.noresult': '未找到相关模型',
|
'models.search.noresult': '未找到相关模型',
|
||||||
'models.search.nofiles': '无可用文件',
|
'models.search.nofiles': '无可用文件',
|
||||||
@@ -53,5 +54,15 @@ export default {
|
|||||||
'models.table.backend': '后端',
|
'models.table.backend': '后端',
|
||||||
'models.table.acrossworker': '跨 worker 推理',
|
'models.table.acrossworker': '跨 worker 推理',
|
||||||
'models.table.cpuoffload': 'CPU 卸载',
|
'models.table.cpuoffload': 'CPU 卸载',
|
||||||
'models.table.layers': '层'
|
'models.table.layers': '层',
|
||||||
|
'models.form.backend': '后端',
|
||||||
|
'models.form.backend_parameters': '后端参数',
|
||||||
|
'models.search.gguf.tips': '1. GGUF 模型后端为 llama-box(llama.cpp)',
|
||||||
|
'models.search.vllm.tips': '2. 非 GGUF 模型后端为 vLLM',
|
||||||
|
'models.form.ollamalink': '在 Ollama Library 中查找',
|
||||||
|
'models.form.backend_parameters.llamabox.placeholder':
|
||||||
|
'例如,--ctx-size=8192',
|
||||||
|
'models.form.backend_parameters.vllm.placeholder':
|
||||||
|
'例如,--max-model-len=8192',
|
||||||
|
'models.form.backend_parameters.vllm.tips': '更多 {backend} 参数说明查看'
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ export default {
|
|||||||
'playground.system.tips': '在这里输入系统消息',
|
'playground.system.tips': '在这里输入系统消息',
|
||||||
'playground.title': '试验场',
|
'playground.title': '试验场',
|
||||||
'playground.system': '系统',
|
'playground.system': '系统',
|
||||||
|
'playground.systemMessage': '系统消息',
|
||||||
'playground.user': '用户',
|
'playground.user': '用户',
|
||||||
'playground.assistant': '小助手',
|
'playground.assistant': '小助手',
|
||||||
'playground.newMessage': '新消息',
|
'playground.newMessage': '新消息',
|
||||||
@@ -29,5 +30,22 @@ export default {
|
|||||||
'停止序列是一个预定义或用户指定的文本字符串,当这些序列出现时,它会提示 AI 停止生成后续的 token。',
|
'停止序列是一个预定义或用户指定的文本字符串,当这些序列出现时,它会提示 AI 停止生成后续的 token。',
|
||||||
'playground.viewcode.tips':
|
'playground.viewcode.tips':
|
||||||
'{here} 查看 API 密钥。您应该使用环境变量或密钥管理工具将您的密钥暴露给您的应用程序。',
|
'{here} 查看 API 密钥。您应该使用环境变量或密钥管理工具将您的密钥暴露给您的应用程序。',
|
||||||
'playground.viewcode.here': '这里'
|
'playground.viewcode.here': '这里',
|
||||||
|
'playground.delete.img': '删除图片',
|
||||||
|
'playground.img.upload': '上传图片',
|
||||||
|
'playground.img.upload.success': '上传成功',
|
||||||
|
'playground.img.upload.error': '上传失败',
|
||||||
|
'playground.toolbar.clearmsg': '清空消息',
|
||||||
|
'playground.toolbar.prompts': '提示词',
|
||||||
|
'playground.toolbar.compare2Model': '2 模型对比',
|
||||||
|
'playground.toolbar.compare3Model': '3 模型对比',
|
||||||
|
'playground.toolbar.compare4Model': '4 模型对比',
|
||||||
|
'playground.toolbar.compare6Model': '6 模型对比',
|
||||||
|
'playground.input.holder': '按 <kbd>/</kbd> 开始输入',
|
||||||
|
'playground.input.keyword.holder': '按 <kbd>/</kbd> 输入你的查询',
|
||||||
|
'playground.compare.apply': '应用',
|
||||||
|
'playground.compare.applytoall': '应用到所有模型',
|
||||||
|
'playground.model.noavailable': '无可用模型',
|
||||||
|
'playground.model.noavailable.tips':
|
||||||
|
'请先部署模型,且不是 Embedding Only 的模型'
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ export default {
|
|||||||
'resources.table.utilization': '利用率',
|
'resources.table.utilization': '利用率',
|
||||||
'resources.table.total': '总量',
|
'resources.table.total': '总量',
|
||||||
'resources.table.used': '已用',
|
'resources.table.used': '已用',
|
||||||
|
'resources.table.allocated': '已分配',
|
||||||
'resources.table.wokers': 'workers',
|
'resources.table.wokers': 'workers',
|
||||||
'resources.table.unified': '统一内存',
|
'resources.table.unified': '统一内存',
|
||||||
'resources.worker.linuxormaxos': 'Linux 或 MacOS',
|
'resources.worker.linuxormaxos': 'Linux 或 MacOS',
|
||||||
|
|||||||
@@ -24,5 +24,7 @@ export default {
|
|||||||
'users.password.confirm': '确认新密码',
|
'users.password.confirm': '确认新密码',
|
||||||
'users.password.confirm.empty': '请确认新密码',
|
'users.password.confirm.empty': '请确认新密码',
|
||||||
'users.password.confirm.error': '两次输入的密码不一致',
|
'users.password.confirm.error': '两次输入的密码不一致',
|
||||||
'users.login.title': '登录 {name}'
|
'users.login.title': '登录 {name}',
|
||||||
|
'users.version.islatest': 'GPUStack {version} 已是最新版本',
|
||||||
|
'users.version.update': 'GPUStack {version} 版本可供更新'
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -185,6 +185,7 @@ const AddModal: React.FC<AddModalProps> = ({
|
|||||||
<Form.Item>
|
<Form.Item>
|
||||||
<div>
|
<div>
|
||||||
<Tag
|
<Tag
|
||||||
|
bordered={false}
|
||||||
color="error"
|
color="error"
|
||||||
style={{ padding: '6px 8px', marginBottom: 16 }}
|
style={{ padding: '6px 8px', marginBottom: 16 }}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import AutoTooltip from '@/components/auto-tooltip';
|
||||||
import DeleteModal from '@/components/delete-modal';
|
import DeleteModal from '@/components/delete-modal';
|
||||||
import PageTools from '@/components/page-tools';
|
import PageTools from '@/components/page-tools';
|
||||||
import { PageAction } from '@/config';
|
import { PageAction } from '@/config';
|
||||||
@@ -218,9 +219,15 @@ const APIKeys: React.FC = () => {
|
|||||||
title={intl.formatMessage({ id: 'common.table.name' })}
|
title={intl.formatMessage({ id: 'common.table.name' })}
|
||||||
dataIndex="name"
|
dataIndex="name"
|
||||||
key="name"
|
key="name"
|
||||||
width={400}
|
|
||||||
ellipsis={{
|
ellipsis={{
|
||||||
showTitle: true
|
showTitle: false
|
||||||
|
}}
|
||||||
|
render={(text, record) => {
|
||||||
|
return (
|
||||||
|
<AutoTooltip ghost style={{ maxWidth: 400 }}>
|
||||||
|
{text}
|
||||||
|
</AutoTooltip>
|
||||||
|
);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -228,10 +235,19 @@ const APIKeys: React.FC = () => {
|
|||||||
title={intl.formatMessage({ id: 'apikeys.form.expiretime' })}
|
title={intl.formatMessage({ id: 'apikeys.form.expiretime' })}
|
||||||
dataIndex="expires_at"
|
dataIndex="expires_at"
|
||||||
key="expiration"
|
key="expiration"
|
||||||
|
ellipsis={{
|
||||||
|
showTitle: false
|
||||||
|
}}
|
||||||
render={(text, record) => {
|
render={(text, record) => {
|
||||||
return text
|
return (
|
||||||
|
<AutoTooltip ghost>
|
||||||
|
{text
|
||||||
? dayjs(text).format('YYYY-MM-DD HH:mm:ss')
|
? dayjs(text).format('YYYY-MM-DD HH:mm:ss')
|
||||||
: intl.formatMessage({ id: 'apikeys.form.expiration.never' });
|
: intl.formatMessage({
|
||||||
|
id: 'apikeys.form.expiration.never'
|
||||||
|
})}
|
||||||
|
</AutoTooltip>
|
||||||
|
);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Column
|
<Column
|
||||||
@@ -239,7 +255,10 @@ const APIKeys: React.FC = () => {
|
|||||||
dataIndex="description"
|
dataIndex="description"
|
||||||
key="description"
|
key="description"
|
||||||
ellipsis={{
|
ellipsis={{
|
||||||
showTitle: true
|
showTitle: false
|
||||||
|
}}
|
||||||
|
render={(text, record) => {
|
||||||
|
return <AutoTooltip ghost>{text}</AutoTooltip>;
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Column
|
<Column
|
||||||
@@ -250,13 +269,23 @@ const APIKeys: React.FC = () => {
|
|||||||
sortOrder={sortOrder}
|
sortOrder={sortOrder}
|
||||||
showSorterTooltip={false}
|
showSorterTooltip={false}
|
||||||
sorter={false}
|
sorter={false}
|
||||||
|
ellipsis={{
|
||||||
|
showTitle: false
|
||||||
|
}}
|
||||||
render={(text, record) => {
|
render={(text, record) => {
|
||||||
return dayjs(text).format('YYYY-MM-DD HH:mm:ss');
|
return (
|
||||||
|
<AutoTooltip ghost>
|
||||||
|
{dayjs(text).format('YYYY-MM-DD HH:mm:ss')}
|
||||||
|
</AutoTooltip>
|
||||||
|
);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Column
|
<Column
|
||||||
title={intl.formatMessage({ id: 'common.table.operation' })}
|
title={intl.formatMessage({ id: 'common.table.operation' })}
|
||||||
key="operation"
|
key="operation"
|
||||||
|
ellipsis={{
|
||||||
|
showTitle: false
|
||||||
|
}}
|
||||||
render={(text, record: ListItem) => {
|
render={(text, record: ListItem) => {
|
||||||
return (
|
return (
|
||||||
<Space size={20}>
|
<Space size={20}>
|
||||||
|
|||||||
@@ -1,72 +1,11 @@
|
|||||||
|
import AutoTooltip from '@/components/auto-tooltip';
|
||||||
import PageTools from '@/components/page-tools';
|
import PageTools from '@/components/page-tools';
|
||||||
import { convertFileSize } from '@/utils';
|
import { convertFileSize } from '@/utils';
|
||||||
import { useIntl } from '@umijs/max';
|
import { useIntl } from '@umijs/max';
|
||||||
import { Col, Row, Table } from 'antd';
|
import { Col, Row, Table } from 'antd';
|
||||||
import { useContext } from 'react';
|
import { memo, useContext } from 'react';
|
||||||
import { DashboardContext } from '../config/dashboard-context';
|
import { DashboardContext } from '../config/dashboard-context';
|
||||||
|
|
||||||
const projectColumns = [
|
|
||||||
{
|
|
||||||
title: 'Name',
|
|
||||||
dataIndex: 'name',
|
|
||||||
key: 'name'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Token Quota',
|
|
||||||
dataIndex: 'quota',
|
|
||||||
key: 'quota',
|
|
||||||
render: (text: any, record: any) => <span>{record.quota}k</span>
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Token Utilization',
|
|
||||||
dataIndex: 'utilization',
|
|
||||||
key: 'utilization',
|
|
||||||
render: (text: any, record: any) => <span>{record.utilization}%</span>
|
|
||||||
},
|
|
||||||
{
|
|
||||||
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 ActiveTable = () => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const data = useContext(DashboardContext).active_models || [];
|
const data = useContext(DashboardContext).active_models || [];
|
||||||
@@ -74,26 +13,27 @@ const ActiveTable = () => {
|
|||||||
{
|
{
|
||||||
title: intl.formatMessage({ id: 'common.table.name' }),
|
title: intl.formatMessage({ id: 'common.table.name' }),
|
||||||
dataIndex: 'name',
|
dataIndex: 'name',
|
||||||
key: 'name'
|
key: 'name',
|
||||||
|
ellipsis: true,
|
||||||
|
render: (text: any, record: any) => {
|
||||||
|
return (
|
||||||
|
<AutoTooltip ghost>
|
||||||
|
<span>{text}</span>
|
||||||
|
</AutoTooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
// {
|
|
||||||
// title: intl.formatMessage({ id: 'dashboard.gpuutilization' }),
|
|
||||||
// dataIndex: 'gpu_utilization',
|
|
||||||
// key: 'gpu_utilization',
|
|
||||||
// render: (text: any, record: any) => (
|
|
||||||
// <ProgressBar percent={_.round(text, 0)}></ProgressBar>
|
|
||||||
// )
|
|
||||||
// },
|
|
||||||
{
|
{
|
||||||
title: intl.formatMessage({ id: 'dashboard.allocatevram' }),
|
title: intl.formatMessage({ id: 'dashboard.allocatevram' }),
|
||||||
dataIndex: 'resource_claim.memory',
|
dataIndex: 'resource_claim.memory',
|
||||||
key: 'vram',
|
key: 'vram',
|
||||||
|
ellipsis: true,
|
||||||
render: (text: any, record: any) => {
|
render: (text: any, record: any) => {
|
||||||
return (
|
return (
|
||||||
<span>
|
<AutoTooltip ghost>
|
||||||
{convertFileSize(record.resource_claim?.vram || 0)} /{' '}
|
{convertFileSize(record.resource_claim?.vram || 0)} /{' '}
|
||||||
{convertFileSize(record.resource_claim?.ram || 0)}
|
{convertFileSize(record.resource_claim?.ram || 0)}
|
||||||
</span>
|
</AutoTooltip>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -105,7 +45,15 @@ const ActiveTable = () => {
|
|||||||
{
|
{
|
||||||
title: intl.formatMessage({ id: 'dashboard.tokens' }),
|
title: intl.formatMessage({ id: 'dashboard.tokens' }),
|
||||||
dataIndex: 'token_count',
|
dataIndex: 'token_count',
|
||||||
key: 'token_count'
|
key: 'token_count',
|
||||||
|
ellipsis: true,
|
||||||
|
render: (text: any, record: any) => {
|
||||||
|
return (
|
||||||
|
<AutoTooltip ghost>
|
||||||
|
<span>{text}</span>
|
||||||
|
</AutoTooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
return (
|
return (
|
||||||
@@ -133,4 +81,4 @@ const ActiveTable = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ActiveTable;
|
export default memo(ActiveTable);
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ const renderCardItem = (data: {
|
|||||||
const Overview: React.FC = () => {
|
const Overview: React.FC = () => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const data = useContext(DashboardContext).resource_counts || {};
|
const data = useContext(DashboardContext).resource_counts || {};
|
||||||
console.log('overview===');
|
|
||||||
const renderValue = (
|
const renderValue = (
|
||||||
value:
|
value:
|
||||||
| number
|
| number
|
||||||
@@ -74,4 +74,4 @@ const Overview: React.FC = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Overview;
|
export default React.memo(Overview);
|
||||||
|
|||||||
@@ -5,11 +5,6 @@ import _ from 'lodash';
|
|||||||
import { memo, useContext, useMemo } from 'react';
|
import { memo, useContext, useMemo } from 'react';
|
||||||
import { DashboardContext } from '../config/dashboard-context';
|
import { DashboardContext } from '../config/dashboard-context';
|
||||||
|
|
||||||
const chartColorMap = {
|
|
||||||
tickLineColor: 'rgba(217,217,217,0.5)',
|
|
||||||
axislabelColor: 'rgba(0, 0, 0, 0.4)'
|
|
||||||
};
|
|
||||||
|
|
||||||
const TypeKeyMap = {
|
const TypeKeyMap = {
|
||||||
cpu: {
|
cpu: {
|
||||||
label: 'CPU',
|
label: 'CPU',
|
||||||
@@ -37,80 +32,7 @@ const TypeKeyMap = {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
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 = `<span class="tooltip-x-name">${params[0].axisValue}</span>`;
|
|
||||||
params.forEach((item: any) => {
|
|
||||||
result += `<span class="tooltip-item">
|
|
||||||
<span class="tooltip-item-name">
|
|
||||||
<span style="display:inline-block;margin-right:5px;border-radius:8px;width:8px;height:8px;background-color:${item.color};"></span>
|
|
||||||
<span class="tooltip-title">${item.seriesName}</span>:
|
|
||||||
</span>
|
|
||||||
<span class="tooltip-value">${item.data.value}</span>
|
|
||||||
</span>`;
|
|
||||||
});
|
|
||||||
return `<div class="tooltip-wrapper">${result}</div>`;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
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 = () => {
|
const UtilizationOvertime: React.FC = () => {
|
||||||
console.log('systemload=====================');
|
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const data = useContext(DashboardContext)?.system_load?.history || {};
|
const data = useContext(DashboardContext)?.system_load?.history || {};
|
||||||
|
|
||||||
@@ -147,7 +69,7 @@ const UtilizationOvertime: React.FC = () => {
|
|||||||
legendData,
|
legendData,
|
||||||
xAxisData: _.uniq(xAxisData)
|
xAxisData: _.uniq(xAxisData)
|
||||||
};
|
};
|
||||||
}, [data]);
|
}, [data, intl]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -5,9 +5,8 @@ import breakpoints from '@/config/breakpoints';
|
|||||||
import useWindowResize from '@/hooks/use-window-resize';
|
import useWindowResize from '@/hooks/use-window-resize';
|
||||||
import { useIntl } from '@umijs/max';
|
import { useIntl } from '@umijs/max';
|
||||||
import { Col, Row } from 'antd';
|
import { Col, Row } from 'antd';
|
||||||
import dayjs from 'dayjs';
|
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { useContext, useEffect, useState } from 'react';
|
import { memo, useContext, useEffect, useMemo, useState } from 'react';
|
||||||
import { DashboardContext } from '../config/dashboard-context';
|
import { DashboardContext } from '../config/dashboard-context';
|
||||||
import ResourceUtilization from './resource-utilization';
|
import ResourceUtilization from './resource-utilization';
|
||||||
|
|
||||||
@@ -28,11 +27,29 @@ const SystemLoad = () => {
|
|||||||
const [paddingRight, setPaddingRight] = useState<string>('20px');
|
const [paddingRight, setPaddingRight] = useState<string>('20px');
|
||||||
const [smallChartHeight, setSmallChartHeight] = useState<number>(190);
|
const [smallChartHeight, setSmallChartHeight] = useState<number>(190);
|
||||||
const [largeChartHeight, setLargeChartHeight] = useState<number>(400);
|
const [largeChartHeight, setLargeChartHeight] = useState<number>(400);
|
||||||
const thresholds = [0.5, 0.7, 1];
|
|
||||||
const height = 400;
|
|
||||||
const currentDate = dayjs().format('YYYY-MM-DD');
|
|
||||||
|
|
||||||
const handleSelectDate = (date: any) => {};
|
const height = 400;
|
||||||
|
|
||||||
|
const chartData = useMemo(() => {
|
||||||
|
return {
|
||||||
|
gpu: {
|
||||||
|
data: _.round(data.gpu || 0, 1),
|
||||||
|
color: strokeColorFunc(data.gpu)
|
||||||
|
},
|
||||||
|
vram: {
|
||||||
|
data: _.round(data.vram || 0, 1),
|
||||||
|
color: strokeColorFunc(data.vram)
|
||||||
|
},
|
||||||
|
cpu: {
|
||||||
|
data: _.round(data.cpu || 0, 1),
|
||||||
|
color: strokeColorFunc(data.cpu)
|
||||||
|
},
|
||||||
|
ram: {
|
||||||
|
data: _.round(data.ram || 0, 1),
|
||||||
|
color: strokeColorFunc(data.ram)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (size.width < breakpoints.xl) {
|
if (size.width < breakpoints.xl) {
|
||||||
@@ -70,8 +87,8 @@ const SystemLoad = () => {
|
|||||||
<Col span={12} style={{ height: smallChartHeight }}>
|
<Col span={12} style={{ height: smallChartHeight }}>
|
||||||
<GaugeChart
|
<GaugeChart
|
||||||
height={smallChartHeight}
|
height={smallChartHeight}
|
||||||
value={_.round(data.gpu || 0, 1)}
|
value={chartData.gpu.data}
|
||||||
color={strokeColorFunc(data.gpu)}
|
color={chartData.gpu.color}
|
||||||
title={intl.formatMessage({
|
title={intl.formatMessage({
|
||||||
id: 'dashboard.gpuutilization'
|
id: 'dashboard.gpuutilization'
|
||||||
})}
|
})}
|
||||||
@@ -83,8 +100,8 @@ const SystemLoad = () => {
|
|||||||
id: 'dashboard.vramutilization'
|
id: 'dashboard.vramutilization'
|
||||||
})}
|
})}
|
||||||
height={smallChartHeight}
|
height={smallChartHeight}
|
||||||
color={strokeColorFunc(data.vram)}
|
color={chartData.vram.color}
|
||||||
value={_.round(data.vram || 0, 1)}
|
value={chartData.vram.data}
|
||||||
></GaugeChart>
|
></GaugeChart>
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={12} style={{ height: smallChartHeight }}>
|
<Col span={12} style={{ height: smallChartHeight }}>
|
||||||
@@ -93,8 +110,8 @@ const SystemLoad = () => {
|
|||||||
id: 'dashboard.cpuutilization'
|
id: 'dashboard.cpuutilization'
|
||||||
})}
|
})}
|
||||||
height={smallChartHeight}
|
height={smallChartHeight}
|
||||||
color={strokeColorFunc(data.cpu)}
|
color={chartData.cpu.color}
|
||||||
value={_.round(data.cpu || 0, 1)}
|
value={chartData.cpu.data}
|
||||||
></GaugeChart>
|
></GaugeChart>
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={12} style={{ height: smallChartHeight }}>
|
<Col span={12} style={{ height: smallChartHeight }}>
|
||||||
@@ -103,8 +120,8 @@ const SystemLoad = () => {
|
|||||||
id: 'dashboard.memoryutilization'
|
id: 'dashboard.memoryutilization'
|
||||||
})}
|
})}
|
||||||
height={smallChartHeight}
|
height={smallChartHeight}
|
||||||
color={strokeColorFunc(data.ram)}
|
color={chartData.ram.color}
|
||||||
value={_.round(data.ram || 0, 1)}
|
value={chartData.ram.data}
|
||||||
></GaugeChart>
|
></GaugeChart>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
@@ -116,4 +133,4 @@ const SystemLoad = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default SystemLoad;
|
export default memo(SystemLoad);
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { useIntl } from '@umijs/max';
|
|||||||
import { Col, Row } from 'antd';
|
import { Col, Row } from 'antd';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { memo, useCallback, useContext } from 'react';
|
import { memo, useCallback, useContext, useEffect, useState } from 'react';
|
||||||
import { DashboardContext } from '../../config/dashboard-context';
|
import { DashboardContext } from '../../config/dashboard-context';
|
||||||
import RequestTokenInner from './request-token-inner';
|
import RequestTokenInner from './request-token-inner';
|
||||||
import TopUser from './top-user';
|
import TopUser from './top-user';
|
||||||
@@ -33,18 +33,29 @@ const getCurrentMonthDays = () => {
|
|||||||
|
|
||||||
const UsageInner: React.FC<{ paddingRight: string }> = ({ paddingRight }) => {
|
const UsageInner: React.FC<{ paddingRight: string }> = ({ paddingRight }) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const currentDate = dayjs();
|
|
||||||
const currentMonthDays = getCurrentMonthDays();
|
const [topUserData, setTopUserData] = useState<{
|
||||||
let requestData: {
|
userData: { name: string; value: number }[];
|
||||||
|
topUserList: string[];
|
||||||
|
}>({
|
||||||
|
userData: [],
|
||||||
|
topUserList: []
|
||||||
|
});
|
||||||
|
|
||||||
|
const [requestTokenData, setRequestTokenData] = useState<{
|
||||||
|
requestData: {
|
||||||
name: string;
|
name: string;
|
||||||
color: string;
|
color: string;
|
||||||
areaStyle: any;
|
areaStyle: any;
|
||||||
data: { time: string; value: number }[];
|
data: { time: string; value: number }[];
|
||||||
}[] = [];
|
}[];
|
||||||
let tokenData: { time: string; value: number }[] = [];
|
tokenData: { time: string; value: number }[];
|
||||||
let userData: { name: string; value: number }[] = [];
|
xAxisData: string[];
|
||||||
const xAxisData: string[] = currentMonthDays;
|
}>({
|
||||||
let topUserList: string[] = [];
|
requestData: [],
|
||||||
|
tokenData: [],
|
||||||
|
xAxisData: []
|
||||||
|
});
|
||||||
|
|
||||||
const { model_usage } = useContext(DashboardContext);
|
const { model_usage } = useContext(DashboardContext);
|
||||||
const data = model_usage || {};
|
const data = model_usage || {};
|
||||||
@@ -54,6 +65,7 @@ const UsageInner: React.FC<{ paddingRight: string }> = ({ paddingRight }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const generateData = useCallback(() => {
|
const generateData = useCallback(() => {
|
||||||
|
const currentMonthDays = getCurrentMonthDays();
|
||||||
const requestList: {
|
const requestList: {
|
||||||
name: string;
|
name: string;
|
||||||
color: string;
|
color: string;
|
||||||
@@ -88,7 +100,7 @@ const UsageInner: React.FC<{ paddingRight: string }> = ({ paddingRight }) => {
|
|||||||
data: []
|
data: []
|
||||||
};
|
};
|
||||||
|
|
||||||
_.each(xAxisData, (date: string) => {
|
_.each(currentMonthDays, (date: string) => {
|
||||||
// tokens data
|
// tokens data
|
||||||
const item = _.find(data.completion_token_history, (item: any) => {
|
const item = _.find(data.completion_token_history, (item: any) => {
|
||||||
return dayjs(item.timestamp * 1000).format('YYYY-MM-DD') === date;
|
return dayjs(item.timestamp * 1000).format('YYYY-MM-DD') === date;
|
||||||
@@ -140,8 +152,10 @@ const UsageInner: React.FC<{ paddingRight: string }> = ({ paddingRight }) => {
|
|||||||
|
|
||||||
// ========== top users ============
|
// ========== top users ============
|
||||||
if (!data.top_users?.length) {
|
if (!data.top_users?.length) {
|
||||||
userData = [];
|
setTopUserData({
|
||||||
topUserList = [];
|
userData: [],
|
||||||
|
topUserList: []
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
const users: string[] = [];
|
const users: string[] = [];
|
||||||
_.each(data.top_users, (item: any) => {
|
_.each(data.top_users, (item: any) => {
|
||||||
@@ -155,15 +169,23 @@ const UsageInner: React.FC<{ paddingRight: string }> = ({ paddingRight }) => {
|
|||||||
value: item.completion_token_count
|
value: item.completion_token_count
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
topUserList = _.uniq(users);
|
|
||||||
userData = [topUserCompletion, topUserPrompt];
|
setTopUserData({
|
||||||
|
userData: [topUserCompletion, topUserPrompt],
|
||||||
|
topUserList: _.uniq(users)
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
requestData = [requestList];
|
setRequestTokenData({
|
||||||
tokenData = [completionData, prompData];
|
requestData: [requestList],
|
||||||
}, [data, xAxisData]);
|
tokenData: [completionData, prompData],
|
||||||
|
xAxisData: currentMonthDays
|
||||||
|
});
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
generateData();
|
generateData();
|
||||||
|
}, [generateData]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -181,13 +203,16 @@ const UsageInner: React.FC<{ paddingRight: string }> = ({ paddingRight }) => {
|
|||||||
style={{ paddingRight: paddingRight }}
|
style={{ paddingRight: paddingRight }}
|
||||||
>
|
>
|
||||||
<RequestTokenInner
|
<RequestTokenInner
|
||||||
requestData={requestData}
|
requestData={requestTokenData.requestData}
|
||||||
xAxisData={xAxisData}
|
xAxisData={requestTokenData.xAxisData}
|
||||||
tokenData={tokenData}
|
tokenData={requestTokenData.tokenData}
|
||||||
></RequestTokenInner>
|
></RequestTokenInner>
|
||||||
</Col>
|
</Col>
|
||||||
<Col xs={24} sm={24} md={24} lg={24} xl={8}>
|
<Col xs={24} sm={24} md={24} lg={24} xl={8}>
|
||||||
<TopUser userData={userData} topUserList={topUserList}></TopUser>
|
<TopUser
|
||||||
|
userData={topUserData.userData}
|
||||||
|
topUserList={topUserData.topUserList}
|
||||||
|
></TopUser>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ interface RequestTokenInnerProps {
|
|||||||
xAxisData: string[];
|
xAxisData: string[];
|
||||||
}
|
}
|
||||||
const RequestTokenInner: React.FC<RequestTokenInnerProps> = (props) => {
|
const RequestTokenInner: React.FC<RequestTokenInnerProps> = (props) => {
|
||||||
console.log('request token inner=====================');
|
|
||||||
const { requestData, tokenData, xAxisData } = props;
|
const { requestData, tokenData, xAxisData } = props;
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const labelFormatter = (v: any) => {
|
const labelFormatter = (v: any) => {
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ interface TopUserProps {
|
|||||||
topUserList: string[];
|
topUserList: string[];
|
||||||
}
|
}
|
||||||
const TopUser: React.FC<TopUserProps> = (props) => {
|
const TopUser: React.FC<TopUserProps> = (props) => {
|
||||||
console.log('TopUser=====================');
|
|
||||||
const { userData, topUserList } = props;
|
const { userData, topUserList } = props;
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user