Compare commits

...
100 Commits
Author SHA1 Message Date
jialin 725409a576 fix: image seed do no clear 2025-12-25 17:57:11 +08:00
jialin 9b25cc51e4 style: user rank chart 2025-12-25 17:57:11 +08:00
jialin 9dcdb6b6e7 fix: reset userSettings first login 2025-12-25 13:14:59 +08:00
jialin 73b987358f fix(style): cluster options 2025-12-24 20:52:48 +08:00
jialin 84c2bf130c style: cluster selector 2025-12-24 20:37:20 +08:00
jialin b25189feea fix: cancel sorting 2025-12-24 15:40:19 +08:00
jialin a9b58a5564 fix: cluster filter do not work in dashboard 2025-12-24 12:20:56 +08:00
jialin 530b20c633 chore: add resource modal button text 2025-12-24 10:53:53 +08:00
jialin fa0aeac10b fix: remove default sorting status on UI 2025-12-23 20:54:16 +08:00
jialin 1b661bb8cd feat: set default cluster 2025-12-23 20:54:16 +08:00
jialin 530e8f7537 fix: replicas, ready_replicas sorting 2025-12-23 18:35:33 +08:00
jialin 9e019a98f6 fix: click create hide modal 2025-12-23 10:51:58 +08:00
jialin 10c6cf9f7e fix: init image seed 2025-12-23 10:51:58 +08:00
jialin 480b238e44 fix: expand all model clear instances cache 2025-12-23 10:51:58 +08:00
jialin 32bdbc693a fix: onTableChange only handles sorting 2025-12-22 19:41:52 +08:00
jialin ab146b6b6b fix: allow to enter seed 2025-12-22 18:11:46 +08:00
jialin 763929eafc fix: instance backend data ui compatability with old data 2025-12-22 18:11:46 +08:00
jialin 711b3fee98 fix: user guidance when no resource 2025-12-22 18:11:46 +08:00
jialin 0d0592eee9 fix: sorter tooltip 2025-12-22 16:41:44 +08:00
jialin cc7948ba45 fix: update Server as GPUStack Server 2025-12-22 16:19:47 +08:00
jialin 7e25b64689 fix: remove instance from cache after deleting 2025-12-22 16:19:47 +08:00
Yuxing Dengandjialin d844cdadb5 fix: update advertise address title and description 2025-12-22 15:14:29 +08:00
jialin e3b3f51e87 fix: ignore the sort_by when no data 2025-12-22 11:57:57 +08:00
jialin 45efda7f1d fix: single sorted field, remove token 2025-12-22 10:10:35 +08:00
jialin db45c861e4 feat: models filter by state 2025-12-21 21:37:05 +08:00
jialin 42f8e6a6d9 chore: set col order 2025-12-21 21:37:05 +08:00
jialin 23a761226a feat: table list sorter 2025-12-21 21:37:05 +08:00
jialin 6738dfe0d3 fix: anyway submit disabled when no catalog specs 2025-12-19 20:33:25 +08:00
jialin 6e16b2eb0c fix: submit pool data from a validate failed form 2025-12-19 20:33:25 +08:00
jialin 707eb15465 fix: update command of adding worker 2025-12-19 20:33:25 +08:00
jialin 8008099e71 fix: image default seed 2025-12-19 20:33:25 +08:00
jialin 79ccb96c95 fix: edit worker_config 2025-12-19 16:44:52 +08:00
jialin bd76934e40 fix: delete worker pool 2025-12-19 15:19:27 +08:00
jialin bb793a41fc fix: entrypoint do not display in edit 2025-12-19 15:00:10 +08:00
jialin ad048a92ed feat: backend entrypoint 2025-12-19 14:40:20 +08:00
jialin 14d944ad2f style: cluster advanced scroller 2025-12-18 20:03:00 +08:00
jialin e1b8b5696c fix: update instance backend field 2025-12-18 17:29:07 +08:00
jialin 78368e9979 fix: cluster worker config issues 2025-12-18 17:29:07 +08:00
jialin e83bff6923 chore: worker ip 2025-12-18 14:06:32 +08:00
jialin e6c12d81ea fix: edit worker pool miss data 2025-12-18 14:06:32 +08:00
jialin 3a73d0c1b2 style: worker added info status 2025-12-18 12:19:31 +08:00
jialin b524587a9a fix: add worker button do not work 2025-12-18 10:56:45 +08:00
jialin e4bd753355 style: no resource modal 2025-12-17 19:58:28 +08:00
jialin 30536cb04a fix: remove advanced agrs in image 2025-12-17 16:15:34 +08:00
jialin da1efa9270 fix: password length 64 2025-12-17 16:15:34 +08:00
jialin bd6f4b3f76 fix: add worker, create cluster 2025-12-17 14:59:07 +08:00
jialin 5e48d75ead fix: add worker message 2025-12-17 14:59:07 +08:00
jialin bd46fcdc75 fix: container name in add worker 2025-12-17 14:59:07 +08:00
jialin 6968f9fa6b feat: worker_config 2025-12-16 18:43:28 +08:00
jialin 5225fd328d fix: backend framework options 2025-12-16 11:11:24 +08:00
jialin fa42474612 chore: worker added message 2025-12-15 15:13:57 +08:00
jialin d2eda78af0 fix: add worker command container_name 2025-12-15 10:18:16 +08:00
jialin 7a0ee6aac4 style: column wrapper srollable style 2025-12-13 19:47:48 +08:00
jialin b327eedb45 fix: logout error 2025-12-12 17:26:06 +08:00
jialin a68e3ff9da fix: no spec list for catalog 2025-12-12 17:22:28 +08:00
jialin 0500dd2e35 fix: query catalog spec, default_version check 2025-12-12 17:08:30 +08:00
jialin cb43131ccb fix: add worker command 2025-12-12 11:14:57 +08:00
jialin 883e0ab115 feat: get initial password 2025-12-12 11:14:57 +08:00
jialin b189160888 fix: docs for gpu link 404 2025-12-11 18:14:25 +08:00
jialin 3dbb0de683 feat: add resource for first login 2025-12-11 17:50:39 +08:00
jialin b38bc8a207 fix: table sorter 2025-12-11 13:33:41 +08:00
jialin 3328828da6 refactor: card list resize container 2025-12-11 13:33:41 +08:00
jialin 7fdabd90a5 fix: update add worker command 2025-12-10 15:50:53 +08:00
jialin d8452e98f5 style: drawer styles 2025-12-10 15:50:53 +08:00
jialin dda2213593 style: workers page no page container 2025-12-10 15:50:53 +08:00
jialin 04f995b80c style: resize terminal height 2025-12-10 15:50:53 +08:00
jialin a08f1e2512 chore: rename ground-left as ground-llm 2025-12-10 15:50:53 +08:00
jialin af4849d6cc style: terminal panel 2025-12-10 15:50:53 +08:00
jialin 087675889f feat: model cache volume mount, clear gpu selector 2025-12-09 19:07:40 +08:00
jialin daa10617c0 chore: prefetch chunk file 2025-12-05 13:31:56 +08:00
jialin c3b5c13cc5 fix: show info when adding worker error 2025-12-05 11:45:44 +08:00
jialin 1dd83645bd fix: reset first login flag after changing password 2025-12-05 11:26:32 +08:00
jialin 2df7c17bc1 chore: remove experimental from hygon 2025-12-04 22:35:15 +08:00
jialin a274349ace chore: upgrade info 2025-12-04 16:43:33 +08:00
jialin 1c699fe307 chore: tidy code 2025-12-04 16:43:33 +08:00
jialin f0a3e84e4f fix: set default os image 2025-12-04 10:48:34 +08:00
jialin f87989d32e fix: clear os_image when it's not in new image list 2025-12-04 10:12:19 +08:00
jialin 7355be9e3a fix: init cluster id when deploy model 2025-12-03 19:18:07 +08:00
jialin d8f1e95e1a fix: use image label in inputbox 2025-12-03 16:00:59 +08:00
jialin 59eace702b fix: add worker init steps failed 2025-12-03 12:32:04 +08:00
jialin 8a24ba0252 fix: cluster option disabled 2025-12-03 11:50:35 +08:00
jialin 03999a9d2c chore: os image filter 2025-12-03 11:38:16 +08:00
jialin f335cb1775 fix: click icon do not worker on upload 2025-12-02 17:24:14 +08:00
jialin b9c034469b fix: input img url fall back image 2025-12-02 15:58:17 +08:00
jialin ce3a82ddda fix: edit message add image 2025-12-02 14:00:58 +08:00
jialin 5cbb38922c feat: add image from url 2025-12-01 19:01:33 +08:00
jialin 14f0afec08 chore: allow space in backend parameters 2025-12-01 10:06:00 +08:00
jialin 769cc09906 fix: logs are not fully loaded 2025-11-30 20:33:16 +08:00
jialin 306daf7591 chore: amd note for add worker 2025-11-27 19:35:04 +08:00
jialin d1c1132e6e style: button for empty 2025-11-27 15:53:23 +08:00
jialin 962df859f1 chore: get started for add cluster 2025-11-27 11:35:45 +08:00
jialin 97601280d1 fix: model download progress text overflow 2025-11-26 10:28:40 +08:00
jialin ede42e99de fix: do not reset the backend after selecting a gguf file 2025-11-25 17:20:09 +08:00
jialin 47cae0134c feat: display deprecated backend versions group 2025-11-25 16:04:27 +08:00
jialin 33defab7df fix: gpus link 2025-11-25 11:34:11 +08:00
EVGENY Mandjialin 6b402e944b Update Russian translations in models.ts 2025-11-24 09:38:32 +08:00
EVGENY Mandjialin f8a6f4c5e5 Translate cluster messages to Russian 2025-11-24 09:38:14 +08:00
EVGENY Mandjialin adb999d3a9 Update dashboard.ts 2025-11-24 09:37:42 +08:00
jialin f4ea23dd14 fix: set default vllm backend for gguf 2025-11-23 21:08:28 +08:00
jialin 5bd0020e49 fix: check vllm backend for gguf model 2025-11-23 19:57:41 +08:00
252 changed files with 5613 additions and 2247 deletions
+3 -1
View File
@@ -4,7 +4,6 @@ import { compressionPluginConfig, monacoPluginConfig } from './plugins';
import proxy from './proxy';
import routes from './routes';
import { getBranchInfo } from './utils';
const CompressionWebpackPlugin = require('compression-webpack-plugin');
const versionInfo = getBranchInfo();
process.env.VERSION = JSON.stringify(versionInfo);
@@ -79,6 +78,9 @@ export default defineConfig({
model: {},
initialState: {},
request: {},
routePrefetch: {
defaultPrefetch: 'intent'
},
keepalive: keepAlive,
locale: {
antd: true,
+7 -2
View File
@@ -79,10 +79,15 @@ export async function getInitialState(): Promise<{
const getAppVersionInfo = async () => {
try {
const data = await queryVersionInfo();
const isProduction = data.version?.indexOf('0.0.0') === -1;
const isDev = data.version?.indexOf('0.0.0') > -1;
const isRc = data.version?.indexOf('rc') > -1;
setAtomStorage(GPUStackVersionAtom, {
...data,
isProduction
isProd: !isDev && !isRc,
isDev,
isRc
});
} catch (error) {
console.error('queryVersionInfo error', error);
+1 -2
View File
@@ -3,8 +3,7 @@
.ant-layout-sider-children {
border-inline: none;
border-radius: 0;
// padding-inline: 16px;
padding-block-end: 12px;
padding-block-end: 8px;
}
.umi-plugin-layout-right {
+2
View File
@@ -29,6 +29,7 @@ export const regionOSImageListAtom = atom<
{
label: string;
value: string;
os_image: string;
name: string;
description: string;
vendor: string;
@@ -60,4 +61,5 @@ export const fromClusterCreationAtom = atom(false);
*/
export const clusterSessionAtom = atom<{
firstAddWorker: boolean;
firstAddCluster: boolean;
} | null>(null);
+9
View File
@@ -27,4 +27,13 @@ export const clusterListAtom = atom<
}[]
>([]);
export const workerListAtom = atom<
{
label: string;
value: number;
}[]
>([]);
export const backendOptionsAtom = atom<BackendOption[]>([]);
export const resourceOverviewAtom = atom<Record<string, any>>({});
+4 -2
View File
@@ -8,14 +8,16 @@ type UserSettings = {
colorPrimary: string;
isDarkTheme: boolean;
collapsed: boolean;
hideAddResourceModal?: boolean;
};
const defaultSettings: UserSettings = {
export const defaultSettings: UserSettings = {
theme: 'light',
mode: 'auto',
isDarkTheme: false,
colorPrimary: COLOR_PRIMARY,
collapsed: false
collapsed: false,
hideAddResourceModal: false
};
export const getStorageUserSettings = () => {
+6 -2
View File
@@ -6,11 +6,15 @@ export const userAtom = atomWithStorage<any>('userInfo', null);
export const GPUStackVersionAtom = atom<{
version: string;
git_commit: string;
isProduction: boolean;
isProd: boolean;
isDev?: boolean;
isRc?: boolean;
}>({
version: '',
git_commit: '',
isProduction: false
isProd: false,
isDev: false,
isRc: false
});
export const UpdateCheckAtom = atom<{
+30 -10
View File
@@ -1,16 +1,36 @@
import { defaultSettings } from '@/atoms/settings';
import { getDefaultStore } from 'jotai';
export const clearStorageUserSettings = () => {
const savedSettings = JSON.parse(
localStorage.getItem('userSettings') || '{}'
);
localStorage.setItem(
'userSettings',
JSON.stringify({
...savedSettings,
colorPrimary: undefined
})
);
try {
const savedSettings = JSON.parse(
localStorage.getItem('userSettings') || '{}'
);
localStorage.setItem(
'userSettings',
JSON.stringify({
...savedSettings,
hideAddResourceModal: false,
colorPrimary: undefined
})
);
} catch (error) {
console.log('Error clearing user settings:', error);
}
};
export const resetStorageUserSettings = () => {
try {
localStorage.setItem(
'userSettings',
JSON.stringify({
...defaultSettings,
colorPrimary: undefined
})
);
} catch (error) {
console.log('Error clearing user settings:', error);
}
};
export const clearAtomStorage = (atom: any) => {
+18 -3
View File
@@ -1,4 +1,8 @@
import { WarningFilled } from '@ant-design/icons';
import {
CheckCircleFilled,
LoadingOutlined,
WarningFilled
} from '@ant-design/icons';
import { Typography } from 'antd';
import { createStyles } from 'antd-style';
import classNames from 'classnames';
@@ -122,6 +126,17 @@ const AlertInfo: React.FC<AlertInfoProps> = (props) => {
overlayScrollerProps = {}
} = props;
const { styles } = useStyles();
const renderIcon = () => {
if (type === 'transition') {
return <LoadingOutlined />;
}
if (type === 'success') {
return <CheckCircleFilled />;
}
return <WarningFilled />;
};
return (
<>
{message ? (
@@ -139,7 +154,7 @@ const AlertInfo: React.FC<AlertInfoProps> = (props) => {
>
<div className={classNames('title', type)}>
<span className={classNames('info-icon', type)}>
{icon ?? <WarningFilled />}
{icon ?? renderIcon()}
</span>
</div>
{title && (
@@ -164,4 +179,4 @@ const AlertInfo: React.FC<AlertInfoProps> = (props) => {
);
};
export default React.memo(AlertInfo);
export default AlertInfo;
+3 -2
View File
@@ -70,8 +70,9 @@ const AutoImage: React.FC<
setIsError(false);
}, [props.onLoad]);
const handleOnError = useCallback(() => {
const handleOnError = useCallback((e: any) => {
setIsError(true);
e.target.src = fallbackImg;
}, []);
useEffect(() => {
@@ -127,4 +128,4 @@ const AutoImage: React.FC<
);
};
export default React.memo(AutoImage);
export default AutoImage;
+6 -2
View File
@@ -9,10 +9,11 @@ import echarts, { ECOption } from '.';
const Chart: React.FC<{
options: ECOption;
chartHeight?: number;
height: number | string;
width: number | string;
ref?: any;
}> = forwardRef(({ options, width, height }, ref) => {
}> = forwardRef(({ options, width, height, chartHeight }, ref) => {
const container = useRef<HTMLDivElement>(null);
const chart = useRef<echarts.EChartsType>();
const resizeable = useRef(false);
@@ -138,7 +139,10 @@ const Chart: React.FC<{
return (
<div className="chart-wrapper" style={{ width: width, height }}>
<div ref={container} style={{ width: width, height }}></div>
<div
ref={container}
style={{ width: width, height: chartHeight || height }}
></div>
</div>
);
});
+1
View File
@@ -220,6 +220,7 @@ export default function useChartConfig() {
};
return {
token,
tooltip,
grid,
legend,
+36 -6
View File
@@ -2,7 +2,7 @@ import Chart from '@/components/echarts/chart';
import useChartConfig from '@/components/echarts/config';
import EmptyData from '@/components/empty-data';
import _ from 'lodash';
import React, { memo, useMemo } from 'react';
import React, { useMemo } from 'react';
import { ChartProps } from './types';
const BarChart: React.FC<ChartProps & { maxItems?: number }> = (props) => {
@@ -17,6 +17,7 @@ const BarChart: React.FC<ChartProps & { maxItems?: number }> = (props) => {
title
} = props;
const {
token,
grid,
legend,
title: titleConfig,
@@ -44,17 +45,27 @@ const BarChart: React.FC<ChartProps & { maxItems?: number }> = (props) => {
xAxis: {
...xAxis,
axisLabel: {
...xAxis.axisLabel,
formatter: labelFormatter
...xAxis.axisLabel
}
},
yAxis: {
...yAxis,
axisLabel: {
...yAxis.axisLabel,
show: true,
overflow: 'truncate',
width: 75,
ellipsis: '...'
// formatter(value: string, index: number) {
// return `{a|${index + 1}}`;
// },
// rich: {
// a: {
// fontWeight: 500,
// fontSize: 14,
// color: token?.colorTextSecondary
// }
// }
}
},
legend: {
@@ -72,7 +83,23 @@ const BarChart: React.FC<ChartProps & { maxItems?: number }> = (props) => {
stack: 'Ad',
barGap: '30%',
label: {
show: false
show: false,
formatter(params: any) {
if (params.seriesIndex === 0) {
return `{value|${params.name}}`;
}
return '';
},
position: 'left',
align: 'left',
offset: [5, 18],
rich: {
value: {
textBorderWidth: 0,
fontSize: 11,
color: token?.colorTextTertiary
}
}
},
itemStyle: {
color: item.color
@@ -138,7 +165,10 @@ const BarChart: React.FC<ChartProps & { maxItems?: number }> = (props) => {
return (
<>
{isEmpty ? (
<EmptyData height={height} title={title}></EmptyData>
<EmptyData
height={height}
title={_.get(title, 'text', title || '')}
></EmptyData>
) : (
<Chart
height={height}
@@ -150,4 +180,4 @@ const BarChart: React.FC<ChartProps & { maxItems?: number }> = (props) => {
);
};
export default memo(BarChart);
export default BarChart;
+2 -2
View File
@@ -3,7 +3,7 @@ import useChartConfig from '@/components/echarts/config';
import EmptyData from '@/components/empty-data';
import { genColors } from '@/utils';
import _ from 'lodash';
import React, { memo, useMemo } from 'react';
import React, { useMemo } from 'react';
import echarts from '.';
import { ChartProps } from './types';
@@ -169,4 +169,4 @@ const LineChart: React.FC<ChartProps> = (props) => {
);
};
export default memo(LineChart);
export default LineChart;
+4 -1
View File
@@ -158,7 +158,10 @@ const MixLineBarChart: React.FC<
return (
<>
{!lineSeriesData.length && !barSeriesData.length ? (
<EmptyData height={height} title={title}></EmptyData>
<EmptyData
height={height}
title={_.get(title, 'text', title || '')}
></EmptyData>
) : (
<Chart
height={height}
@@ -1,4 +1,3 @@
import { memo } from 'react';
import CodeViewer from './code-viewer';
import './styles/dark.less';
@@ -38,4 +37,4 @@ const DarkViewer: React.FC<CodeViewerProps> = (props) => {
);
};
export default memo(DarkViewer);
export default DarkViewer;
@@ -1,4 +1,3 @@
import { memo } from 'react';
import CodeViewer from './code-viewer';
import './styles/light.less';
@@ -39,4 +38,4 @@ const LightViewer: React.FC<CodeViewerProps> = (props) => {
);
};
export default memo(LightViewer);
export default LightViewer;
@@ -1,6 +1,6 @@
import classNames from 'classnames';
import hljs from 'highlight.js';
import { memo, useMemo } from 'react';
import { useMemo } from 'react';
import styled from 'styled-components';
import CopyButton from '../copy-button';
import { escapeHtml } from './utils';
@@ -155,4 +155,4 @@ const CodeViewer: React.FC<CodeViewerProps> = (props) => {
);
};
export default memo(CodeViewer);
export default CodeViewer;
@@ -46,7 +46,7 @@
white-space: pre-wrap;
code {
line-height: 1.5;
line-height: 1.6;
}
&.copyable {
+3 -1
View File
@@ -10,6 +10,7 @@ import {
KubernetesOutlined,
ProfileOutlined,
RetweetOutlined,
StarOutlined,
ThunderboltOutlined
} from '@ant-design/icons';
import React from 'react';
@@ -58,7 +59,8 @@ const icons = {
LockPerson: React.createElement(IconFont, { type: 'icon-lock_person' }),
LockOpen: React.createElement(IconFont, { type: 'icon-lock_open' }),
Permission: React.createElement(IconFont, { type: 'icon-permission' }),
CaptivePortal: React.createElement(IconFont, { type: 'icon-captive_portal' })
CaptivePortal: React.createElement(IconFont, { type: 'icon-captive_portal' }),
StarOutlined: React.createElement(StarOutlined)
};
export default icons;
+2 -2
View File
@@ -1,8 +1,8 @@
import { createFromIconfontCN } from '@ant-design/icons';
import './iconfont/iconfont.js';
// import './iconfont/iconfont.js';
const IconFont = createFromIconfontCN({
scriptUrl: ''
scriptUrl: '//at.alicdn.com/t/c/font_4613488_xvmo08q7urm.js'
});
export default IconFont;
+5 -7
View File
@@ -8,13 +8,14 @@ interface HintInputProps {
onChange: (value: string) => void;
onBlur?: (e: any) => void;
placeholder?: string;
trim?: boolean;
sourceOptions?: Global.HintOptions[];
}
const matchReg = /[^=]+=[^=]*$/;
const HintInput: React.FC<HintInputProps> = (props) => {
const { value, label, onChange, onBlur, sourceOptions } = props;
const { value, label, onChange, onBlur, sourceOptions, trim = true } = props;
const cursorPosRef = React.useRef(0);
const contextBeforeCursorRef = React.useRef('');
const [options, setOptions] = React.useState<
@@ -70,11 +71,7 @@ const HintInput: React.FC<HintInputProps> = (props) => {
const handleInput = (e: any) => {
getContextBeforeCursor(e);
onChange(e.target.value?.trim());
};
const handleOnChange = (value: string) => {
onChange(value?.trim());
onChange(e.target.value);
};
const handleOnSelect = (value: string) => {
@@ -93,9 +90,10 @@ const HintInput: React.FC<HintInputProps> = (props) => {
onBlur={onBlur}
label={label}
options={options}
trim={trim}
style={{ width: '100%' }}
/>
);
};
export default React.memo(HintInput);
export default HintInput;
+4 -3
View File
@@ -1,4 +1,3 @@
import { useIntl } from '@umijs/max';
import _ from 'lodash';
import React from 'react';
import Wrapper from '../label-selector/wrapper';
@@ -12,13 +11,13 @@ interface ListInputProps {
options?: Global.HintOptions[];
placeholder?: string;
labelExtra?: React.ReactNode;
trim?: boolean;
onChange: (data: string[]) => void;
onBlur?: (e: any, index: number) => void;
onDelete?: (index: number) => void;
}
const ListInput: React.FC<ListInputProps> = (props) => {
const intl = useIntl();
const {
dataList,
label,
@@ -28,7 +27,8 @@ const ListInput: React.FC<ListInputProps> = (props) => {
onDelete,
btnText,
options,
labelExtra
labelExtra,
trim = true
} = props;
const [list, setList] = React.useState<{ value: string; uid: number }[]>([]);
const countRef = React.useRef(0);
@@ -97,6 +97,7 @@ const ListInput: React.FC<ListInputProps> = (props) => {
onBlur={(e) => onBlur?.(e, index)}
onRemove={() => handleOnRemove(index)}
onChange={(val) => handleOnChange(val, index)}
trim={trim}
/>
);
})}
+12 -2
View File
@@ -13,10 +13,19 @@ interface LabelItemProps {
label?: string;
placeholder?: string;
options?: Global.HintOptions[];
trim?: boolean;
}
const ListItem: React.FC<LabelItemProps> = (props) => {
const { onRemove, onChange, onBlur, label, value, options } = props;
const {
onRemove,
onChange,
onBlur,
label,
value,
options,
trim = true
} = props;
const handleOnChange = (value: any) => {
onChange(value);
@@ -30,6 +39,7 @@ const ListItem: React.FC<LabelItemProps> = (props) => {
onBlur={onBlur}
label={label}
sourceOptions={options}
trim={trim}
placeholder={props.placeholder}
/>
<Button
@@ -44,4 +54,4 @@ const ListItem: React.FC<LabelItemProps> = (props) => {
);
};
export default React.memo(ListItem);
export default ListItem;
+3 -3
View File
@@ -64,9 +64,9 @@ const LogsList: React.FC<LogsListProps> = forwardRef((props, ref) => {
const handleOnWheel = useCallback(
(e: any) => {
const scrollTop = scrollEventElement?.scrollTop;
const scrollHeight = scrollEventElement?.scrollHeight;
const clientHeight = scrollEventElement?.clientHeight;
const scrollTop = scrollEventElement?.current.scrollTop;
const scrollHeight = scrollEventElement?.current.scrollHeight;
const clientHeight = scrollEventElement?.current.clientHeight;
stopScroll.current = scrollTop + clientHeight <= scrollHeight;
@@ -1,4 +1,5 @@
import useSetChunkFetch from '@/hooks/use-chunk-fetch';
import { useMemoizedFn } from 'ahooks';
import { Spin } from 'antd';
import classNames from 'classnames';
import _ from 'lodash';
@@ -177,7 +178,7 @@ const LogsViewer: React.FC<LogsViewerProps> = forwardRef((props, ref) => {
});
};
const handleOnScroll = useCallback(
const handleOnScroll = useMemoizedFn(
async (data: { isTop: boolean; isBottom: boolean }) => {
const { isTop, isBottom } = data;
setIsAtTop(isTop);
@@ -225,17 +226,7 @@ const LogsViewer: React.FC<LogsViewerProps> = forwardRef((props, ref) => {
} else if (isBottom && page < totalPage) {
// getNextPage();
}
},
[
loading,
logs.length,
pageSize,
enableScorllLoad,
page,
totalPage,
setScrollPos,
createChunkConnection
]
}
);
const debouncedScroll = useCallback(
+1 -2
View File
@@ -6,7 +6,6 @@ import classNames from 'classnames';
import _ from 'lodash';
import {
forwardRef,
memo,
useEffect,
useImperativeHandle,
useRef,
@@ -176,4 +175,4 @@ const LogsViewer: React.FC<LogsViewerProps> = forwardRef((props, ref) => {
);
});
export default memo(LogsViewer);
export default LogsViewer;
+35
View File
@@ -0,0 +1,35 @@
import { Col, Row } from 'antd';
import ResizeObserver from 'rc-resize-observer';
import useResponsive from './use-responsive';
interface ResizeObserverContainerProps {
dataList: any[];
renderItem: (data: any) => React.ReactNode;
defaultSpan?: number;
resizable?: boolean;
}
const ResizeContainer: React.FC<ResizeObserverContainerProps> = ({
defaultSpan = 8,
dataList,
renderItem,
resizable = true
}) => {
const { span, handleResize } = useResponsive({ defaultSpan });
return (
<ResizeObserver onResize={handleResize} disabled={!resizable}>
<Row gutter={[16, 16]}>
{dataList.map((item: any, index) => {
return (
<Col span={span} key={item.id || index}>
{renderItem(item)}
</Col>
);
})}
</Row>
</ResizeObserver>
);
};
export default ResizeContainer;
@@ -0,0 +1,23 @@
import breakpoints from '@/config/breakpoints';
import { useMemoizedFn } from 'ahooks';
import _ from 'lodash';
import { useState } from 'react';
const useResponsive = ({ defaultSpan = 8 }: { defaultSpan?: number }) => {
const [span, setSpan] = useState(defaultSpan);
const getSpanByWidth = (width: number) => {
if (width < breakpoints.md) return 24;
if (width < breakpoints.lg) return 12;
return 8;
};
const handleResize = useMemoizedFn(
_.throttle((size: { width: number; height: number }) => {
setSpan(getSpanByWidth(size.width));
}, 100)
);
return { span, handleResize };
};
export default useResponsive;
+26 -1
View File
@@ -2,8 +2,20 @@ import { useEscHint } from '@/hooks/use-esc-hint';
import { CloseOutlined } from '@ant-design/icons';
import { Button, Drawer, type DrawerProps } from 'antd';
/**
* use ColumnWrapper to wrap content in Drawer with scroller
* 57px is the height of header
* @param props
* @returns
*/
const ScrollerModal = (props: DrawerProps) => {
const { title, closable = true, maskClosable = false, ...restProps } = props;
const {
title,
closable = true,
maskClosable = false,
styles,
...restProps
} = props;
const { EscHint } = useEscHint({
enabled: !props.keyboard && props.open
});
@@ -14,6 +26,19 @@ const ScrollerModal = (props: DrawerProps) => {
<>
<Drawer
{...restProps}
styles={{
body: {
height: 'calc(100vh - 57px)',
paddingBlock: 16,
paddingInline: 0,
overflowX: 'hidden',
...styles?.body
},
content: {
borderRadius: '6px 0 0 6px',
...styles?.content
}
}}
closable={false}
maskClosable={maskClosable}
title={
+24 -11
View File
@@ -3,6 +3,7 @@ import useOverlayScroller from '@/hooks/use-overlay-scroller';
import { Modal, type ModalProps } from 'antd';
import React from 'react';
import styled from 'styled-components';
import { ScrollerContext } from './use-scroller-context';
const Wrapper = styled.div<{ $maxHeight?: number | string }>`
max-height: ${({ $maxHeight }) =>
@@ -11,12 +12,21 @@ const Wrapper = styled.div<{ $maxHeight?: number | string }>`
width: 100%;
`;
const Title = styled.div`
display: flex;
align-items: center;
max-width: 360px;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
`;
const ScrollerModal = (
props: ModalProps & { maxContentHeight?: number | string }
) => {
const scroller = React.useRef<any>(null);
const { saveScrollHeight, restoreScrollHeight } = useBodyScroll();
const { initialize, destroyInstance } = useOverlayScroller();
const { initialize, destroyInstance, scrollToBottom } = useOverlayScroller();
React.useEffect(() => {
if (props.open) {
@@ -47,6 +57,7 @@ const ScrollerModal = (
return (
<Modal
{...props}
title={<Title>{props.title}</Title>}
destroyOnHidden={true}
styles={{
content: {
@@ -68,16 +79,18 @@ const ScrollerModal = (
: {}
}}
>
<Wrapper
ref={scroller}
data-overlayscrollbars-initialize
className="overlay-scroller-wrapper"
$maxHeight={props.maxContentHeight || 500}
hidden={false}
style={{ paddingInline: 24, paddingBlockEnd: 0 }}
>
{props.children}
</Wrapper>
<ScrollerContext.Provider value={{ scrollToBottom }}>
<Wrapper
ref={scroller}
data-overlayscrollbars-initialize
className="overlay-scroller-wrapper"
$maxHeight={props.maxContentHeight || 500}
hidden={false}
style={{ paddingInline: 24, paddingBlockEnd: 0 }}
>
{props.children}
</Wrapper>
</ScrollerContext.Provider>
</Modal>
);
};
@@ -0,0 +1,11 @@
import { createContext, useContext } from 'react';
interface ScrollerContextProps {
scrollToBottom: () => void;
}
export const ScrollerContext = createContext<ScrollerContextProps>({
scrollToBottom: () => {}
});
export const useScrollerContext = () => useContext(ScrollerContext);
@@ -90,6 +90,14 @@ const SealAutoComplete: React.FC<
const handleOnSelect = (value: any, option: any) => {
onSelect?.(value, option);
};
const handleOnInput = (e: any) => {
if (trim) {
e.target.value = e.target.value?.trim();
}
props.onInput?.(e);
};
const renderAfter = () => {
if (loading) {
return (
@@ -141,6 +149,7 @@ const SealAutoComplete: React.FC<
onSearch={handleSearch}
onChange={handleChange}
popupRender={popupRender}
onInput={handleOnInput}
></AutoComplete>
</Wrapper>
</SelectWrapper>
+57 -41
View File
@@ -2,52 +2,68 @@ import IconFont from '@/components/icon-font';
import type { SelectProps } from 'antd';
import { Select } from 'antd';
import React, { forwardRef, useImperativeHandle } from 'react';
import styled from 'styled-components';
import NotFoundContent from '../components/not-found-content';
const BaseSelect: React.FC<SelectProps & { ref?: any }> = forwardRef(
(props, ref) => {
const { notFoundContent, loading, ...restProps } = props;
const [isFocus, setIsFocus] = React.useState(false);
const inputRef = React.useRef<any>(null);
const Footer = styled.div`
color: var(--ant-color-text-tertiary);
margin-top: 8px;
margin-bottom: 0;
padding: 8px 12px;
border-top: 1px solid var(--ant-color-split);
`;
useImperativeHandle(ref, () => ({
...(inputRef.current || ({} as any))
}));
const BaseSelect: React.FC<
SelectProps & { ref?: any; footer?: React.ReactNode }
> = forwardRef((props, ref) => {
const { notFoundContent, loading, ...restProps } = props;
const [isFocus, setIsFocus] = React.useState(false);
const inputRef = React.useRef<any>(null);
const handleFocus = (e: React.FocusEvent<HTMLDivElement>) => {
setIsFocus(true);
props.onFocus?.(e);
};
const handleBlur = (e: React.FocusEvent<HTMLDivElement>) => {
setIsFocus(false);
props.onBlur?.(e);
};
const renderSuffixIcon = () => {
if (props.suffixIcon) {
return props.suffixIcon;
useImperativeHandle(ref, () => ({
...(inputRef.current || ({} as any))
}));
const handleFocus = (e: React.FocusEvent<HTMLDivElement>) => {
setIsFocus(true);
props.onFocus?.(e);
};
const handleBlur = (e: React.FocusEvent<HTMLDivElement>) => {
setIsFocus(false);
props.onBlur?.(e);
};
const renderSuffixIcon = () => {
if (props.suffixIcon) {
return props.suffixIcon;
}
if (!props.showSearch) {
return <IconFont type="icon-down"></IconFont>;
}
return !isFocus ? <IconFont type="icon-down"></IconFont> : undefined;
};
return (
<Select
{...restProps}
notFoundContent={
<NotFoundContent loading={loading} notFoundContent={notFoundContent} />
}
if (!props.showSearch) {
return <IconFont type="icon-down"></IconFont>;
ref={inputRef}
onFocus={handleFocus}
onBlur={handleBlur}
suffixIcon={renderSuffixIcon()}
popupRender={
props.footer
? (originNode) => (
<>
{originNode}
{props.footer && <Footer>{props.footer}</Footer>}
</>
)
: undefined
}
return !isFocus ? <IconFont type="icon-down"></IconFont> : undefined;
};
return (
<Select
{...restProps}
notFoundContent={
<NotFoundContent
loading={loading}
notFoundContent={notFoundContent}
/>
}
ref={inputRef}
onFocus={handleFocus}
onBlur={handleBlur}
suffixIcon={renderSuffixIcon()}
/>
);
}
);
/>
);
});
export default BaseSelect;
@@ -12,10 +12,11 @@
.note-info {
margin-left: 4px;
display: flex;
align-items: center;
}
.star {
position: relative;
top: 2px;
font-size: 0.7rem;
}
}
@@ -14,7 +14,7 @@ const NoteInfo: React.FC<NoteInfoProps> = (props) => {
if (!label) return null;
const renderRequiredStar = required ? (
<span className="star" style={{ color: 'red' }}>
*
</span>
) : null;
+1
View File
@@ -64,6 +64,7 @@ const SealPassword: React.FC<InputProps & SealFormItemProps> = (props) => {
required={required}
description={description}
disabled={props.disabled}
labelExtra={props.labelExtra}
hasPrefix={!!props.prefix}
onClick={handleClickWrapper}
>
+3 -1
View File
@@ -10,7 +10,9 @@ import { SealFormItemProps } from './types';
import Wrapper from './wrapper';
import SelectWrapper from './wrapper/select';
const SealSelect: React.FC<SelectProps & SealFormItemProps> = (props) => {
const SealSelect: React.FC<
SelectProps & SealFormItemProps & { footer?: React.ReactNode }
> = (props) => {
const {
label,
placeholder,
+1 -1
View File
@@ -102,7 +102,7 @@ const SealTextArea: React.FC<InputTextareaProps & SealFormItemProps> = (
<InputWrapper>
<Wrapper
status={status}
label={<LabelWrapper>{label}</LabelWrapper>}
label={label && <LabelWrapper>{label}</LabelWrapper>}
isFocus={alwaysFocus || isFocus}
required={required}
description={description}
+75
View File
@@ -0,0 +1,75 @@
import { Switch, Tooltip } from 'antd';
import React, { useState } from 'react';
import styled from 'styled-components';
import LabelInfo from '../seal-form/components/label-info';
const SwitchContainer = styled.div`
display: flex;
flex-direction: column;
justify-content: center;
gap: 8px;
border: 1px solid var(--ant-color-border);
border-radius: var(--ant-border-radius);
padding: 12px 14px;
min-height: 54px;
`;
const LabelContainer = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
`;
interface SwitchInputProps {
label?: React.ReactNode;
description?: string;
checked?: boolean;
defaultChecked?: boolean;
onChange?: (checked: boolean) => void;
children?: React.ReactNode;
style?: React.CSSProperties;
alwaysShowChildren?: boolean;
btnTips?: React.ReactNode;
size?: 'small' | 'default';
}
const SwitchInput: React.FC<SwitchInputProps> = (props) => {
const {
label,
description,
checked,
defaultChecked,
onChange,
children,
style,
alwaysShowChildren = true,
size,
btnTips
} = props;
const [internalChecked, setInternalChecked] = useState<boolean>(
defaultChecked || false
);
const handleChange = (checked: boolean) => {
setInternalChecked(checked);
onChange?.(checked);
};
return (
<SwitchContainer style={style}>
<LabelContainer>
<LabelInfo label={label} description={description} />
<Tooltip title={btnTips}>
<Switch
size={size}
checked={checked !== undefined ? checked : internalChecked}
onChange={handleChange}
/>
</Tooltip>
</LabelContainer>
{(alwaysShowChildren || internalChecked) && children}
</SwitchContainer>
);
};
export default SwitchInput;
@@ -1,15 +1,18 @@
import { Col, Row } from 'antd';
import React from 'react';
import { SealColumnProps } from '../types';
import { OnSortFn, SealColumnProps, TableOrder } from '../types';
import TableHeader from './table-header';
interface HeaderProps {
columns: SealColumnProps[];
onSort?: (dataIndex: string, order: any) => void;
sortDirections?: ('ascend' | 'descend' | null)[];
sorterList: TableOrder | Array<TableOrder>;
showSorterTooltip?: boolean;
onSort?: OnSortFn;
}
const Header: React.FC<HeaderProps> = (props) => {
const { onSort } = props;
const { onSort, sortDirections, sorterList, showSorterTooltip } = props;
return (
<Row className="row">
@@ -29,9 +32,12 @@ const Header: React.FC<HeaderProps> = (props) => {
<Col span={span} key={dataIndex || i}>
<TableHeader
onSort={onSort}
showSorterTooltip={showSorterTooltip}
sorter={sorter}
sorterList={sorterList}
dataIndex={dataIndex}
sortOrder={sortOrder}
sortDirections={sortDirections}
width={width}
defaultSortOrder={defaultSortOrder}
title={title}
@@ -29,7 +29,7 @@ const CellWrapper = styled.div`
`;
const TableCell: React.FC<SealColumnProps> = (props) => {
const { dataIndex, render, align, editable } = props;
const { dataIndex, render, align, editable, dataField } = props;
return (
<CellWrapper
@@ -40,7 +40,7 @@ const TableCell: React.FC<SealColumnProps> = (props) => {
})}
>
<CellContent
dataIndex={dataIndex}
dataIndex={dataField || dataIndex}
render={render}
editable={editable}
></CellContent>
@@ -1,7 +1,8 @@
import AutoTooltip from '@/components/auto-tooltip';
import { CaretDownOutlined, CaretUpOutlined } from '@ant-design/icons';
import { useIntl } from '@umijs/max';
import classNames from 'classnames';
import React from 'react';
import React, { useEffect } from 'react';
import '../styles/header.less';
import { TableHeaderProps } from '../types';
@@ -14,19 +15,74 @@ const TableHeader: React.FC<TableHeaderProps> = (props) => {
firstCell,
lastCell,
sortOrder,
sortDirections = ['ascend', 'descend', null],
defaultSortOrder,
onSort,
sorter,
sorterList,
sorter = false,
width,
dataIndex
} = props;
const intl = useIntl();
const [currentSortOrder, setCurrentSortOrder] = React.useState<
'ascend' | 'descend' | null
>(sortOrder || defaultSortOrder || null);
const getNextSortOrder = (currentOrder: 'ascend' | 'descend' | null) => {
const index = sortDirections.indexOf(currentOrder);
const nextIndex = (index + 1) % sortDirections.length;
return sortDirections[nextIndex];
};
const nextSortTips = () => {
if (!currentSortOrder) {
return intl.formatMessage({ id: 'common.sorter.tips.ascend' });
}
if (currentSortOrder === 'ascend') {
return intl.formatMessage({ id: 'common.sorter.tips.descend' });
}
return intl.formatMessage({ id: 'common.sorter.tips.cancel' });
};
const handleOnSort = () => {
if (sortOrder === 'ascend') {
onSort?.(dataIndex, 'descend');
} else {
onSort?.(dataIndex, 'ascend');
}
setCurrentSortOrder((prev) => {
const next = getNextSortOrder(prev);
onSort?.(
{
columnKey: dataIndex,
field: dataIndex,
order: next
},
sorter
);
return next;
});
};
useEffect(() => {
if (!sorterList) {
setCurrentSortOrder(null);
return;
}
if (Array.isArray(sorterList)) {
const sortItem = sorterList.find(
(item) => item.columnKey === dataIndex || item.field === dataIndex
);
setCurrentSortOrder(sortItem?.order || null);
} else {
if (
sorterList.columnKey === dataIndex ||
sorterList.field === dataIndex
) {
setCurrentSortOrder(sorterList.order);
} else {
setCurrentSortOrder(null);
}
}
}, [sorterList]);
return (
<div
style={{ width, ...style }}
@@ -47,12 +103,12 @@ const TableHeader: React.FC<TableHeaderProps> = (props) => {
<span className="sorter">
<CaretUpOutlined
className={classNames('sorter-up', {
'sorter-active': sortOrder === 'ascend'
'sorter-active': currentSortOrder === 'ascend'
})}
></CaretUpOutlined>
<CaretDownOutlined
className={classNames('sorter-down', {
'sorter-active': sortOrder === 'descend'
'sorter-active': currentSortOrder === 'descend'
})}
></CaretDownOutlined>
</span>
+15 -2
View File
@@ -7,6 +7,7 @@ import HeaderPrefix from './components/header-prefix';
import TableBody from './components/table-body';
import './styles/index.less';
import { SealColumnProps, SealTableProps } from './types';
import useSorter from './use-sorter';
const Wrapper = styled.div<{ $token: any }>`
--ant-table-cell-padding-inline: ${(props) =>
@@ -32,7 +33,7 @@ const SealTable: React.FC<SealTableProps & { pagination: PaginationProps }> = (
childParentKey,
onExpand,
onExpandAll,
onSort,
onTableSort,
onCell,
expandedRowKeys,
loading,
@@ -43,10 +44,16 @@ const SealTable: React.FC<SealTableProps & { pagination: PaginationProps }> = (
rowSelection,
pagination,
empty,
sortDirections,
showSorterTooltip,
renderChildren,
loadChildren,
loadChildrenAPI
} = props;
const { handleOnTableSort, sorterList } = useSorter({
onTableSort,
columns
});
const { token } = theme.useToken();
const parsedColumns = useMemo(() => {
if (columns) return columns;
@@ -156,7 +163,13 @@ const SealTable: React.FC<SealTableProps & { pagination: PaginationProps }> = (
disabled={!props.dataSource?.length}
hasColumns={parsedColumns.length > 0}
></HeaderPrefix>
<Header onSort={onSort} columns={parsedColumns}></Header>
<Header
onSort={handleOnTableSort}
columns={parsedColumns}
sortDirections={sortDirections}
sorterList={sorterList}
showSorterTooltip={showSorterTooltip}
></Header>
</div>
<Spin spinning={loading}>
<TableBody
@@ -4,6 +4,7 @@
display: flex;
align-items: center;
justify-content: flex-start;
height: 50px;
padding-inline: var(--ant-table-cell-padding-inline);
&::before {
@@ -40,6 +41,7 @@
.sorter-header {
display: flex;
flex: 1;
width: 100%;
justify-content: space-between;
align-items: center;
cursor: pointer;
+32 -6
View File
@@ -1,5 +1,14 @@
import React from 'react';
export type OnSortFn = (
order: {
columnKey: string;
field: string;
order: 'ascend' | 'descend' | null;
},
sorter: boolean | { multiple?: number }
) => void;
export interface CellContentProps {
dataIndex: string;
render?: (text: any, record: any) => React.ReactNode;
@@ -16,11 +25,12 @@ export interface SealColumnProps {
render?: (text: any, record: any) => React.ReactNode;
dataIndex: string;
key?: string;
dataField?: string; // Added dataField property, aviods conflict with dataIndex, because dataIndex maybe used in sorting
width?: number;
span: number;
align?: 'left' | 'center' | 'right';
headerStyle?: React.CSSProperties;
sorter?: boolean;
sorter?: boolean | { multiple?: number };
defaultSortOrder?: 'ascend' | 'descend';
editable?:
| boolean
@@ -34,17 +44,25 @@ export interface SealColumnProps {
}
export interface TableHeaderProps {
sorter?: boolean;
defaultSortOrder?: 'ascend' | 'descend';
showSorterTooltip?: boolean;
sorterList?: TableOrder | Array<TableOrder>;
sorter?: boolean | { multiple?: number };
sortDirections?: ('ascend' | 'descend' | null)[];
defaultSortOrder?: 'ascend' | 'descend' | null;
sortOrder?: 'ascend' | 'descend' | null;
dataIndex: string;
onSort?: (dataIndex: string, order: 'ascend' | 'descend') => void;
onSort?: OnSortFn;
title: React.ReactNode;
style?: React.CSSProperties;
firstCell?: boolean;
lastCell?: boolean;
align?: 'left' | 'center' | 'right';
width?: number | string;
sortedDataIndexList?: Array<{
columnKey: string;
field: string;
order: 'ascend' | 'descend' | null;
}>;
}
export interface RowSelectionProps {
@@ -54,7 +72,15 @@ export interface RowSelectionProps {
removeSelectedKeys: (rowKeys: React.Key[]) => void;
onChange: (selectedRowKeys: React.Key[], selectedRows: any[]) => void;
}
export type TableOrder = {
columnKey?: string;
field?: string;
order: 'ascend' | 'descend' | null;
};
export interface SealTableProps {
showSorterTooltip?: boolean;
sortDirections?: ('ascend' | 'descend' | null)[];
columns?: SealColumnProps[];
childParentKey?: string;
expandedRowKeys?: React.Key[];
@@ -67,8 +93,8 @@ export interface SealTableProps {
watchChildren?: boolean;
loading?: boolean;
loadend?: boolean;
onCell?: (record: any, dataIndex: string) => void;
onSort?: (dataIndex: string, order: 'ascend' | 'descend') => void;
onCell?: (record: any, extra: any) => void;
onTableSort?: (order: TableOrder | Array<TableOrder>) => void;
onExpand?: (expanded: boolean, record: any, rowKey: any) => void;
onExpandAll?: (expanded: boolean) => void;
renderChildren?: (
+90
View File
@@ -0,0 +1,90 @@
import { isBoolean } from 'lodash';
import { useRef, useState } from 'react';
import { SealColumnProps, TableOrder } from './types';
const initSorterList = (columns: SealColumnProps[]) => {
const list = columns.filter((col) => col.defaultSortOrder);
if (list.length === 0) {
return [];
}
return list.map((col) => ({
columnKey: col.key || col.dataIndex,
field: col.dataIndex,
order: col.defaultSortOrder || null
}));
};
export default function useSorter(options: {
onTableSort?: (TableOrder: TableOrder | Array<TableOrder>) => void;
columns?: SealColumnProps[];
}) {
const { onTableSort, columns } = options;
const sorterListRef = useRef<TableOrder | Array<TableOrder>>(
initSorterList(columns || [])
);
const [sorterList, setSorterList] = useState<TableOrder | Array<TableOrder>>(
initSorterList(columns || [])
);
const handleOnTableSort = (
order: TableOrder,
sorter: boolean | { multiple?: number }
) => {
let currentOrder: TableOrder = { ...order };
if (order.order === null) {
currentOrder = {
columnKey: undefined,
field: undefined,
order: null
};
}
// single column sort
if (isBoolean(sorter)) {
setSorterList(currentOrder);
sorterListRef.current = currentOrder;
onTableSort?.(sorterListRef.current);
return;
}
// multi column sort
if (sorter && typeof sorter === 'object' && sorter.multiple) {
if (!Array.isArray(sorterListRef.current)) {
if (
sorterListRef.current.columnKey === currentOrder.columnKey ||
sorterListRef.current.field === currentOrder.field
) {
// remove the sorter if order is null
sorterListRef.current = {
...currentOrder
};
} else {
sorterListRef.current = [sorterListRef.current, { ...currentOrder }];
}
} else if (Array.isArray(sorterListRef.current)) {
const existingIndex = sorterListRef.current.findIndex(
(item) =>
item.columnKey === order.columnKey || item.field === order.field
);
if (existingIndex !== -1) {
sorterListRef.current.splice(existingIndex, 1);
sorterListRef.current.push({ ...currentOrder });
} else {
sorterListRef.current.push({ ...currentOrder });
}
}
}
setSorterList(sorterListRef.current);
onTableSort?.(sorterListRef.current);
};
return {
sorterListRef,
sorterList,
handleOnTableSort
};
}
+20 -59
View File
@@ -1,10 +1,9 @@
import breakpoints from '@/config/breakpoints';
import ResizeContainer from '@/components/resize-container';
import CardSkeleton from '@/components/templates/card-skelton';
import InfiniteScroller from '@/pages/_components/infinite-scroller';
import { useScrollerContext } from '@/pages/_components/infinite-scroller/use-scroller-context';
import { Col, FloatButton, Row, Spin } from 'antd';
import _ from 'lodash';
import ResizeObserver from 'rc-resize-observer';
import React, { useCallback } from 'react';
import { Spin } from 'antd';
import React from 'react';
import styled from 'styled-components';
const SpinWrapper = styled.div`
@@ -30,22 +29,12 @@ interface CatalogListProps {
activeId: Global.WithFalse<number>;
isFirst: boolean;
renderItem: (data: any) => React.ReactNode;
Skeleton: React.ComponentType<{ span: number }>;
}
const InnerSkeleton = (Skeleton: React.ComponentType<{ span: number }>) => {
return (props: any) => {
return <Skeleton {...props} />;
};
};
const ListSkeleton: React.FC<{
span: number;
loading: boolean;
isFirst: boolean;
Skeleton: React.ComponentType<{ span: number }>;
}> = ({ span, loading, isFirst, Skeleton }) => {
const CatalogSkeleton = InnerSkeleton(Skeleton);
}> = ({ loading, isFirst }) => {
return (
<div>
{loading && (
@@ -57,7 +46,7 @@ const ListSkeleton: React.FC<{
}}
wrapperClassName="skelton-wrapper"
>
{isFirst && <CatalogSkeleton span={span}></CatalogSkeleton>}
{isFirst && <CardSkeleton></CardSkeleton>}
</Spin>
</SpinWrapper>
)}
@@ -72,10 +61,8 @@ const CardList: React.FC<CatalogListProps> = (props) => {
isFirst,
defaultSpan = 8,
resizable = true,
Skeleton,
renderItem
} = props;
const [span, setSpan] = React.useState(defaultSpan);
const {
total,
current,
@@ -83,47 +70,21 @@ const CardList: React.FC<CatalogListProps> = (props) => {
refresh
} = useScrollerContext();
const getSpanByWidth = (width: number) => {
if (width < breakpoints.md) return 24;
if (width < breakpoints.lg) return 12;
return 8;
};
const handleResize = useCallback(
_.throttle((size: { width: number; height: number }) => {
setSpan(getSpanByWidth(size.width));
}, 100),
[]
);
return (
<div className="relative" style={{ width: '100%' }}>
<ResizeObserver onResize={handleResize} disabled={!resizable}>
<InfiniteScroller
total={total}
current={current}
loading={contextLoading}
refresh={refresh}
>
<Row gutter={[16, 16]}>
{dataList.map((item: any, index) => {
return (
<Col span={span} key={item.id}>
{renderItem?.(item)}
</Col>
);
})}
</Row>
<ListSkeleton
span={span}
loading={loading}
isFirst={isFirst}
Skeleton={Skeleton}
/>
</InfiniteScroller>
</ResizeObserver>
<FloatButton.BackTop visibilityHeight={1000} />
</div>
<InfiniteScroller
total={total}
current={current}
loading={contextLoading}
refresh={refresh}
>
<ResizeContainer
dataList={dataList}
renderItem={renderItem}
defaultSpan={defaultSpan}
resizable={resizable}
></ResizeContainer>
<ListSkeleton loading={loading} isFirst={isFirst} />
</InfiniteScroller>
);
};
+21 -26
View File
@@ -1,38 +1,33 @@
import { Col, Row, Skeleton } from 'antd';
import { Skeleton } from 'antd';
import React from 'react';
import ResizeContainer from '../resize-container';
interface CatalogSkeltonProps {
span: number;
skeletonProps?: any;
skeletonStyle?: React.CSSProperties;
}
const CardSkelton: React.FC<CatalogSkeltonProps> = (props) => {
return (
<Row gutter={[16, 16]}>
{Array(6)
.fill(1)
.map((_, index) => {
return (
<Col span={props.span} key={index}>
<Skeleton
avatar={{
size: 32
}}
paragraph={{ rows: 2 }}
style={{
border: '1px solid var(--ant-color-border)',
borderRadius: 'var(--border-radius-base)',
padding: '16px 16px',
...props.skeletonStyle
}}
{...(props.skeletonProps || {})}
></Skeleton>
</Col>
);
})}
</Row>
<ResizeContainer
dataList={Array(6).fill({ label: 'skeleton' })}
renderItem={() => (
<Skeleton
avatar={{
size: 32
}}
paragraph={{ rows: 2 }}
style={{
border: '1px solid var(--ant-color-border)',
borderRadius: 'var(--border-radius-base)',
padding: '16px 16px',
...props.skeletonStyle
}}
{...(props.skeletonProps || {})}
></Skeleton>
)}
></ResizeContainer>
);
};
export default React.memo(CardSkelton);
export default CardSkelton;
+1 -1
View File
@@ -87,4 +87,4 @@ const UploadAudio: React.FC<UploadAudioProps> = (props) => {
);
};
export default React.memo(UploadAudio);
export default UploadAudio;
+21 -17
View File
@@ -1,18 +1,29 @@
import Logo from '@/assets/images/gpustack-logo.png';
import { GPUStackVersionAtom, UpdateCheckAtom, userAtom } from '@/atoms/user';
import { getAtomStorage } from '@/atoms/utils';
import externalLinks from '@/constants/external-links';
import { Button } from 'antd';
import { useAtom } from 'jotai';
import React from 'react';
import './index.less';
const VersionInfo: React.FC<{ intl: any }> = ({ intl }) => {
const latestVersion = getAtomStorage(UpdateCheckAtom).latest_version;
const currentVersion = getAtomStorage(GPUStackVersionAtom)?.version;
const [gpuStackVersionAtom] = useAtom(GPUStackVersionAtom);
const [userDataAtom] = useAtom(userAtom);
const [updateCheck] = useAtom(UpdateCheckAtom);
const isProd =
currentVersion?.indexOf('rc') === -1 &&
currentVersion?.indexOf('0.0.0') === -1;
// current version info
const {
version: currentVersion,
git_commit,
isProd,
isDev
} = gpuStackVersionAtom;
// user info
const { is_admin } = userDataAtom || {};
// update check latest version info
const { latest_version: latestVersion } = updateCheck;
const uiVersion = document.documentElement.getAttribute('data-version');
@@ -29,10 +40,7 @@ const VersionInfo: React.FC<{ intl: any }> = ({ intl }) => {
</span>
)}
{isProd ? (
<span className="val">
{getAtomStorage(GPUStackVersionAtom)?.version ||
getAtomStorage(GPUStackVersionAtom)?.git_commit}
</span>
<span className="val">{currentVersion || git_commit}</span>
) : (
<span className="val dev">
<span className="item">
@@ -40,9 +48,7 @@ const VersionInfo: React.FC<{ intl: any }> = ({ intl }) => {
{' '}
{intl.formatMessage({ id: 'common.footer.version.server' })}
</span>
{currentVersion.indexOf('0.0.0') > -1
? getAtomStorage(GPUStackVersionAtom)?.git_commit
: getAtomStorage(GPUStackVersionAtom)?.version}
{isDev ? git_commit : currentVersion}
</span>
<span className="item">
<span className="tl">UI</span>
@@ -51,12 +57,10 @@ const VersionInfo: React.FC<{ intl: any }> = ({ intl }) => {
</span>
)}
</div>
{getAtomStorage(userAtom)?.is_admin && isProd && (
{is_admin && isProd && (
<div className="upgrade-text">
<span className="m-l-5">
{latestVersion &&
latestVersion !== currentVersion &&
latestVersion.indexOf('0.0.0') === -1
{latestVersion && latestVersion !== currentVersion && !isDev
? intl.formatMessage(
{ id: 'users.version.update' },
{ version: latestVersion }
+1 -1
View File
@@ -23,7 +23,7 @@ declare namespace Global {
require_password_change: boolean;
id: number;
source: string;
avatar: string;
avatar_url: string;
}
type EmptyObject = Record<never, never>;
+2 -2
View File
@@ -49,7 +49,7 @@ export const WatchEventType = {
};
export const PasswordReg =
/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%^&*_+])[a-zA-Z\d!@#$%^&*_+]{6,12}$/;
/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%^&*_+])[a-zA-Z\d!@#$%^&*_+]{6,64}$/;
export const uppercaseReg = /(?=.*[A-Z])/;
@@ -61,7 +61,7 @@ export const specialCharacterReg = /(?=.*[\W_])/;
export const noSpaceReg = /(?=\S+$)/;
export const lengthReg = /^.{6,12}$/;
export const lengthReg = /^.{6,64}$/;
/**
* Model name rules:
+22 -1
View File
@@ -2,10 +2,31 @@ export const HEADER_HEIGHT = 56;
export const DEFAULT_ENTER_PAGE = {
adminForNormal: '/dashboard',
adminForFirst: '/models/deployments',
adminForFirst: '/resources/workers',
user: '/models/user-models',
login: '/login'
};
export const GPUSTACK_API_BASE_URL = 'v2';
export const OPENAI_COMPATIBLE = 'v1';
type SortDirection = 'ascend' | 'descend' | null;
export const TABLE_SORT_DIRECTIONS: SortDirection[] = [
'ascend',
'descend',
null
];
export const tableSorter = (order: number | boolean) => {
return true;
// mutiple sorting can be supported in future
// if (typeof order === 'number') {
// return {
// multiple: order
// };
// }
// return order;
};
+2 -1
View File
@@ -10,7 +10,8 @@ export default {
inputFontSizeLG: 14
},
Steps: {
descriptionMaxWidth: 200
descriptionMaxWidth: 200,
iconSizeSM: 20
},
Table: {
headerBorderRadius: 4,
+2 -1
View File
@@ -10,7 +10,8 @@ export default {
inputFontSizeLG: 14
},
Steps: {
descriptionMaxWidth: 200
descriptionMaxWidth: 200,
iconSizeSM: 20
},
Table: {
headerBorderRadius: 4,
+10 -2
View File
@@ -203,7 +203,7 @@ body {
// form item help
.ant-form-item-with-help .ant-form-item-explain {
padding-left: 16px;
// padding-left: 16px;
}
// icon
@@ -218,6 +218,11 @@ body {
background-color: var(--color-fill-1);
height: 100%;
.ant-pro-sider-footer {
padding-block: 2px 0;
padding-left: 6px;
}
.ant-pro-sider-actions-list-item {
padding: 0;
@@ -228,7 +233,6 @@ body {
.ant-pro-sider-logo {
position: relative;
padding-left: 18px;
padding-block: 12px;
border-block-end: none;
}
@@ -788,3 +792,7 @@ body {
.ant-page-header .ant-page-header-heading-extra {
margin-block: 0;
}
.ant-pro-footer-bar {
padding-inline: 0 !important;
}
+3 -1
View File
@@ -82,7 +82,9 @@ export default function useOverlayScroller(data?: {
...scrollbars,
theme:
scrollbars?.theme ||
(userSettings.theme === 'light' ? 'os-theme-dark' : 'os-theme-light')
(userSettings.theme === 'light' || !userSettings.theme
? 'os-theme-dark'
: 'os-theme-light')
}
},
events: {
+28 -7
View File
@@ -1,11 +1,12 @@
import { TABLE_SORT_DIRECTIONS } from '@/config/settings';
import useSetChunkRequest from '@/hooks/use-chunk-request';
import useTableRowSelection from '@/hooks/use-table-row-selection';
import useTableSort from '@/hooks/use-table-sort';
import useUpdateChunkedList from '@/hooks/use-update-chunk-list';
import { handleBatchRequest } from '@/utils';
import _ from 'lodash';
import qs from 'query-string';
import { useEffect, useRef, useState } from 'react';
import { useTableMultiSort } from './use-table-sort';
type EventsType = 'CREATE' | 'UPDATE' | 'DELETE' | 'INSERT';
@@ -52,9 +53,7 @@ export default function useTableFetch<T>(
const chunkRequedtRef = useRef<any>(null);
const modalRef = useRef<any>(null);
const rowSelection = useTableRowSelection();
const { sortOrder, setSortOrder } = useTableSort({
defaultSortOrder: 'descend'
});
const { sortOrder, handleMultiSortChange } = useTableMultiSort();
// for skeleton loading
const [extraStatus, setExtraStatus] = useState<Record<string, any>>({
@@ -78,6 +77,7 @@ export default function useTableFetch<T>(
page: 1,
perPage: 10,
search: '',
sort_by: '',
...defaultQueryParams
});
@@ -103,7 +103,6 @@ export default function useTableFetch<T>(
const updateHandler = (list: any) => {
_.each(list, (data: any) => {
console.log('list================:', list);
updateChunkedList(data);
});
};
@@ -234,8 +233,29 @@ export default function useTableFetch<T>(
handleQueryChange({ page, perPage: pageSize || 10 });
};
const handleTableChange = (pagination: any, filters: any, sorter: any) => {
setSortOrder(sorter.order);
const handleTableChange = (
pagination: any,
filters: any,
sorter: any,
extra: any
) => {
if (extra.action === 'sort') {
const sortKeys = handleMultiSortChange(sorter);
setQueryParams((pre: any) => {
return {
...pre,
page: 1,
sort_by: sortKeys.join(',')
};
});
fetchData({
query: {
...queryParams,
page: 1,
sort_by: sortKeys.join(',')
}
});
}
};
const handleSearch = () => {
@@ -331,6 +351,7 @@ export default function useTableFetch<T>(
queryParams,
modalRef,
extraStatus,
TABLE_SORT_DIRECTIONS,
setQueryParams,
handleDelete,
handleDeleteBatch,
+29
View File
@@ -23,3 +23,32 @@ export default function useTableSort({
setSortOrder: handleSortChange
};
}
type orderType = {
columnKey?: string;
field?: string;
order: SortOrder;
};
export function useTableMultiSort() {
const [sortOrder, setSortOrder] = useState<string[]>([]);
const handleMultiSortChange = (order: orderType | orderType[]) => {
const sortOrders = Array.isArray(order) ? order : [order];
const sortOrderMap: string[] = [];
sortOrders.forEach((item) => {
const key = item.columnKey || item.field;
if (key) {
sortOrderMap.push(item.order === 'descend' ? `-${key}` : key);
}
});
setSortOrder(sortOrderMap);
return sortOrderMap;
};
return {
sortOrder,
handleMultiSortChange
};
}
+13 -1
View File
@@ -15,7 +15,10 @@ export function useUpdateChunkedList(options: {
events?: EventsType[];
dataList?: any[];
limit?: number;
setDataList: (args: any, opts?: any) => void;
onCreate?: (args: any) => void;
onUpdate?: (args: any) => void;
onDelete?: (args: any) => void;
setDataList?: (args: any, opts?: any) => void;
callback?: (args: any) => void;
filterFun?: (args: any) => boolean;
mapFun?: (args: any) => any;
@@ -31,6 +34,7 @@ export function useUpdateChunkedList(options: {
cacheDataListRef.current = [...(options.dataList || [])];
}, [options.dataList]);
// use to clear the table row selection after delete
const setDeletedIds = (ids: number[]) => {
deletedIdsRef.current = new Set([...deletedIdsRef.current, ...ids]);
};
@@ -41,6 +45,7 @@ export function useUpdateChunkedList(options: {
}, 200);
};
const updateChunkedList = (data: ChunkedCollection) => {
console.log('updateChunkedList data:', data);
let collections = data?.collection || [];
if (options?.computedID) {
collections = collections?.map((item: any) => {
@@ -55,6 +60,7 @@ export function useUpdateChunkedList(options: {
collections = data?.collection?.map(options?.mapFun);
}
const ids: any[] = data?.ids || [];
// CREATE
if (data?.type === WatchEventType.CREATE && events.includes('CREATE')) {
const newDataList = collections.reduce((acc: any[], item: any) => {
@@ -70,11 +76,15 @@ export function useUpdateChunkedList(options: {
return acc;
}, []);
cacheDataListRef.current = [
...newDataList,
...cacheDataListRef.current
].slice(0, limit);
options.onCreate?.(newDataList);
}
// DELETE
if (data?.type === WatchEventType.DELETE && events.includes('DELETE')) {
cacheDataListRef.current = cacheDataListRef.current?.filter(
@@ -87,6 +97,7 @@ export function useUpdateChunkedList(options: {
deletedIds: [...deletedIdsRef.current]
});
}
// UPDATE
if (data?.type === WatchEventType.UPDATE && events.includes('UPDATE')) {
collections?.forEach((item: any) => {
@@ -101,6 +112,7 @@ export function useUpdateChunkedList(options: {
updateItem,
...cacheDataListRef.current.slice(0, limit - 1)
];
options.onCreate?.([updateItem]);
}
});
}
+56 -49
View File
@@ -10,30 +10,38 @@ import {
DiscordOutlined,
GithubOutlined,
HomeOutlined,
InfoCircleOutlined,
LogoutOutlined,
ReadOutlined,
SettingOutlined
} from '@ant-design/icons';
import { history, useIntl, useNavigate } from '@umijs/max';
import { Avatar, Divider, Modal } from 'antd';
import { Avatar, Button, Divider, Modal } from 'antd';
import { useAtom } from 'jotai';
import { useMemo } from 'react';
import styled from 'styled-components';
import { DEFAULT_ENTER_PAGE } from '../config/settings';
const UpdateDot = styled.span`
margin-left: 5px;
display: flex;
width: 8px;
height: 8px;
border-radius: 4px;
background-color: var(--ant-orange-5);
`;
const NewLabel = styled.span`
position: relative;
bottom: 3px;
top: -4px;
right: 4px;
padding: 2px 4px;
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;
border-radius: 6px 6px 6px 0;
transform: scale(0.9);
.text {
@@ -117,17 +125,18 @@ export const ExtraContent = (props: { isDarkTheme?: boolean }) => {
const loginPath = DEFAULT_ENTER_PAGE.login;
// only admin can see upgrade info, and current version is a prod version(exclude dev/rc)
const showUpgrade = useMemo(() => {
return (
initialState?.currentUser?.is_admin &&
updateCheck.latest_version &&
updateCheck.latest_version !== version?.version &&
updateCheck.latest_version?.indexOf('0.0.0') === -1 &&
updateCheck.latest_version?.indexOf('rc') === -1
version?.isProd
);
}, [
updateCheck.latest_version,
version.version,
version.isProd,
initialState?.currentUser?.is_admin
]);
@@ -186,11 +195,6 @@ export const ExtraContent = (props: { isDarkTheme?: boolean }) => {
icon: <ReadOutlined />,
label: intl.formatMessage({ id: 'common.button.docs' }),
url: externalLinks.documentation
},
{
key: 'version',
icon: <InfoCircleOutlined />,
label: intl.formatMessage({ id: 'common.button.version' })
}
];
@@ -200,27 +204,14 @@ export const ExtraContent = (props: { isDarkTheme?: boolean }) => {
label: (
<span className="flex flex-center">
{item.icon}
{item.key === 'version' ? (
<>
<a className="m-l-8">{item.label}</a>
{showUpgrade && (
<NewLabel>
<span className="text">
{intl.formatMessage({ id: 'common.text.new' })}
</span>
</NewLabel>
)}
</>
) : (
<a
className="m-l-8 "
href={item.url}
target="_blank"
rel="noreferrer"
>
{item.label}
</a>
)}
<a
className="m-l-8 "
href={item.url}
target="_blank"
rel="noreferrer"
>
{item.label}
</a>
</span>
),
onClick() {
@@ -272,7 +263,7 @@ export const ExtraContent = (props: { isDarkTheme?: boolean }) => {
<Avatar
size={24}
style={{ ...avatarStyle }}
src={initialState?.currentUser?.avatar}
src={initialState?.currentUser?.avatar_url}
icon={
<IconFont type="icon-user-filled" className="font-size-24" />
}
@@ -300,21 +291,37 @@ export const ExtraContent = (props: { isDarkTheme?: boolean }) => {
return (
<Wrapper>
{contextHolder}
<div
style={{
display: 'flex',
alignItems: 'center'
}}
>
<Button
type="text"
size="small"
onClick={showVersion}
style={{
color: 'var(--ant-color-text-tertiary)'
}}
>
{version.version}
</Button>
{showUpgrade && (
<NewLabel>
<span className="text">
{intl.formatMessage({ id: 'common.text.new' })}
</span>
</NewLabel>
)}
</div>
<DropDownActions menu={{ ...helpMenu }} popupRender={helpPopupRender}>
<IconWrapper>
<IconFont type="icon-help" className="font-size-20" />
{showUpgrade && (
<span
className="m-l-5"
style={{
display: 'flex',
width: 8,
height: 8,
borderRadius: 4,
backgroundColor: 'var(--ant-orange-5)'
}}
></span>
)}
<IconFont
type="icon-help"
className="font-size-20"
style={{ color: 'var(--ant-color-text-tertiary)' }}
/>
</IconWrapper>
</DropDownActions>
<DropDownActions menu={{ ...userMenu }} popupRender={userPopupRender}>
@@ -322,7 +329,7 @@ export const ExtraContent = (props: { isDarkTheme?: boolean }) => {
<Avatar
size={24}
style={{ ...avatarStyle }}
src={initialState?.currentUser?.avatar}
src={initialState?.currentUser?.avatar_url}
icon={<IconFont type="icon-user-filled" className="font-size-24" />}
/>
</IconWrapper>
+55 -221
View File
@@ -1,24 +1,19 @@
// @ts-nocheck
import { routeCacheAtom, setRouteCache } from '@/atoms/route-cache';
import { GPUStackVersionAtom, UpdateCheckAtom, userAtom } from '@/atoms/user';
import { GPUStackVersionAtom, userAtom } from '@/atoms/user';
import DarkMask from '@/components/dark-mask';
import IconFont from '@/components/icon-font';
import ShortCuts, {
modalConfig as ShortCutsConfig
} from '@/components/short-cuts';
import VersionInfo, { modalConfig } from '@/components/version-info';
import routeCachekey from '@/config/route-cachekey';
import { DEFAULT_ENTER_PAGE } from '@/config/settings';
import useBodyScroll from '@/hooks/use-body-scroll';
import useOverlayScroller from '@/hooks/use-overlay-scroller';
import useUserSettings from '@/hooks/use-user-settings';
import useAddResource from '@/pages/dashboard/hooks/use-add-resource';
import { logout } from '@/pages/login/apis';
import { useAccessMarkedRoutes } from '@@/plugin-access';
import { useModel } from '@@/plugin-model';
import { ProLayout } from '@ant-design/pro-components';
import {
Link,
Outlet,
dropByCacheKey,
history,
@@ -29,18 +24,17 @@ import {
useNavigate,
type IRoute
} from '@umijs/max';
import { Button, ConfigProvider, Modal, Tooltip, theme } from 'antd';
import { Button, ConfigProvider, Modal, theme } from 'antd';
import 'driver.js/dist/driver.css';
import { useAtom } from 'jotai';
import 'overlayscrollbars/overlayscrollbars.css';
import { useEffect, useMemo, useState } from 'react';
import { useEffect, useMemo, useRef } from 'react';
import { PageContainerInner } from '../pages/_components/page-box';
import Exception from './Exception';
import './Layout.css';
import { LogoIcon, SLogoIcon } from './Logo';
import ErrorBoundary from './error-boundary';
import { ExtraContent } from './extraRender';
import { getRightRenderContent } from './rightRender';
import { patchRoutes } from './runtime';
import SiderMenu from './sider-menu';
@@ -55,18 +49,21 @@ const NO_CONTAINER_PAGES = [
'clusterCreate'
];
const loginPath = DEFAULT_ENTER_PAGE.login;
const CHECK_RESOURCE_PATH = [
'/resources/workers',
'/cluster-management/clusters/list',
'/cluster-management/credentials',
'/cluster-management/clusters/create',
''
];
type InitialStateType = {
fetchUserInfo: () => Promise<Global.UserInfo>;
currentUser?: Global.UserInfo;
};
const loginPath = DEFAULT_ENTER_PAGE.login;
// Filter out the routes that need to be displayed, where filterFn indicates the levels that should not be shown
const filterRoutes = (
routes: IRoute[],
filterFn: (route: IRoute) => boolean
) => {
): any[] => {
if (routes.length === 0) {
return [];
}
@@ -117,21 +114,26 @@ export default (props: any) => {
defer: false
});
const [modal, contextHolder] = Modal.useModal();
const { themeData, setTheme, setUserSettings, userSettings, isDarkTheme } =
useUserSettings();
const { saveScrollHeight, restoreScrollHeight } = useBodyScroll();
const { initialize: initializeMenu } = useOverlayScroller();
const { themeData, setUserSettings, userSettings } = useUserSettings();
const [userInfo] = useAtom(userAtom);
const [routeCache] = useAtom(routeCacheAtom);
const [version] = useAtom(GPUStackVersionAtom);
const [updateCheck] = useAtom(UpdateCheckAtom);
const location = useLocation();
const navigate = useNavigate();
const intl = useIntl();
const { clientRoutes, pluginManager } = useAppData();
// const [collapsed, setCollapsed] = useState(userSettings.collapsed || false);
const [collapseValue, setCollapseValue] = useState(false);
const [collapseKeys, setCollapseKeys] = useState<Set<string>>(new Set());
const { clientRoutes } = useAppData();
const [version] = useAtom(GPUStackVersionAtom);
const requestResourceRef = useRef<boolean>(false);
const {
setLoadingStatus,
fetchResourceData,
NoResourceModal,
loadingStatus
} = useAddResource({
onCreate() {
requestResourceRef.current = false;
}
});
const initialInfo = (useModel && useModel('@@initialState')) || {
initialState: undefined,
@@ -150,23 +152,6 @@ export default (props: any) => {
return intl.formatMessage({ id: args.id });
};
const showVersion = () => {
saveScrollHeight();
modal.info({
...modalConfig,
width: 460,
content: <VersionInfo intl={intl} />,
onCancel: restoreScrollHeight
});
};
const showShortcuts = () => {
Modal.info({
...ShortCutsConfig,
content: <ShortCuts intl={intl} />
});
};
const initRouteCacheValue = (pathname) => {
if (routeCache.get(pathname) === undefined && routeCachekey[pathname]) {
setRouteCache(pathname, false);
@@ -184,22 +169,17 @@ export default (props: any) => {
const runtimeConfig = {
...initialInfo,
logout: async (userInfo) => {
logout: async () => {
await logout();
navigate(loginPath);
},
showVersion: () => {
return showVersion();
},
showShortcuts: () => {
return showShortcuts();
},
showVersion: () => {},
showShortcuts: () => {},
notFound: <span>404 not found</span>
};
const handleToggleCollapse = (e: any) => {
e.stopPropagation();
// setCollapsed(!collapsed);
setUserSettings({
...userSettings,
collapsed: !userSettings.collapsed
@@ -235,38 +215,6 @@ export default (props: any) => {
console.log('matchedRoute=========', matchedRoute, route);
const allRouteKeys = useMemo(() => {
const keys = new Set<string>();
const childrenRoutes = route?.children || [];
const traverseRoutes = (routes) => {
routes.forEach((r) => {
if (r.path) {
keys.add(r.path);
}
if (r.children) {
traverseRoutes(r.children);
}
});
};
traverseRoutes(childrenRoutes);
return keys;
}, [route?.children]);
const showUpgrade = useMemo(() => {
return (
initialState?.currentUser?.is_admin &&
updateCheck.latest_version &&
updateCheck.latest_version !== version?.version &&
updateCheck.latest_version?.indexOf('0.0.0') === -1 &&
updateCheck.latest_version?.indexOf('rc') === -1
);
}, [
updateCheck.latest_version,
version.version,
initialState?.currentUser?.is_admin
]);
useEffect(() => {
const body = document.querySelector('body');
if (body) {
@@ -306,92 +254,23 @@ export default (props: any) => {
);
};
const handleToggleGroup = (menuItemProps, e) => {
e.stopPropagation();
if (collapseKeys.has(menuItemProps.key)) {
collapseKeys.delete(menuItemProps.key);
} else {
collapseKeys.add(menuItemProps.key);
}
setCollapseKeys(new Set(collapseKeys));
};
const menuContentRender = (menuProps, defaultDom) => {
const menuContentRender = (menuProps: any, defaultDom: React.ReactNode) => {
return <SiderMenu {...menuProps}></SiderMenu>;
};
const actionRender = (layoutProps) => {
const dom = getRightRenderContent({
runtimeConfig,
loading,
initialState,
setInitialState,
intl,
isDarkTheme: userSettings.isDarkTheme,
siderWidth: layoutProps?.siderWidth,
collapsed: layoutProps?.collapsed,
showUpgrade
});
return dom;
};
const menuItemRender = (menuItemProps, defaultDom) => {
if (menuItemProps.isUrl || menuItemProps.children) {
return defaultDom;
}
if (menuItemProps.path && location.pathname !== menuItemProps.path) {
return (
<Tooltip
title={collapsed ? menuItemProps.name : false}
placement="right"
>
<Link
to={menuItemProps.path.replace('/*', '')}
target={menuItemProps.target}
>
{defaultDom}
</Link>
</Tooltip>
);
}
return (
<Tooltip title={collapsed ? menuItemProps.name : false} placement="right">
{defaultDom}
</Tooltip>
);
};
const menuDataRender = (menuData) => {
const currentItem = menuData.find((s) => location.pathname === s.path);
const result = menuData.map((item) => {
const newItem = { ...item };
const selected =
location.pathname === newItem.path ||
location.pathname.indexOf(newItem.path) > -1;
if (newItem.icon) {
newItem.icon = selected ? (
<IconFont type={newItem.selectedIcon} />
) : (
<IconFont type={newItem.defaultIcon} />
);
}
if (newItem.children) {
newItem.children = menuDataRender(newItem.children);
}
return newItem;
});
return result;
};
const onPageChange = (route) => {
const onPageChange = async (route: any) => {
const { location } = history;
const { pathname } = location;
console.log('onPageChange', pathname, route);
if (
!CHECK_RESOURCE_PATH.includes(pathname) &&
initialState?.currentUser?.is_admin &&
!requestResourceRef.current &&
!userSettings.hideAddResourceModal
) {
requestResourceRef.current = true;
await fetchResourceData();
}
initRouteCacheValue(pathname);
dropRouteCache(pathname);
@@ -413,7 +292,7 @@ export default (props: any) => {
}
};
const onMenuHeaderClick = (e) => {
const onMenuHeaderClick = (e: React.MouseEvent) => {
e.stopPropagation();
e.preventDefault();
const pagepath = initialState?.currentUser?.is_admin
@@ -423,65 +302,17 @@ export default (props: any) => {
navigate(pagepath);
};
const onCollapse = (value) => {
const onCollapse = (value: boolean) => {
// only trigger by window resize
if (!value) {
return;
}
setUserSettings({
...userSettings,
collapsed: value
});
};
useEffect(() => {
// clear previous marks and measures
performance.clearMarks();
performance.clearMeasures();
// record start time
performance.mark('route-start');
requestAnimationFrame(() => {
// record end time
performance.mark('route-end');
// make sure `route-start` exists
if (performance.getEntriesByName('route-start').length > 0) {
performance.measure('route-change', 'route-start', 'route-end');
const measure = performance.getEntriesByName('route-change')[0];
console.log(
`[Performance] Route change to ${location.pathname} took ${measure.duration.toFixed(2)}ms`
);
// clear marks and measures
performance.clearMarks();
performance.clearMeasures();
} else {
console.warn('Missing performance mark: route-start');
}
});
}, [location.pathname]);
const onRenderCallback = (id, phase, actualDuration) => {
console.log(
`[Profiler] Route: ${id} - Phase: ${phase} - Render time: ${actualDuration.toFixed(2)}ms`
);
};
const currentTheme = useMemo(() => {
const data = {
algorithm: userSettings.isDarkTheme
? theme.darkAlgorithm
: theme.defaultAlgorithm,
...themeData
};
return data;
}, [userSettings.isDarkTheme, themeData]);
const outlet = runtimeConfig.childrenRender ? (
runtimeConfig.childrenRender(<Outlet />, props)
) : (
<Outlet />
);
return (
<ConfigProvider
componentSize="large"
@@ -530,7 +361,7 @@ export default (props: any) => {
type: 'group'
}}
splitMenus={true}
logo={collapsed ? SLogoIcon : LogoIcon}
logo={userSettings.collapsed ? <SLogoIcon /> : <LogoIcon />}
menuContentRender={menuContentRender}
disableContentMargin
{...runtimeConfig}
@@ -544,11 +375,14 @@ export default (props: any) => {
noAccessible={runtimeConfig?.noAccessible}
>
{isNoContainerPage ? (
outlet
<Outlet />
) : (
<PageContainerInner>{outlet}</PageContainerInner>
<PageContainerInner>
<Outlet />
</PageContainerInner>
)}
</Exception>
{NoResourceModal}
</ProLayout>
{contextHolder}
</ConfigProvider>
+1
View File
@@ -164,6 +164,7 @@ const SiderMenu: React.FC<SiderMenuProps> = (props) => {
key={key}
>
<Link
prefetch="intent"
to={menuItem.path.replace('/*', '')}
target={menuItem.target}
className={cx(styles.menuItemWrapper, 'menu-item', {
+7 -1
View File
@@ -32,5 +32,11 @@ export default {
'The built-in backends custom version name must end with "-custom".',
'backend.backend.rules.custom':
'The custom backend name must end with "-custom".',
'backend.quickConfig': 'Quick Config'
'backend.quickConfig': 'Quick Config',
'backend.version.default.not.exists':
'The default version does not exist in {versions}.',
'backend.replaceEntrypoint': 'Override Image Entrypoint',
'backend.entrypoint': 'Image Entrypoint',
'backend.entrypoint.tips':
'If specified, the ENTRYPOINT defined in the image will be ignored, and the command below will be used as the container startup entrypoint.'
};
+40 -4
View File
@@ -32,7 +32,7 @@ export default {
'clusters.create.configBasic': 'Basic Configuration',
'clusters.create.execCommand': 'Execute Command',
'clusters.create.supportedGpu': 'Supported GPUs',
'clusters.create.skipfornow': 'Skip for now',
'clusters.create.skipfornow': 'Skip for Now',
'clusters.create.noImages': 'No images available',
'clusters.create.noInstanceTypes': 'No instance types available',
'clusters.create.noRegions': 'No regions available',
@@ -68,17 +68,53 @@ Same applies to the <span class="bold-text">/opt/dtk</span> directory.`,
'clusters.addworker.checkEnv': 'Check Environment',
'clusters.addworker.specifyArgs': 'Specify Arguments',
'clusters.addworker.runCommand': 'Run Command',
'clusters.addworker.specifyWorkerIP': 'Specify Worker IP',
'clusters.addworker.specifyWorkerIP': 'Worker IP',
'clusters.addworker.detectWorkerIP': 'Auto-detect Worker IP',
'clusters.addworker.specifyWorkerAddress': 'Worker External Address',
'clusters.addworker.detectWorkerAddress': 'Worker External Address',
'clusters.addworker.detectWorkerAddress.tips':
'Defaults to Worker IP if not specified.',
'clusters.addworker.externalIP.tips':
'If running in a VPC or private network, please specify the Worker external address reachable by the GPUStack Server.',
'clusters.addworker.enterWorkerIP': 'Enter worker IP',
'clusters.addworker.enterWorkerIP.error': 'Please enter the worker IP.',
'clusters.addworker.enterWorkerAddress': 'Enter worker external address',
'clusters.addworker.enterWorkerAddress.error':
'Please enter the worker external address.',
'clusters.addworker.extraVolume': 'Additional Volume Mount',
'clusters.addworker.cacheVolume': 'Model Cache Volume Mount',
'clusters.addworker.cacheVolume.tips':
'If you want to customize the model cache directory, you can specify the path to mount it.',
'clusters.addworker.configSummary': 'Configuration Summary',
'clusters.addworker.gpuVendor': 'GPU Vendor',
'clusters.addworker.workerIP': 'Worker IP',
'clusters.addworker.workerExternalIP': 'Worker External Address',
'clusters.addworker.notSpecified': 'Not Specified',
'clusters.addworker.autoDetect': 'Auto-detect',
'clusters.addworker.autoDetect': 'Auto',
'clusters.addworker.extraVolume.holder':
'e.g. /data/models (path must start with /)',
'clusters.addworker.vendorNotes.title': 'Notes for {vendor} Device'
'clusters.addworker.cacheVolume.holder':
'e.g. /data/cache (path must start with /)',
'clusters.addworker.vendorNotes.title': 'Notes for {vendor} Device',
'clusters.button.genToken':
'Need to create a new token? Click <a href="{link}" target="_blank">here</a>.',
'clusters.addworker.amdNotes-01': `If the <span class="bold-text">/opt/rocm</span> directory does not exist, please create a symbolic link pointing to the ROCm installed path: <span class="bold-text">ln -s /path/to/rocm /opt/rocm</span>.`,
'clusters.addworker.message.success_single':
'{count} new worker has been added to the cluster.',
'clusters.addworker.message.success_multiple':
'{count} new workers have been added to the cluster.',
'clusters.create.serverUrl': 'Server URL',
'clusters.create.workerConfig': 'Worker Configuration',
'clusters.addworker.containerName': 'Worker Container Name',
'clusters.addworker.containerName.tips':
'Specify a name for the worker container.',
'clusters.addworker.dataVolume': 'GPUStack Data Volume',
'clusters.addworker.dataVolume.tips':
'Specify a data storage path for GPUStack.',
'clusters.table.ip.internal': 'Internal',
'clusters.table.ip.external': 'External',
'clusters.form.serverUrl.tips':
'Specify the server URL accessible from your cloud provider.',
'clusters.form.setDefault': 'Set as Default',
'clusters.form.setDefault.tips': 'Default for deployment.'
};
+6 -2
View File
@@ -247,7 +247,7 @@ export default {
'common.appearance.theme': 'Theme',
'common.page.wentwrong': 'Something went wrong.',
'common.page.refresh.tips':
'Oops! Something went wrong. Try refreshing the page.',
'The page may need to be updated. Try refreshing it!',
'common.tips.escape.disable':
'Click Cancel or the X at the top right to close.',
'common.button.clearSelection': 'Clear Selection',
@@ -265,5 +265,9 @@ export default {
'common.filter.status': 'Filter by status',
'common.form.rule.selectInput': 'Please select or enter a {name}',
'common.tag.experimental': 'Experimental',
'common.title.example': 'Example'
'common.title.example': 'Example',
'common.button.dontshowagain': "Don't show again",
'common.sorter.tips.ascend': 'Click to sort ascending',
'common.sorter.tips.descend': 'Click to sort descending',
'common.sorter.tips.cancel': 'Click to cancel sorting'
};
+18 -6
View File
@@ -69,11 +69,11 @@ export default {
'models.form.ollamalink':
'Find More in <a href="https://www.ollama.com/library" target="_blank">Ollama Library</a>.',
'models.form.backend_parameters.llamabox.placeholder':
'e.g., --ctx-size=8192 (use = to separate name and value)',
'e.g., --ctx-size=8192 (use = or a space to separate name and value)',
'models.form.backend_parameters.vllm.placeholder':
'e.g., --max-model-len=8192 (use = to separate name and value)',
'e.g., --max-model-len=8192 (use = or a space to separate name and value)',
'models.form.backend_parameters.sglang.placeholder':
'e.g., --context-length=8192 (use = to separate name and value)',
'e.g., --context-length=8192 (use = or a space to separate name and value)',
'models.form.backend_parameters.vllm.tips':
'For more details about {backend} parameters, see <a href={link} target="_blank">here</a>.',
'models.logs.pagination.prev': 'Previous {lines} Lines',
@@ -216,8 +216,8 @@ export default {
'models.form.gpusAllocationType.custom.tips':
'You can specify the exact number of GPUs per replica.',
'models.mymodels.status.inactive': 'Stopped',
'models.mymodels.status.degrade': 'Abnormal',
'models.mymodels.status.active': 'Active',
'models.mymodels.status.degrade': 'Not Ready',
'models.mymodels.status.active': 'Ready',
'models.form.kvCache.tips':
'Extended KV cache and speculative decoding are only available with built-in backends (vLLM / SGLang), Please switch the backend in the <span class="bold-text">Advanced</span> settings to enable them.',
'models.form.kvCache.tips2':
@@ -260,5 +260,17 @@ export default {
'models.form.generic_proxy.button': 'Generic Proxy',
'models.accessControlModal.includeusers': 'Include Users',
'models.table.genericProxy':
'Use the following path prefix, and set the model name in either the <span class="bold-text">X-GPUStack-Model</span> request header or the model field in the request body. All requests under this path prefix will be forwarded to the inference backend.'
'Use the following path prefix, and set the model name in either the <span class="bold-text">X-GPUStack-Model</span> request header or the model field in the request body. All requests under this path prefix will be forwarded to the inference backend.',
'models.form.backendVersion.deprecated': 'Deprecated',
'models.accessSettings.public.desc':
'Accessible to anyone without authentication.',
'models.accessSettings.authed.tips':
'Accessible to all authenticated platform users.',
'models.accessSettings.allowedUsers.tips':
'Only designated users can access the model.',
'models.form.backendVersions.tips': `To use more versions, go to the {link} page and edit the backend to add versions.`,
'models.catalog.nogpus.tips':
'No compatible GPUs are available in the selected cluster for this model.',
'models.form.modelfile.notfound': `The model file path you specified does not exist on the GPUStack server. It's recommended to place the model file at the same path on both the GPUStack server and GPUStack workers. This helps GPUStack make better decisions.`,
'models.form.readyWorkers': 'workers ready'
};
+8 -2
View File
@@ -12,7 +12,7 @@ export default {
'noresult.workers.title': 'No Workers',
'noresult.workers.subTitle': 'No workers found in any clusters.',
'noresult.workers.nofound': 'No matching workers found.',
'noresult.workers.button.add': 'Go to Add Worker',
'noresult.workers.button.add': 'Add Worker',
'noresult.modelfiles.title': 'No Model Files',
'noresult.modelfiles.subTitle': 'No model files have been added yet.',
'noresult.modelfiles.nofound': 'No matching model files found.',
@@ -33,5 +33,11 @@ export default {
'noresult.keys.nofound': 'No matching API keys found.',
'noresult.catalog.title': 'No Models',
'noresult.catalog.subTitle': 'No models have been configured yet.',
'noresult.catalog.nofound': 'No matching models found.'
'noresult.catalog.nofound': 'No matching models found.',
'noresult.resources.cluster':
'No clusters available. Add a cluster to get started.',
'noresult.resources.worker':
'No workers available. Add a worker to get started.',
'noresult.resources.gotocluster': 'Create Your First Cluster',
'noresult.resources.gotoworker': 'Add Worker'
};
+5 -1
View File
@@ -156,5 +156,9 @@ export default {
'playground.image.generate.error':
'Something went wrong. The image could not be generated.',
'playground.uploadfile.sizeError':
'File size exceeds the limit. Maximum allowed: {size}.'
'File size exceeds the limit. Maximum allowed: {size}.',
'playground.uploadImage.url.invalid':
'Please enter a direct image URL (e.g. https://.../image.png). Press ESC to cancel.',
'playground.uploadImage.url.holder': 'Enter an image URL',
'playground.uploadImage.url.button': 'Add Image from URL'
};
+5 -3
View File
@@ -19,12 +19,12 @@ export default {
'users.form.currentpassword': 'Current Password',
'users.form.updatepassword': 'Modify Password',
'users.form.rule.password':
'Contains uppercase and lowercase letters, numbers, and special characters(!@#$%^&*_+), 6 to 12 characters in length, no spaces allowed.',
'Contains uppercase and lowercase letters, numbers, and special characters(!@#$%^&*_+), 6 to 64 characters in length, no spaces allowed.',
'users.password.uppcase': 'At least one uppercase letter',
'users.password.lowercase': 'At least one lowercase letter',
'users.password.number': 'At least one number',
'users.password.special': 'At least one special character',
'users.password.length': 'Length between 6 and 12 characters',
'users.password.length': 'Length between 6 and 64 characters',
'users.password.modify.title': 'Modify Password',
'users.password.modify.description':
"For your account's security, please change your initial password.",
@@ -37,5 +37,7 @@ export default {
'users.settings.title': 'User Settings',
'users.status.activate': 'Activate Account',
'users.status.deactivate': 'Deactivate Account',
'users.status.inactiveAccount': 'Inactive Account'
'users.status.inactiveAccount': 'Inactive Account',
'users.login.getInitialPassword':
'Run the following command on your server to retrieve the initial admin password.'
};
+7 -1
View File
@@ -32,5 +32,11 @@ export default {
'The built-in backends custom version name must end with "-custom".',
'backend.backend.rules.custom':
'The custom backend name must end with "-custom".',
'backend.quickConfig': 'Quick Config'
'backend.quickConfig': 'Quick Config',
'backend.version.default.not.exists':
'The default version does not exist in {versions}.',
'backend.replaceEntrypoint': 'Override Image Entrypoint',
'backend.entrypoint': 'Image Entrypoint',
'backend.entrypoint.tips':
'If specified, the ENTRYPOINT defined in the image will be ignored, and the command below will be used as the container startup entrypoint.'
};
+67 -9
View File
@@ -32,7 +32,7 @@ export default {
'clusters.create.configBasic': 'Basic Configuration',
'clusters.create.execCommand': 'Execute Command',
'clusters.create.supportedGpu': 'Supported GPUs',
'clusters.create.skipfornow': 'Skip for now',
'clusters.create.skipfornow': 'Skip for Now',
'clusters.create.noImages': 'No images available',
'clusters.create.noInstanceTypes': 'No instance types available',
'clusters.create.noRegions': 'No regions available',
@@ -68,19 +68,55 @@ Same applies to the <span class="bold-text">/opt/dtk</span> directory.`,
'clusters.addworker.checkEnv': 'Check Environment',
'clusters.addworker.specifyArgs': 'Specify Arguments',
'clusters.addworker.runCommand': 'Run Command',
'clusters.addworker.specifyWorkerIP': 'Specify Worker IP',
'clusters.addworker.detectWorkerIP': 'Auto-detect Worker IP',
'clusters.addworker.specifyWorkerIP': 'Worker IP',
'clusters.addworker.detectWorkerIP': 'Worker IP を自動検出',
'clusters.addworker.specifyWorkerAddress': 'Worker 外部アドレス',
'clusters.addworker.detectWorkerAddress': 'Worker 外部アドレス',
'clusters.addworker.detectWorkerAddress.tips':
'指定がない場合、デフォルトで Worker IP が使用されます。',
'clusters.addworker.externalIP.tips':
'VPCやプライベートネットワークで動作している場合は、サーバーから到達可能なWorkerの外部アドレスを指定してください。',
'clusters.addworker.enterWorkerIP': 'Enter worker IP',
'clusters.addworker.enterWorkerIP.error': 'Please enter the worker IP.',
'clusters.addworker.enterWorkerAddress': 'Enter worker external address',
'clusters.addworker.enterWorkerAddress.error':
'Please enter the worker external address.',
'clusters.addworker.extraVolume': 'Additional Volume Mount',
'clusters.addworker.cacheVolume': 'Model Cache Volume Mount',
'clusters.addworker.cacheVolume.tips':
'If you want to customize the model cache directory, you can specify the path to mount it.',
'clusters.addworker.configSummary': 'Configuration Summary',
'clusters.addworker.gpuVendor': 'GPU Vendor',
'clusters.addworker.workerIP': 'Worker IP',
'clusters.addworker.workerExternalIP': 'Worker External Address',
'clusters.addworker.notSpecified': 'Not Specified',
'clusters.addworker.autoDetect': 'Auto-detect',
'clusters.addworker.autoDetect': 'Auto',
'clusters.addworker.extraVolume.holder':
'e.g. /data/models (path must start with /)',
'clusters.addworker.vendorNotes.title': 'Notes for {vendor} Device'
'clusters.addworker.cacheVolume.holder':
'e.g. /data/cache (path must start with /)',
'clusters.addworker.vendorNotes.title': 'Notes for {vendor} Device',
'clusters.button.genToken':
'Need to create a new token? Click <a href="{link}" target="_blank">here</a>.',
'clusters.addworker.amdNotes-01': `If the <span class="bold-text">/opt/rocm</span> directory does not exist, please create a symbolic link pointing to the ROCm installed path: <span class="bold-text">ln -s /path/to/rocm /opt/rocm</span>.`,
'clusters.addworker.message.success_single':
'{count} new worker has been added to the cluster.',
'clusters.addworker.message.success_multiple':
'{count} new workers have been added to the cluster.',
'clusters.create.serverUrl': 'Server URL',
'clusters.create.workerConfig': 'Worker Configuration',
'clusters.addworker.containerName': 'Worker Container Name',
'clusters.addworker.containerName.tips':
'Specify a name for the worker container.',
'clusters.addworker.dataVolume': 'GPUStack Data Volume',
'clusters.addworker.dataVolume.tips':
'Specify a data storage path for GPUStack.',
'clusters.table.ip.internal': 'Internal',
'clusters.table.ip.external': 'External',
'clusters.form.serverUrl.tips':
'Specify the server URL accessible from your cloud provider.',
'clusters.form.setDefault': 'Set as Default',
'clusters.form.setDefault.tips': 'Default for deployment.'
};
// ========== To-Do: Translate Keys (Remove After Translation) ==========
@@ -117,7 +153,7 @@ Same applies to the <span class="bold-text">/opt/dtk</span> directory.`,
// 31. 'clusters.create.configBasic': 'Basic Configuration',
// 32. 'clusters.create.execCommand': 'Execute Command',
// 33. 'clusters.create.supportedGpu': 'Supported GPUs',
// 34. 'clusters.create.skipfornow': 'Skip for now',
// 34. 'clusters.create.skipfornow': 'Skip for Now',
// 35. 'clusters.create.noImages': 'No images available',
// 36. 'clusters.create.noInstanceTypes': 'No instance types available',
// 37. 'clusters.create.noRegions': 'No regions available',
@@ -140,7 +176,7 @@ Same applies to the <span class="bold-text">/opt/dtk</span> directory.`,
// 54. 'clusters.addworker.checkEnv': 'Check Environment',
// 55. 'clusters.addworker.specifyArgs': 'Specify Arguments',
// 56. 'clusters.addworker.runCommand': 'Run Command',
// 57. 'clusters.addworker.specifyWorkerIP': 'Specify Worker IP',
// 57. 'clusters.addworker.specifyWorkerIP': 'Worker IP',
// 58. 'clusters.addworker.detectWorkerIP': 'Auto-detect Worker IP',
// 59. 'clusters.addworker.enterWorkerIP': 'Enter worker IP',
// 60. 'clusters.addworker.enterWorkerIP.error': 'Please enter the worker IP.',
@@ -148,8 +184,30 @@ Same applies to the <span class="bold-text">/opt/dtk</span> directory.`,
// 62. 'clusters.addworker.configSummary': 'Configuration Summary',
// 63. 'clusters.addworker.gpuVendor': 'GPU Vendor',
// 64. 'clusters.addworker.workerIP': 'Worker IP',
// 65. 'clusters.addworker.workerExternalIP': 'Worker External Address',
// 65. 'clusters.addworker.notSpecified': 'Not Specified',
// 66. 'clusters.addworker.autoDetect': 'Auto-detect',
// 66. 'clusters.addworker.autoDetect': 'Auto',
// 67. 'clusters.addworker.extraVolume.holder': 'e.g. /data/models (path must start with /)'
// 68. 'clusters.addworker.vendorNotes.title': 'Notes for {vendor} Device'
// 68. 'clusters.addworker.vendorNotes.title': 'Notes for {vendor} Device',
// 69. 'clusters.button.genToken': 'Need to create a new token? Click <a href="{link}" target="_blank">here</a>.',
// 70. 'clusters.addworker.amdNotes-01': `If the <span class="bold-text">/opt/rocm</span> directory does not exist, please create a symbolic link pointing to the ROCm installed path: <span class="bold-text">ln -s /path/to/rocm /opt/rocm</span>.`,
// 71. 'clusters.addworker.cacheVolume': 'Model Cache Volume Mount',
// 72. 'clusters.addworker.cacheVolume.tips': 'If you want to customize the model cache directory, you can specify the path to mount it.',
// 73. 'clusters.addworker.cacheVolume.holder': 'e.g. /data/cache (path must start with /)',
// 74. 'clusters.addworker.message.success_single': '{count} new worker has been added to the cluster.',
// 75. 'clusters.addworker.message.success_multiple': '{count} new workers have been added to the cluster.',
// 76. 'clusters.create.serverUrl': 'Server URL',
// 77. 'clusters.create.workerConfig': 'Worker Configuration'
// 78. 'clusters.addworker.containerName': 'Worker Container Name',
// 79. 'clusters.addworker.containerName.tips':'Specify a name for the worker container.',
// 77. 'clusters.addworker.dataVolume': 'GPUStack Data Volume',
// 78. 'clusters.addworker.dataVolume.tips': 'Specify a data storage path for GPUStack.',
// 79. 'clusters.table.ip.internal': 'Internal',
// 80. 'clusters.table.ip.external': 'External',
// 81. 'clusters.form.serverUrl.tips': 'Specify the server URL accessible from your cloud provider.',
// 82. 'clusters.addworker.externalIP.tips': 'Specify an external IP if the worker is in a VPC or private network.',
// 83. 'clusters.form.setDefault': 'Set as Default',
// 84. 'clusters.form.setDefault.tips': 'Default for deployment',
// 85. 'clusters.addworker.enterWorkerAddress': 'Enter worker external address',
// 86. 'clusters.addworker.enterWorkerAddress.error': 'Please enter the worker external address.',
// ========== End of To-Do List ==========
+12 -4
View File
@@ -247,7 +247,7 @@ export default {
'common.appearance.theme': 'Theme',
'common.page.wentwrong': 'Something went wrong.',
'common.page.refresh.tips':
'Oops! Something went wrong. Try refreshing the page.',
'The page may need to be updated. Try refreshing it!',
'common.tips.escape.disable':
'Click Cancel or the X at the top right to close.',
'common.button.clearSelection': 'Clear Selection',
@@ -265,7 +265,11 @@ export default {
'common.filter.status': 'Filter by status',
'common.form.rule.selectInput': 'Please select or enter a {name}',
'common.tag.experimental': 'Experimental',
'common.title.example': 'Example'
'common.title.example': 'Example',
'common.button.dontshowagain': "Don't show again",
'common.sorter.tips.ascend': 'Click to sort ascending',
'common.sorter.tips.descend': 'Click to sort descending',
'common.sorter.tips.cancel': 'Click to cancel sorting'
};
// ========== To-Do: Translate Keys (Remove After Translation) ==========
@@ -280,7 +284,7 @@ export default {
// 9. 'common.button.forgotpassword': 'Forgot Password?'
// 10. 'common.appearance.theme': 'Theme',
// 11. 'common.page.wentwrong': 'Something went wrong.',
// 12. 'common.page.refresh.tips': 'Oops! Something went wrong. Try refreshing the page.'
// 12. 'common.page.refresh.tips': 'The page may need to be updated. Try refreshing it!',
// 13. 'common.tips.escape.disable': 'Click Cancel or the X at the top right to close.'
// 14. 'common.button.clearSelection': 'Clear Selection',
// 15. 'common.select.count': '{count} selected',
@@ -296,5 +300,9 @@ export default {
// 25. 'common.filter.status': 'Filter by status'
// 26. 'common.form.rule.selectInput': 'Please select or enter a {name}',
// 27. 'common.tag.experimental': 'Experimental',
// 28. 'common.title.example': 'Example'
// 28. 'common.title.example': 'Example',
// 29. 'common.button.dontshowagain': "Don't show again",
// 30. 'common.sorter.tips.ascend': 'Click to sort ascending',
// 31. 'common.sorter.tips.descend': 'Click to sort descending',
// 32. 'common.sorter.tips.cancel': 'Click to cancel sorting'
// ========== End of To-Do List ==========
+29 -9
View File
@@ -70,11 +70,11 @@ export default {
'models.form.ollamalink':
'<a href="https://www.ollama.com/library" target="_blank">Ollamaライブラリ</a>でさらに探す',
'models.form.backend_parameters.llamabox.placeholder':
'例: --ctx-size=8192=で名前と値を分ける)',
'例: --ctx-size=8192=または空白で名前と値を分ける)',
'models.form.backend_parameters.vllm.placeholder':
'例: --max-model-len=8192=で名前と値を分ける)',
'例: --max-model-len=8192=または空白で名前と値を分ける)',
'models.form.backend_parameters.sglang.placeholder':
'例: --context-length=8192=で名前と値を分ける)',
'例: --context-length=8192=または空白で名前と値を分ける)',
'models.form.backend_parameters.vllm.tips':
'For more details about {backend} parameters, see <a href={link} target="_blank">here</a>.',
'models.logs.pagination.prev': '前の{lines}行',
@@ -216,8 +216,8 @@ export default {
'models.form.gpusAllocationType.custom.tips':
'You can specify the exact number of GPUs per replica.',
'models.mymodels.status.inactive': 'Stopped',
'models.mymodels.status.degrade': 'Abnormal',
'models.mymodels.status.active': 'Active',
'models.mymodels.status.degrade': 'Not Ready',
'models.mymodels.status.active': 'Ready',
'models.form.kvCache.tips':
'Extended KV cache and speculative decoding are only available with built-in backends (vLLM / SGLang), Please switch the backend in the <span class="bold-text">Advanced</span> settings to enable them.',
'models.form.kvCache.tips2':
@@ -260,7 +260,19 @@ export default {
'models.form.generic_proxy.button': 'Generic Proxy',
'models.accessControlModal.includeusers': 'Include Users',
'models.table.genericProxy':
'Use the following path prefix, and set the model name in either the <span class="bold-text">X-GPUStack-Model</span> request header or the model field in the request body. All requests under this path prefix will be forwarded to the inference backend.'
'Use the following path prefix, and set the model name in either the <span class="bold-text">X-GPUStack-Model</span> request header or the model field in the request body. All requests under this path prefix will be forwarded to the inference backend.',
'models.form.backendVersion.deprecated': 'Deprecated',
'models.accessSettings.public.desc':
'Accessible to anyone without authentication.',
'models.accessSettings.authed.tips':
'Accessible to all authenticated platform users.',
'models.accessSettings.allowedUsers.tips':
'Only designated users can access the model.',
'models.form.backendVersions.tips': `To use more versions, go to the {link} page and edit the backend to add versions.`,
'models.catalog.nogpus.tips':
'No compatible GPUs are available in the selected cluster for this model.',
'models.form.modelfile.notfound': `The model file path you specified does not exist on the GPUStack server. It's recommended to place the model file at the same path on both the GPUStack server and GPUStack workers. This helps GPUStack make better decisions.`,
'models.form.readyWorkers': 'workers ready'
};
// ========== To-Do: Translate Keys (Remove After Translation) ==========
@@ -302,8 +314,8 @@ export default {
// 37. 'models.form.gpusAllocationType.auto.tips': 'The system automatically calculates the GPU count per replica, using powers of two by default and capped by the selected GPUs.',
// 38. 'models.form.gpusAllocationType.custom.tips': 'You can specify the exact number of GPUs per replica.',
// 39. 'models.mymodels.status.inactive': 'Stopped',
// 41. 'models.mymodels.status.degrade': 'Abnormal',
// 42. 'models.mymodels.status.active': 'Active'
// 41. 'models.mymodels.status.degrade': 'Not Ready',
// 42. 'models.mymodels.status.active': 'Ready',
// 43. 'models.form.remoteURL.tips': 'Refer to the <a href="https://docs.lmcache.ai/api_reference/configurations.html" target="_blank">configuration documentation</a> for details.',
// 44. 'models.form.kvCache.tips': 'Extended KV cache and speculative decoding are only available with built-in backends (vLLM / SGLang), Please switch the backend in the <span class="bold-text">Advanced</span> settings to enable them.',
// 45. 'models.form.kvCache.tips2': 'Only supported when using built-in inference backends (vLLM or SGLang).',
@@ -347,5 +359,13 @@ export default {
// 64. 'models.table.userSelection.tips': 'Admin users can access all models by default.',
// 65. 'models.form.partialoffload.tips': `When CPU offloading is enabled, GPUStack will allocate CPU memory if GPU resources are insufficient. You must correctly configure the inference backend to use hybrid CPU+GPU or full CPU inference.`,
// 66. 'models.form.backend.warning': 'The selected backend does not support GGUF models. Please add a backend with GGUF support in the Inference Backend.',
// 67. 'models.form.backend.warning.gguf': 'Please ensure that the selected custom backend supports GGUF models.',
// 67. 'models.form.backend.warning.gguf': 'Please ensure that the selected custom backend supports GGUF models.',,
// 68. 'models.form.backendVersion.deprecated': 'Deprecated',
// 69. 'models.accessSettings.public.desc': 'Accessible to anyone without authentication.',
// 70. 'models.accessSettings.authed.tips': 'Accessible to all authenticated platform users.',
// 71.'models.accessSettings.allowedUsers.tips': 'Only designated users can access the model.',
// 72. 'models.form.backendVersions.tips': `To use more versions, go to the {link} page and edit the backend to add versions.`,
// 73. 'models.catalog.nogpus.tips': 'No compatible GPUs are available in the selected cluster for this model.',
// 74. 'models.form.modelfile.notfound': `The model file path you specified does not exist on the GPUStack server. It's recommended to place the model file at the same path on both the GPUStack server and GPUStack workers. This helps GPUStack make better decisions.`,
// 75. 'models.form.readyWorkers': 'workers ready'
// ========== End of To-Do List ==========
+8 -2
View File
@@ -12,7 +12,7 @@ export default {
'noresult.workers.title': 'No Workers',
'noresult.workers.subTitle': 'No workers found in any clusters.',
'noresult.workers.nofound': 'No matching workers found.',
'noresult.workers.button.add': 'Go to Add Worker',
'noresult.workers.button.add': 'Add Worker',
'noresult.modelfiles.title': 'No Model Files',
'noresult.modelfiles.subTitle': 'No model files have been added yet.',
'noresult.modelfiles.nofound': 'No matching model files found.',
@@ -33,5 +33,11 @@ export default {
'noresult.keys.nofound': 'No matching API keys found.',
'noresult.catalog.title': 'No Models',
'noresult.catalog.subTitle': 'No models have been configured yet.',
'noresult.catalog.nofound': 'No matching models found.'
'noresult.catalog.nofound': 'No matching models found.',
'noresult.resources.cluster':
'No clusters available. Add a cluster to get started.',
'noresult.resources.worker':
'No workers available. Add a worker to get started.',
'noresult.resources.gotocluster': 'Create Your First Cluster',
'noresult.resources.gotoworker': 'Add Worker'
};
+8 -1
View File
@@ -159,11 +159,18 @@ export default {
'playground.image.generate.error':
'Something went wrong. The image could not be generated.',
'playground.uploadfile.sizeError':
'File size exceeds the limit. Maximum allowed: {size}.'
'File size exceeds the limit. Maximum allowed: {size}.',
'playground.uploadImage.url.invalid':
'Please enter a direct image URL (e.g. https://.../image.png). Press ESC to cancel.',
'playground.uploadImage.url.holder': 'Enter an image URL',
'playground.uploadImage.url.button': 'Add Image from URL'
};
// ========== To-Do: Translate Keys (Remove After Translation) ==========
// 1. 'playground.rerank.query.validate': 'The query is required.'
// 2. 'playground.image.generate.error': 'Something went wrong. The image could not be generated.',
// 3. 'playground.uploadfile.sizeError': 'File size exceeds the limit. Maximum allowed: {size}.'
// 4. 'playground.uploadImage.url.invalid': 'Please enter a direct image URL(e.g. https://.../image.png). Press ESC to cancel.',
// 5. 'playground.uploadImage.url.holder': 'Enter an image URL'
// 6. 'playground.uploadImage.url.button': 'Add Image from URL'
// ========== End of To-Do List ==========
+7 -4
View File
@@ -20,12 +20,12 @@ export default {
'users.form.currentpassword': '現在のパスワード',
'users.form.updatepassword': 'パスワードを変更',
'users.form.rule.password':
'大文字と小文字、数字、特殊文字(!@#$%^&*_+)を含む6〜12文字で、スペースは使用できません。',
'大文字と小文字、数字、特殊文字(!@#$%^&*_+)を含む6〜64文字で、スペースは使用できません。',
'users.password.uppcase': '少なくとも1つの大文字を含む',
'users.password.lowercase': '少なくとも1つの小文字を含む',
'users.password.number': '少なくとも1つの数字を含む',
'users.password.special': '少なくとも1つの特殊文字を含む',
'users.password.length': '6〜12文字の長さ',
'users.password.length': '6〜64文字の長さ',
'users.password.modify.title': 'パスワードを変更',
'users.password.modify.description':
'アカウントのセキュリティのため、初期パスワードを変更してください。',
@@ -38,12 +38,15 @@ export default {
'users.settings.title': 'User Settings',
'users.status.activate': 'Activate Account',
'users.status.deactivate': 'Deactivate Account',
'users.status.inactiveAccount': 'Inactive Account'
'users.status.inactiveAccount': 'Inactive Account',
'users.login.getInitialPassword':
'Run the following command on your server to retrieve the initial admin password.'
};
// ========== To-Do: Translate Keys (Remove After Translation) ==========
// 1. 'users.settings.title': 'User Settings',
// 2. 'users.status.activate': 'Activate Account',
// 3. 'users.status.deactivate': 'Deactivate Account',
// 4. 'users.status.inactiveAccount': 'Inactive Account'
// 4. 'users.status.inactiveAccount': 'Inactive Account',
// 5. 'users.login.getInitialPassword': 'Run the following command on your server to retrieve the initial admin password.'
// ========== End of To-Do List ==========
+12 -2
View File
@@ -32,9 +32,19 @@ export default {
'Пользовательское название версии встроенного бэкенда должно оканчиваться на «-custom».',
'backend.backend.rules.custom':
'Пользовательское название бэкенда должно оканчиваться на «-custom».',
'backend.quickConfig': 'Быстрая настройка'
'backend.quickConfig': 'Быстрая настройка',
'backend.version.default.not.exists':
'The default version does not exist in {versions}.',
'backend.replaceEntrypoint': 'Override Image Entrypoint',
'backend.entrypoint': 'Image Entrypoint',
'backend.entrypoint.tips':
'If specified, the ENTRYPOINT defined in the image will be ignored, and the command below will be used as the container startup entrypoint.'
};
// ========== To-Do: Translate Keys (Remove After Translation) ==========
// 1. 'backend.version.default.not.exists': 'The default version does not exist in {versions}.',
// 2. 'backend.replaceEntrypoint': 'Override Image Entrypoint',
// 3. 'backend.entrypoint': 'Image Entrypoint',
// 4. 'backend.entrypoint.tips':
// 'If specified, the ENTRYPOINT defined in the image will be ignored, and the command below will be used as the container startup entrypoint.'
// ========== End of To-Do List ==========
+78 -43
View File
@@ -48,9 +48,9 @@ export default {
'На Kubernetes кластере, который необходимо добавить, выполните следующую команду, чтобы присоединить его узлы к кластеру.',
'cluster.provider.comingsoon': 'Скоро будет',
'clusters.addworker.nvidiaNotes-01':
'If multiple outbound IPs exist, specify the one you want the worker to use. Please double-check with <span class="bold-text">hostname -I | xargs -n1</span>.',
'Если существует несколько исходящих IP-адресов, укажите тот, который должен использовать воркер. Пожалуйста, перепроверьте с помощью <span class="bold-text">hostname -I | xargs -n1</span>.',
'clusters.addworker.nvidiaNotes-02':
'If a model directory already exists on the worker, you can specify the path to mount it.',
'Если директория с моделями уже существует на воркере, вы можете указать путь для её монтирования.',
'clusters.addworker.hygonNotes':
'Если директория <span class="bold-text">/opt/hyhal</span> не существует, создайте символическую ссылку на путь установки Hygon: <span class="bold-text">/opt/hyhal</span>. Аналогично для директории <span class="bold-text">/opt/dtk</span>.',
'clusters.addworker.corexNotes':
@@ -60,49 +60,84 @@ export default {
'clusters.addworker.cambriconNotes':
'Если директория <span class="bold-text">/usr/local/neuware</span> не существует, создайте символическую ссылку на путь установки Cambricon: <span class="bold-text">ln -s /path/to/neuware /usr/local/neuware</span>.',
'clusters.addworker.hygonNotes-02':
'If failed to detect devices, please try to remove <span class="bold-text">--env ROCM_SMI_LIB_PATH=/opt/hyhal/lib</span>.',
'clusters.addworker.selectCluster': 'Select Cluster',
'Если не удается обнаружить устройства, попробуйте удалить <span class="bold-text">--env ROCM_SMI_LIB_PATH=/opt/hyhal/lib</span>.',
'clusters.addworker.selectCluster': 'Выбрать кластер',
'clusters.addworker.selectCluster.tips':
'For non-Docker clusters, please register clusters or manage worker pools from the Clusters page.',
'clusters.addworker.selectGPU': 'Select GPU Vendor',
'clusters.addworker.checkEnv': 'Check Environment',
'clusters.addworker.specifyArgs': 'Specify Arguments',
'clusters.addworker.runCommand': 'Run Command',
'clusters.addworker.specifyWorkerIP': 'Specify Worker IP',
'clusters.addworker.detectWorkerIP': 'Auto-detect Worker IP',
'clusters.addworker.enterWorkerIP': 'Enter worker IP',
'clusters.addworker.enterWorkerIP.error': 'Please enter the worker IP.',
'clusters.addworker.extraVolume': 'Additional Volume Mount',
'clusters.addworker.configSummary': 'Configuration Summary',
'clusters.addworker.gpuVendor': 'GPU Vendor',
'clusters.addworker.workerIP': 'Worker IP',
'clusters.addworker.notSpecified': 'Not Specified',
'clusters.addworker.autoDetect': 'Auto-detect',
'Для не-Docker кластеров, пожалуйста, регистрируйте кластеры или управляйте пулами воркеров на странице Кластеры.',
'clusters.addworker.selectGPU': 'Выбрать производителя GPU',
'clusters.addworker.checkEnv': 'Проверить окружение',
'clusters.addworker.specifyArgs': 'Указать аргументы',
'clusters.addworker.runCommand': 'Выполнить команду',
'clusters.addworker.specifyWorkerIP': 'Указать IP воркера',
'clusters.addworker.detectWorkerIP': 'Автоматически определить IP воркера',
'clusters.addworker.specifyWorkerAddress': 'Указать внешний адрес воркера',
'clusters.addworker.detectWorkerAddress': 'Указать внешний адрес воркера',
'clusters.addworker.detectWorkerAddress.tips':
'По умолчанию используется IP воркера, если не указано иное.',
'clusters.addworker.externalIP.tips':
'Если работаете в VPC или частной сети, укажите внешний адрес воркера, доступный для сервера.',
'clusters.addworker.enterWorkerIP': 'Введите IP воркера',
'clusters.addworker.enterWorkerIP.error': 'Пожалуйста, введите IP воркера.',
'clusters.addworker.enterWorkerAddress': 'Enter worker external address',
'clusters.addworker.enterWorkerAddress.error':
'Please enter the worker external address.',
'clusters.addworker.extraVolume': 'Дополнительное монтирование тома',
'clusters.addworker.cacheVolume': 'Model Cache Volume Mount',
'clusters.addworker.cacheVolume.tips':
'If you want to customize the model cache directory, you can specify the path to mount it.',
'clusters.addworker.configSummary': 'Сводка конфигурации',
'clusters.addworker.gpuVendor': 'Производитель GPU',
'clusters.addworker.workerIP': 'IP воркера',
'clusters.addworker.workerExternalIP': 'Внешний адрес воркера',
'clusters.addworker.notSpecified': 'Не указано',
'clusters.addworker.autoDetect': 'Автоопределение',
'clusters.addworker.extraVolume.holder':
'e.g. /data/models (path must start with /)',
'clusters.addworker.vendorNotes.title': 'Notes for {vendor} Device'
'напр. /data/models (путь должен начинаться с /)',
'clusters.addworker.cacheVolume.holder':
'напр. /data/cache (путь должен начинаться с /)',
'clusters.addworker.vendorNotes.title': 'Примечания для устройств {vendor}',
'clusters.button.genToken':
'Need to create a new token? Click <a href="{link}" target="_blank">here</a>.',
'clusters.addworker.amdNotes-01': `If the <span class="bold-text">/opt/rocm</span> directory does not exist, please create a symbolic link pointing to the ROCm installed path: <span class="bold-text">ln -s /path/to/rocm /opt/rocm</span>.`,
'clusters.addworker.message.success_single':
'{count} new worker has been added to the cluster.',
'clusters.addworker.message.success_multiple':
'{count} new workers have been added to the cluster.',
'clusters.create.serverUrl': 'Server URL',
'clusters.create.workerConfig': 'Worker Configuration',
'clusters.addworker.containerName': 'Worker Container Name',
'clusters.addworker.containerName.tips':
'Specify a name for the worker container.',
'clusters.addworker.dataVolume': 'GPUStack Data Volume',
'clusters.addworker.dataVolume.tips':
'Specify a data storage path for GPUStack.',
'clusters.table.ip.internal': 'Internal',
'clusters.table.ip.external': 'External',
'clusters.form.serverUrl.tips':
'Specify the server URL accessible from your cloud provider.',
'clusters.form.setDefault': 'Set as Default',
'clusters.form.setDefault.tips': 'Default for deployment.'
};
// ========== To-Do: Translate Keys (Remove After Translation) ==========
// 1. 'clusters.addworker.hygonNotes-02': 'If failed to detect devices, please try to remove <span class="bold-text">--env ROCM_SMI_LIB_PATH=/opt/hyhal/lib</span>.',
// 2. 'clusters.addworker.selectCluster': 'Select Cluster',
// 3. 'clusters.addworker.selectCluster.tips': 'For non-Docker clusters, please register clusters or manage worker pools from the Clusters page.',
// 4. 'clusters.addworker.selectGPU': 'Select GPU Vendor',
// 5. 'clusters.addworker.checkEnv': 'Check Environment',
// 6. 'clusters.addworker.specifyArgs': 'Specify Arguments',
// 7. 'clusters.addworker.runCommand': 'Run Command',
// 8. 'clusters.addworker.specifyWorkerIP': 'Specify Worker IP',
// 9. 'clusters.addworker.detectWorkerIP': 'Auto-detect Worker IP',
// 10. 'clusters.addworker.enterWorkerIP': 'Enter worker IP',
// 11. 'clusters.addworker.enterWorkerIP.error': 'Please enter the worker IP.',
// 12. 'clusters.addworker.extraVolume': 'Additional Volume Mount',
// 13. 'clusters.addworker.configSummary': 'Configuration Summary',
// 14. 'clusters.addworker.gpuVendor': 'GPU Vendor',
// 15. 'clusters.addworker.workerIP': 'Worker IP',
// 16. 'clusters.addworker.notSpecified': 'Not Specified',
// 17. 'clusters.addworker.nvidiaNotes-01': 'If multiple outbound IPs exist, specify the one you want the worker to use. Please double-check with <span class="bold-text">hostname -I | xargs -n1</span>.',
// 18. 'clusters.addworker.nvidiaNotes-02': 'If a model directory already exists on the worker, you can specify the path to mount it.',
// 19. 'clusters.addworker.autoDetect': 'Auto-detect',
// 20. 'clusters.addworker.extraVolume.holder': 'e.g. /data/models (path must start with /)',
// 21. 'clusters.addworker.vendorNotes.title': 'Notes for {vendor} Device'
// ========== End of To-Do List ==========
// 1. 'clusters.addworker.amdNotes-01': `If the <span class="bold-text">/opt/rocm</span> directory does not exist, please create a symbolic link pointing to the ROCm installed path: <span class="bold-text">ln -s /path/to/rocm /opt/rocm</span>.`,
// 2. 'clusters.button.genToken': 'Need to create a new token? Click <a href="{link}" target="_blank">here</a>.',
// 3. 'clusters.addworker.cacheVolume': 'Model Cache Volume Mount',
// 4. 'clusters.addworker.cacheVolume.tips': 'If you want to customize the model cache directory, you can specify the path to mount it.',
// 5. 'clusters.addworker.message.success_single': '{count} new worker has been added to the cluster.',
// 6. 'clusters.addworker.message.success_multiple': '{count} new workers have been added to the cluster.',
// 7. 'clusters.create.serverUrl': 'Server URL',
// 8. 'clusters.create.workerConfig': 'Worker Configuration'
// 9. 'clusters.addworker.containerName': 'Worker Container Name',
// 10. 'clusters.addworker.containerName.tips':'Specify a name for the worker container.',
// 11. 'clusters.addworker.dataVolume': 'GPUStack Data Volume',
// 12. 'clusters.addworker.dataVolume.tips': 'Specify a data storage path for GPUStack.',
// 10. 'clusters.table.ip.internal': 'Internal',
// 11. 'clusters.table.ip.external': 'External',
// 12. 'clusters.form.serverUrl.tips': 'Specify the server URL accessible from your cloud provider.'
// 13. 'clusters.addworker.specifyWorkerIP': 'Specify Worker IP <span class="text-tertiary">{type}</span>',
// 14. 'clusters.form.setDefault': 'Set as Default',
// 15. 'clusters.form.setDefault.tips': 'Default for deployment',
// 16. 'clusters.addworker.enterWorkerAddress': 'Enter worker external address',
// 17. 'clusters.addworker.enterWorkerAddress.error': 'Please enter the worker external address.',
// ================================================================
+10 -3
View File
@@ -246,7 +246,7 @@ export default {
'common.appearance.theme': 'Тема',
'common.page.wentwrong': 'Что-то пошло не так.',
'common.page.refresh.tips':
'Упс! Что-то пошло не так. Попробуйте обновить страницу.',
'Страница может нуждаться в обновлении. Попробуйте обновить её!',
'common.tips.escape.disable':
'Чтобы закрыть, нажмите "Отмена" или крестик (X) в правом верхнем углу.',
'common.button.clearSelection': 'Сбросить выбор',
@@ -264,9 +264,16 @@ export default {
'common.filter.status': 'Фильтровать по статусу',
'common.form.rule.selectInput': 'Выберите или введите {name}',
'common.tag.experimental': 'Экспериментальный',
'common.title.example': 'Пример'
'common.title.example': 'Пример',
'common.button.dontshowagain': "Don't show again",
'common.sorter.tips.ascend': 'Click to sort ascending',
'common.sorter.tips.descend': 'Click to sort descending',
'common.sorter.tips.cancel': 'Click to cancel sorting'
};
// ========== To-Do: Translate Keys (Remove After Translation) ==========
// 1. 'common.button.dontshowagain': "Don't show again",
// 2. 'common.sorter.tips.ascend': 'Click to sort ascending',
// 3. 'common.sorter.tips.descend': 'Click to sort descending',
// 4. 'common.sorter.tips.cancel': 'Click to cancel sorting'
// ========== End of To-Do List ==========
+2 -2
View File
@@ -31,9 +31,9 @@ export default {
'dashboard.usage.datePicker.last7days': 'Последние 7 Days',
'dashboard.usage.datePicker.last30days': 'Последние 30 Days',
'dashboard.usage.datePicker.last60days': 'Последние 60 Days',
'dashboard.clusters': 'Clusters'
'dashboard.clusters': 'Кластеры'
};
// ========== To-Do: Translate Keys (Remove After Translation) ==========
// 1. 'dashboard.clusters': 'Clusters',
// ========== End of To-Do List ==========
+47 -38
View File
@@ -2,7 +2,7 @@ export default {
'models.button.deploy': 'Развернуть модель',
'models.title': 'Модели',
'models.title.edit': 'Редактировать модель',
'models.table.models': 'модели',
'models.table.models': 'Модели',
'models.table.name': 'Название модели',
'models.form.source': 'Источник',
'models.form.repoid': 'ID репозитория',
@@ -13,8 +13,10 @@ export default {
'models.form.env': 'Переменные окружения',
'models.form.configurations': 'Конфигурации',
'models.form.s3address': 'S3-адрес',
'models.form.partialoffload.tips': `When CPU offloading is enabled, GPUStack will allocate CPU memory if GPU resources are insufficient. You must correctly configure the inference backend to use hybrid CPU+GPU or full CPU inference.`, // Already translated
'models.form.distribution.tips': `Позволяет переносить часть слоёв модели на один или несколько удалённых воркеров, когда ресурсов текущего воркера недостаточно.`,
'models.form.partialoffload.tips':
'При включении CPU оффлоудинга GPUStack будет выделять оперативную память, если ресурсов GPU недостаточно. Вы должны правильно настроить бэкенд вывода для использования гибридного CPU+GPU или полного CPU вывода.',
'models.form.distribution.tips':
'Позволяет переносить часть слоёв модели на один или несколько удалённых воркеров, когда ресурсов текущего воркера недостаточно.',
'models.openinplayground': 'Открыть в Песочнице',
'models.instances': 'инстансы',
'models.table.replicas.edit': 'Редактировать реплики',
@@ -69,13 +71,13 @@ export default {
'models.form.ollamalink':
'Больше моделей в библиотеке <a href="https://www.ollama.com/library" target="_blank">Ollama</a>',
'models.form.backend_parameters.llamabox.placeholder':
'например: --ctx-size=8192(параметр и значение разделены знаком =)',
'например: --ctx-size=8192(параметр и значение разделены знаком = или пробелом',
'models.form.backend_parameters.vllm.placeholder':
'например: --max-model-len=8192(параметр и значение разделены знаком =)',
'например: --max-model-len=8192(параметр и значение разделены знаком = или пробелом',
'models.form.backend_parameters.sglang.placeholder':
'например: --context-length=8192(параметр и значение разделены знаком =)',
'например: --context-length=8192(параметр и значение разделены знаком = или пробелом',
'models.form.backend_parameters.vllm.tips':
'For more details about {backend} parameters, see <a href={link} target="_blank">here</a>.',
'Для получения подробной информации о параметрах {backend} см. <a href={link} target="_blank">здесь</a>.',
'models.logs.pagination.prev': 'Предыдущие {lines} строк',
'models.logs.pagination.next': 'Следующие {lines} строк',
'models.logs.pagination.last': 'Последняя страница',
@@ -89,11 +91,11 @@ export default {
'models.form.backend.llamabox':
'Для моделей формата GGUF. Поддержка Linux, macOS и Windows.',
'models.form.backend.vllm':
'Built-in support for NVIDIA, AMD, Ascend, Hygon, Iluvatar, and MetaX devices.',
'models.form.backend.voxbox': 'Only supports NVIDIA GPUs and CPUs.',
'models.form.backend.mindie': 'Only supports Ascend NPUs.',
'Встроенная поддержка устройств NVIDIA, AMD, Ascend, Hygon, Iluvatar и MetaX.',
'models.form.backend.voxbox': 'Поддерживает только GPU NVIDIA и CPU.',
'models.form.backend.mindie': 'Поддерживает только Ascend NPU.',
'models.form.backend.sglang':
'Built-in support for NVIDIA/AMD GPUs and Ascend NPUs.',
'Встроенная поддержка GPU NVIDIA/AMD и Ascend NPU.',
'models.form.search.gguftips':
'Для воркеров на macOS/Windows отметьте GGUF (для аудиомоделей снимите).',
'models.form.button.addlabel': 'Добавить метку',
@@ -120,9 +122,9 @@ export default {
'models.form.moreparameters': 'Описание параметров',
'models.table.vram.allocated': 'Выделенная VRAM',
'models.form.backend.warning':
'The selected backend does not support GGUF models. Please add a backend with GGUF support in the Inference Backend.',
'Выбранный бэкенд не поддерживает модели GGUF. Пожалуйста, добавьте бэкенд с поддержкой GGUF в разделе Бэкенды вывода.',
'models.form.backend.warning.gguf':
'Please ensure that the selected custom backend supports GGUF models.',
'Пожалуйста, убедитесь, что выбранный пользовательский бэкенд поддерживает модели GGUF.',
'models.form.ollama.warning':
'Чтобы развернуть бэкенд для моделей Ollama с использованием llama-box , выполните следующие шаги.',
'models.form.backend.warning.llamabox':
@@ -147,7 +149,8 @@ export default {
'Изменения вступят в силу только после удаления и повторного создания инстанса.',
'models.table.download.progress': 'Прогресс',
'models.table.button.apiAccessInfo': 'Доступ к API',
'models.table.button.apiAccessInfo.tips': `Для интеграции этой модели со сторонними приложениями используйте следующие данные: URL доступа, имя модели и ключ API. Эти учетные данные необходимы для обеспечения правильного подключения и использования сервиса модели.`, // Translated
'models.table.button.apiAccessInfo.tips':
'Для интеграции этой модели со сторонними приложениями используйте следующие данные: URL доступа, имя модели и ключ API. Эти учетные данные необходимы для обеспечения правильного подключения и использования сервиса модели.',
'models.table.apiAccessInfo.endpoint': 'URL доступа',
'models.table.apiAccessInfo.modelName': 'Имя модели',
'models.table.apiAccessInfo.apikey': 'Ключ API',
@@ -165,7 +168,8 @@ export default {
'<span class="bold-text">После обновления до версии (v0.7.0),</span> все ранее развёрнутые модели продолжат работать в обычном режиме.',
'models.ollama.deprecated.issue':
'См. связанную проблему: <a href="https://github.com/gpustack/gpustack/issues/1979" target="_blank">#1979 on GitHub</a>.',
'models.ollama.deprecated.notice': `Источник моделей Ollama объявлен устаревшим начиная с версии v0.6.1. Подробности см. в <a href="https://github.com/gpustack/gpustack/issues/1979" target="_blank">соответствующем issue на GitHub</a>.`,
'models.ollama.deprecated.notice':
'Источник моделей Ollama объявлен устаревшим начиная с версии v0.6.1. Подробности см. в <a href="https://github.com/gpustack/gpustack/issues/1979" target="_blank">соответствующем issue на GitHub</a>.',
'models.backend.mindie.310p':
'Ascend 310P поддерживает только FP16, поэтому необходимо установить --dtype=float16.',
'models.form.gpuCount': 'GPU на реплику',
@@ -181,9 +185,9 @@ export default {
'models.table.accessScope.all': 'Все пользователи',
'models.table.userSelection': 'Выбор пользователей',
'models.button.accessSettings.tips':
'Changes to access settings take effect after one minute.',
'Изменения в настройках доступа вступают в силу через одну минуту.',
'models.table.userSelection.tips':
'Admin users can access all models by default.',
'Администраторы по умолчанию имеют доступ ко всем моделям.',
'models.table.filterByName': 'Фильтр по имени пользователя',
'models.table.admin': 'Администратор',
'models.table.noselected': 'Пользователи не выбраны',
@@ -197,7 +201,7 @@ export default {
'models.form.maxCPUSize': 'Максимальный размер CPU кэша (ГиБ)',
'models.form.remoteURL': 'URL удаленного хранилища',
'models.form.remoteURL.tips':
'Refer to the <a href="https://docs.lmcache.ai/api_reference/configurations.html" target="_blank">configuration documentation</a> for details.',
'Подробности см. в <a href="https://docs.lmcache.ai/api_reference/configurations.html" target="_blank">документации по конфигурации</a>.',
'models.form.runCommandPlaceholder':
'напр., vllm serve Qwen/Qwen2.5-1.5B-Instruct',
'models.accessSettings.public': 'Публичный',
@@ -212,12 +216,12 @@ export default {
'models.form.gpusAllocationType.auto': 'Авто',
'models.form.gpusAllocationType.custom': 'Вручную',
'models.form.gpusAllocationType.auto.tips':
'The system automatically calculates the GPU count per replica, using powers of two by default and capped by the selected GPUs.',
'Система автоматически вычисляет количество GPU на реплику, по умолчанию используя степени двойки и ограничиваясь выбранными GPU.',
'models.form.gpusAllocationType.custom.tips':
'Вы можете указать точное количество GPU на реплику.',
'models.mymodels.status.inactive': 'Остановлен',
'models.mymodels.status.degrade': 'Аномальный',
'models.mymodels.status.active': 'Активен',
'models.mymodels.status.degrade': 'Не готов',
'models.mymodels.status.active': 'Готов',
'models.form.kvCache.tips':
'Расширенный KV-кэш и спекулятивное декодирование доступны только со встроенными бэкендами (vLLM / SGLang). Переключите бэкенд в <span class="bold-text">Расширенных</span> настройках, чтобы включить их.',
'models.form.kvCache.tips2':
@@ -252,31 +256,36 @@ export default {
'models.form.backend.custom': 'Пользовательский',
'models.form.rules.name':
'До 63 символов; только буквы, цифры, точки (.), подчёркивания (_) и дефисы (-); должно начинаться и заканчиваться буквенно-цифровым символом.',
'models.catalog.button.explore': 'Explore More Models',
'models.catalog.button.explore': 'Исследовать больше моделей',
'models.catalog.precision': 'Точность',
'models.form.gpuPerReplica.tips': 'Введите произвольное число',
'models.form.generic_proxy': 'Включить универсальный прокси',
'models.form.generic_proxy.tips':
'After enabling the generic proxy, you can access URI paths that do not follow the OpenAI API standard.',
'После включения универсального прокси вы можете получать доступ к URI-путям, которые не следуют стандарту OpenAI API.',
'models.form.generic_proxy.button': 'Универсальный прокси',
'models.accessControlModal.includeusers': 'Включить пользователей',
'models.table.genericProxy':
'Use the following path prefix, and set the model name in either the <span class="bold-text">X-GPUStack-Model</span> request header or the model field in the request body. All requests under this path prefix will be forwarded to the inference backend.'
'Используйте следующий префикс пути и укажите имя модели либо в заголовке запроса <span class="bold-text">X-GPUStack-Model</span>, либо в поле model в теле запроса. Все запросы с этим префиксом пути будут перенаправлены в бэкенд вывода.',
'models.form.backendVersion.deprecated': 'Устаревший',
'models.accessSettings.public.desc':
'Accessible to anyone without authentication.',
'models.accessSettings.authed.tips':
'Accessible to all authenticated platform users.',
'models.accessSettings.allowedUsers.tips':
'Only designated users can access the model.',
'models.form.backendVersions.tips': `To use more versions, go to the {link} page and edit the backend to add versions.`,
'models.catalog.nogpus.tips':
'No compatible GPUs are available in the selected cluster for this model.',
'models.form.modelfile.notfound': `The model file path you specified does not exist on the GPUStack server. It's recommended to place the model file at the same path on both the GPUStack server and GPUStack workers. This helps GPUStack make better decisions.`,
'models.form.readyWorkers': 'workers ready'
};
// ========== To-Do: Translate Keys (Remove After Translation) ==========
// 1. 'models.catalog.button.explore': 'Explore More Models',
// 2. 'models.table.genericProxy': 'Use the following path prefix, and set the model name in either the <span class="bold-text">X-GPUStack-Model</span> request header or the model field in the request body. All requests under this path prefix will be forwarded to the inference backend.',
// 3. 'models.form.backend.vllm': 'Built-in support for NVIDIA, AMD, Ascend, Hygon, Iluvatar, and MetaX devices.',
// 4. 'models.form.backend.voxbox': 'Only supports NVIDIA GPUs and CPUs.',
// 5. 'models.form.backend.mindie': 'Only supports Ascend NPUs.',
// 6. 'models.form.backend.sglang': 'Built-in support for NVIDIA/AMD GPUs and Ascend NPUs.',
// 7. 'models.form.gpusAllocationType.auto.tips': 'The system automatically calculates the GPU count per replica, using powers of two by default and capped by the selected GPUs.',
// 8. 'models.form.backend_parameters.vllm.tips': 'For more details about {backend} parameters, see <a href={link} target="_blank">here</a>.',
// 9. 'models.button.accessSettings.tips': 'Changes to access settings take effect after one minute.',
// 10. 'models.table.userSelection.tips': 'Admin users can access all models by default.',
// 11. 'models.form.generic_proxy.tips': 'After enabling the generic proxy, you can access URI paths that do not follow the OpenAI API standard.',
// 12. 'models.form.partialoffload.tips': `When CPU offloading is enabled, GPUStack will allocate CPU memory if GPU resources are insufficient. You must correctly configure the inference backend to use hybrid CPU+GPU or full CPU inference.`,
// 13. 'models.form.backend.warning': 'The selected backend does not support GGUF models. Please add a backend with GGUF support in the Inference Backend.',
// 14. 'models.form.backend.warning.gguf': 'Please ensure that the selected custom backend supports GGUF models.',
// 1. 'models.accessSettings.public.desc': 'Accessible to anyone without authentication.',
// 2. 'models.accessSettings.authed.tips': 'Accessible to all authenticated platform users.',
// 3. 'models.accessSettings.allowedUsers.tips': 'Only designated users can access the model.',
// 4. 'models.form.backendVersions.tips': `To use more versions, go to the {link} page and edit the backend to add versions.`,
// 5. 'models.catalog.nogpus.tips': 'No compatible GPUs are available in the selected cluster for this model.',
// 6. 'models.form.modelfile.notfound': `The model file path you specified does not exist on the GPUStack server. It's recommended to place the model file at the same path on both the GPUStack server and GPUStack workers. This helps GPUStack make better decisions.`,
// 7. 'models.form.readyWorkers': 'workers ready'
// ========== End of To-Do List ==========
+18 -3
View File
@@ -1,10 +1,12 @@
export default {
'noresult.button.add': 'Добавить сейчас',
'noresult.mymodels.title': 'Нет доступных моделей',
'noresult.mymodels.subTitle': 'Обратитесь к администратору для получения доступа.',
'noresult.mymodels.subTitle':
'Обратитесь к администратору для получения доступа.',
'noresult.mymodels.nofound': 'Подходящие модели не найдены',
'noresult.deployments.title': 'Нет развернутых моделей',
'noresult.deployments.subTitle': 'Вы еще не развернули ни одной модели. Нажмите кнопку ниже, чтобы начать.',
'noresult.deployments.subTitle':
'Вы еще не развернули ни одной модели. Нажмите кнопку ниже, чтобы начать.',
'noresult.gpus.title': 'GPU устройства не обнаружены',
'noresult.gpus.subTitle': 'Проверьте, что статус Worker READY.',
'noresult.gpus.nofound': 'Подходящие GPU устройства не найдены.',
@@ -32,5 +34,18 @@ export default {
'noresult.keys.nofound': 'Подходящие API-ключи не найдены.',
'noresult.catalog.title': 'Нет моделей',
'noresult.catalog.subTitle': 'Модели еще не настроены.',
'noresult.catalog.nofound': 'Подходящие модели не найдены.'
'noresult.catalog.nofound': 'Подходящие модели не найдены.',
'noresult.resources.cluster':
'No clusters available. Add a cluster to get started.',
'noresult.resources.worker':
'No workers available. Add a worker to get started.',
'noresult.resources.gotocluster': 'Create Your First Cluster',
'noresult.resources.gotoworker': 'Add Worker'
};
// ========== To-Do: Translate Keys (Remove After Translation) ==========
// 1. 'noresult.resources.cluster': 'No clusters available. Add a cluster to get started.',
// 2. 'noresult.resources.worker': 'No workers available. Add a worker to get started.',
// 3. 'noresult.resources.gotocluster': 'Create Your First Cluster',
// 4. 'noresult.resources.gotoworker': 'Add Worker'
// ========================================================================
+11 -3
View File
@@ -150,10 +150,18 @@ export default {
'playground.model.noavailable.tips2':
'Если нужная модель не отображается, убедитесь, что она запущена и ей присвоена правильная категория. Если категория указана неверно, её можно изменить вручную в настройках модели.',
'playground.rerank.query.validate': 'Необходимо указать запрос.',
'playground.image.generate.error': 'Произошла ошибка. Не удалось сгенерировать изображение.',
'playground.uploadfile.sizeError': 'Размер файла превышает ограничение. Максимальный размер: {size}.'
'playground.image.generate.error':
'Произошла ошибка. Не удалось сгенерировать изображение.',
'playground.uploadfile.sizeError':
'Размер файла превышает ограничение. Максимальный размер: {size}.',
'playground.uploadImage.url.invalid':
'Please enter a direct image URL (e.g. https://.../image.png). Press ESC to cancel.',
'playground.uploadImage.url.holder': 'Enter an image URL',
'playground.uploadImage.url.button': 'Add Image from URL'
};
// ========== To-Do: Translate Keys (Remove After Translation) ==========
// 1. 'playground.uploadImage.url.invalid': 'Please enter a direct image URL(e.g. https://.../image.png). Press ESC to cancel.',
// 2. 'playground.uploadImage.url.holder': 'Enter an image URL',
// 3. 'playground.uploadImage.url.button': 'Add Image from URL'
// ========== End of To-Do List ==========
+6 -4
View File
@@ -20,12 +20,12 @@ export default {
'users.form.currentpassword': 'Текущий пароль',
'users.form.updatepassword': 'Изменить пароль',
'users.form.rule.password':
'Должен содержать заглавные и строчные буквы, цифры и спецсимволы (!@#$%^&*_+), длина 6-12 символов, без пробелов.',
'Должен содержать заглавные и строчные буквы, цифры и спецсимволы (!@#$%^&*_+), длина 6-64 символов, без пробелов.',
'users.password.uppcase': 'Минимум одна заглавная буква',
'users.password.lowercase': 'Минимум одна строчная буква',
'users.password.number': 'Минимум одна цифра',
'users.password.special': 'Минимум один спецсимвол',
'users.password.length': 'Длина от 6 до 12 символов',
'users.password.length': 'Длина от 6 до 64 символов',
'users.password.modify.title': 'Смена пароля',
'users.password.modify.description':
'В целях безопасности измените первоначальный пароль.',
@@ -38,9 +38,11 @@ export default {
'users.settings.title': 'Настройки пользователя',
'users.status.activate': 'Активировать аккаунт',
'users.status.deactivate': 'Деактивировать аккаунт',
'users.status.inactiveAccount': 'Неактивный аккаунт'
'users.status.inactiveAccount': 'Неактивный аккаунт',
'users.login.getInitialPassword':
'Run the following command on your server to retrieve the initial admin password.'
};
// ========== To-Do: Translate Keys (Remove After Translation) ==========
// 1. 'users.login.getInitialPassword': 'Run the following command on your server to retrieve the initial admin password.'
// ========== End of To-Do List ==========
+6 -1
View File
@@ -30,5 +30,10 @@ export default {
'backend.version.rules.builtin': '须以 "-custom" 结尾',
'backend.version.no.tips': '内置后端自定义版本名称必须以 "-custom" 结尾。',
'backend.backend.rules.custom': '自定义后端名称须以 "-custom" 结尾。',
'backend.quickConfig': '快速配置'
'backend.quickConfig': '快速配置',
'backend.version.default.not.exists': '默认版本不存在于 {versions}。',
'backend.replaceEntrypoint': '覆盖镜像入口命令',
'backend.entrypoint': '镜像入口命令',
'backend.entrypoint.tips':
'如指定,镜像中定义的 ENTRYPOINT 将被忽略,下面的命令将作为容器启动入口命令使用。'
};
+41 -8
View File
@@ -48,15 +48,15 @@ export default {
'clusters.addworker.nvidiaNotes-01':
'如果节点有多个出站 IP 地址,请填写 <span class="bold-text">WORKER_IP</span>,以确保使用指定的 IP。可通过命令 <span class="bold-text">hostname -I | xargs -n1</span> 进行确认。',
'clusters.addworker.nvidiaNotes-02':
'如果模型目录已存在于节点上,请添加 <span class="bold-text">--volume</span> 参数进行挂载。',
'如果节点上已经存在模型目录,你可以指定该路径进行挂载。',
'clusters.addworker.hygonNotes':
'如果 <span class="bold-text">/opt/hyhal</span> 目录不存在,请创建指向 Hygon 安装路径的符号链接:<span class="bold-text">/opt/hyhal</span>。与 <span class="bold-text">/opt/dtk</span> 目录相同。',
'如果 <span class="bold-text">/opt/hyhal</span> 目录不存在,请创建指向海光安装路径的符号链接:<span class="bold-text">/opt/hyhal</span>。与 <span class="bold-text">/opt/dtk</span> 目录相同。',
'clusters.addworker.corexNotes':
'如果 <span class="bold-text">/lib/modules</span> 目录不存在,请创建指向 Iluvatar 安装路径的符号链接:<span class="bold-text">ln -s /path/to/corex /lib/modules</span>。与 <span class="bold-text">/usr/local/corex</span> 目录相同。',
'如果 <span class="bold-text">/lib/modules</span> 目录不存在,请创建指向天数智芯安装路径的符号链接:<span class="bold-text">ln -s /path/to/corex /lib/modules</span>。与 <span class="bold-text">/usr/local/corex</span> 目录相同。',
'clusters.addworker.metaxNotes':
'如果 <span class="bold-text">/opt/mxdriver</span> 目录不存在,请创建指向 MetaX 安装路径的符号链接:<span class="bold-text">ln -s /path/to/metax /opt/mxdriver</span>。与 <span class="bold-text">/opt/maca</span> 目录相同。',
'如果 <span class="bold-text">/opt/mxdriver</span> 目录不存在,请创建指向沐曦安装路径的符号链接:<span class="bold-text">ln -s /path/to/metax /opt/mxdriver</span>。与 <span class="bold-text">/opt/maca</span> 目录相同。',
'clusters.addworker.cambriconNotes':
'如果 <span class="bold-text">/usr/local/neuware</span> 目录不存在,请创建指向 Cambricon 安装路径的符号链接:<span class="bold-text">ln -s /path/to/neuware /usr/local/neuware</span>。',
'如果 <span class="bold-text">/usr/local/neuware</span> 目录不存在,请创建指向寒武纪安装路径的符号链接:<span class="bold-text">ln -s /path/to/neuware /usr/local/neuware</span>。',
'clusters.addworker.hygonNotes-02':
'如果未能检测到设备,请尝试移除 <span class="bold-text">--env ROCM_SMI_LIB_PATH=/opt/hyhal/lib</span>。',
'clusters.addworker.selectCluster': '选择集群',
@@ -66,17 +66,50 @@ export default {
'clusters.addworker.checkEnv': '检查环境',
'clusters.addworker.specifyArgs': '指定参数',
'clusters.addworker.runCommand': '运行指令',
'clusters.addworker.specifyWorkerIP': '指定 Worker IP',
'clusters.addworker.detectWorkerIP': '自动检测 Worker IP',
'clusters.addworker.specifyWorkerIP': '节点 IP',
'clusters.addworker.detectWorkerIP': '自动检测节点 IP',
'clusters.addworker.specifyWorkerAddress': '节点外部地址',
'clusters.addworker.detectWorkerAddress': '节点外部地址',
'clusters.addworker.detectWorkerAddress.tips':
'如果未指定,则默认为节点 IP。',
'clusters.addworker.externalIP.tips':
'如运行在 VPC 或私有网络时,请指定 GPUStack Server 可达的节点外部地址。',
'clusters.addworker.enterWorkerIP': '输入节点 IP',
'clusters.addworker.enterWorkerIP.error': '请输入节点 IP',
'clusters.addworker.enterWorkerAddress': '输入节点外部地址',
'clusters.addworker.enterWorkerAddress.error': '请输入节点外部地址',
'clusters.addworker.extraVolume': '额外卷挂载',
'clusters.addworker.cacheVolume': '缓存卷挂载',
'clusters.addworker.cacheVolume.tips':
'如果要自定义模型缓存目录,可以指定路径进行挂载。',
'clusters.addworker.configSummary': '配置摘要',
'clusters.addworker.gpuVendor': 'GPU 厂商',
'clusters.addworker.workerIP': '节点 IP',
'clusters.addworker.workerExternalIP': '节点外部地址',
'clusters.addworker.notSpecified': '未指定',
'clusters.addworker.autoDetect': '自动检测',
'clusters.addworker.extraVolume.holder':
'例如:/data/models(路径需以 / 开头)',
'clusters.addworker.vendorNotes.title': '{vendor}设备注意事项'
'clusters.addworker.cacheVolume.holder':
'例如:/data/cache(路径需以 / 开头)',
'clusters.addworker.vendorNotes.title': '{vendor}设备注意事项',
'clusters.button.genToken':
'需要创建令牌?点击<a href="{link}" target="_blank">这里</a>。',
'clusters.addworker.amdNotes-01':
'如果 <span class="bold-text">/opt/rocm</span> 目录不存在,请创建一个指向已安装 ROCm 路径的符号链接:<span class="bold-text">ln -s /path/to/rocm /opt/rocm</span>。',
'clusters.addworker.message.success_single':
'已将 {count} 个新节点添加到集群中。',
'clusters.addworker.message.success_multiple':
'已将 {count} 个新节点添加到集群中。',
'clusters.create.serverUrl': '服务器地址',
'clusters.create.workerConfig': '节点配置',
'clusters.addworker.containerName': '节点容器名称',
'clusters.addworker.containerName.tips': '为节点容器指定一个名称。',
'clusters.addworker.dataVolume': 'GPUStack 数据卷',
'clusters.addworker.dataVolume.tips': '为 GPUStack 指定数据存储路径。',
'clusters.table.ip.internal': '内',
'clusters.table.ip.external': '外',
'clusters.form.serverUrl.tips': '指定可从您的云服务提供商访问的服务器地址。',
'clusters.form.setDefault': '设为默认',
'clusters.form.setDefault.tips': '部署时的默认集群。'
};
+6 -2
View File
@@ -241,7 +241,7 @@ export default {
'common.button.forgotpassword': '忘记密码?',
'common.appearance.theme': '主题',
'common.page.wentwrong': '哎呀,出了点问题',
'common.page.refresh.tips': '出了点问题,试试刷新页面吧!',
'common.page.refresh.tips': '页面似乎需要更新,刷新一下试试吧!',
'common.tips.escape.disable': '请点击「取消」按钮或右上角 X 关闭窗口',
'common.button.clearSelection': '清除选择',
'common.select.count': '已选 {count} 项',
@@ -257,5 +257,9 @@ export default {
'common.filter.status': '按状态筛选',
'common.form.rule.selectInput': '请选择或输入{name}',
'common.tag.experimental': '实验性',
'common.title.example': '示例'
'common.title.example': '示例',
'common.button.dontshowagain': '不再提示',
'common.sorter.tips.ascend': '点击升序',
'common.sorter.tips.descend': '点击降序',
'common.sorter.tips.cancel': '取消排序'
};
+13 -4
View File
@@ -68,11 +68,11 @@ export default {
'models.form.ollamalink':
'在 <a href="https://www.ollama.com/library" target="_blank">Ollama Library</a> 中查找',
'models.form.backend_parameters.llamabox.placeholder':
'例如,--ctx-size=8192(参数名和值用 = 号分隔)',
'例如,--ctx-size=8192(参数名和值用 = 号或空格分隔)',
'models.form.backend_parameters.vllm.placeholder':
'例如,--max-model-len=8192(参数名和值用 = 号分隔)',
'例如,--max-model-len=8192(参数名和值用 = 号或空格分隔)',
'models.form.backend_parameters.sglang.placeholder':
'例如,--context-length=8192(参数名和值用 = 号分隔)',
'例如,--context-length=8192(参数名和值用 = 号或空格分隔)',
'models.form.backend_parameters.vllm.tips':
'更多 {backend} 参数说明查看<a href={link} target="_blank">这里</a>。',
'models.logs.pagination.prev': '上一 {lines} 行',
@@ -247,5 +247,14 @@ export default {
'models.form.generic_proxy.button': '通用代理',
'models.accessControlModal.includeusers': '显示用户',
'models.table.genericProxy':
'使用以下路径前缀,并在请求头 <span class="bold-text">X-GPUStack-Model</span> 或请求体中的 model 字段设置模型名称后访问该模型。该路径的所有子路径请求会被转发到推理后端。'
'使用以下路径前缀,并在请求头 <span class="bold-text">X-GPUStack-Model</span> 或请求体中的 model 字段设置模型名称后访问该模型。该路径的所有子路径请求会被转发到推理后端。',
'models.form.backendVersion.deprecated': '已弃用',
'models.accessSettings.public.desc': '任何人无需认证即可访问。',
'models.accessSettings.authed.tips': '平台内所有已认证用户可访问。',
'models.accessSettings.allowedUsers.tips': '仅允许选定的特定用户访问。',
'models.form.backendVersions.tips': `如需使用更多版本,请前往{link}页面并编辑对应的后端以添加版本。`,
'models.catalog.nogpus.tips': '所选集群中没有兼容该模型的 GPU。',
'models.form.modelfile.notfound':
'你指定的模型文件路径在 GPUStack 服务器上不存在。建议在 GPUStack 服务器和 GPUStack 节点上使用相同的模型文件路径,这有助于 GPUStack 做出更优的调度与决策。',
'models.form.readyWorkers': '节点就绪'
};
+6 -2
View File
@@ -12,7 +12,7 @@ export default {
'noresult.workers.title': '暂无节点',
'noresult.workers.subTitle': '当前集群中没有任何节点',
'noresult.workers.nofound': '未找到匹配的节点',
'noresult.workers.button.add': '前往添加节点',
'noresult.workers.button.add': '添加节点',
'noresult.modelfiles.title': '暂无模型文件',
'noresult.modelfiles.subTitle': '尚未添加任何模型文件。',
'noresult.modelfiles.nofound': '未找到匹配的模型文件',
@@ -33,5 +33,9 @@ export default {
'noresult.keys.nofound': '未找到匹配的 API 密钥',
'noresult.catalog.title': '暂无模型',
'noresult.catalog.subTitle': '尚未配置任何模型。',
'noresult.catalog.nofound': '未找到匹配的模型'
'noresult.catalog.nofound': '未找到匹配的模型',
'noresult.resources.cluster': '暂无可用集群,请添加集群以开始使用。',
'noresult.resources.worker': '暂无可用节点,请添加节点以开始使用。',
'noresult.resources.gotocluster': '创建您的第一个集群',
'noresult.resources.gotoworker': '添加节点'
};
+6 -1
View File
@@ -148,5 +148,10 @@ export default {
'若预期的模型未显示,请检查模型是否已正常运行并被正确分类。如分类不正确,请编辑模型并手动调整其类别。',
'playground.rerank.query.validate': '查询内容不能为空',
'playground.image.generate.error': '出了一点问题,图片未能生成。',
'playground.uploadfile.sizeError': '上传的文件大小超过限制,最大允许 {size}。'
'playground.uploadfile.sizeError':
'上传的文件大小超过限制,最大允许 {size}。',
'playground.uploadImage.url.invalid':
'请输入直接的图片链接(例如:https://…/image.png)。按 ESC 可取消。',
'playground.uploadImage.url.holder': '请输入图片链接',
'playground.uploadImage.url.button': '从链接添加图片'
};
+5 -3
View File
@@ -19,12 +19,12 @@ export default {
'users.form.currentpassword': '当前密码',
'users.form.updatepassword': '修改密码',
'users.form.rule.password':
'包含大小写字母、数字和特殊字符(!@#$%^&*_+),6至12个字符,不允许有空格',
'包含大小写字母、数字和特殊字符(!@#$%^&*_+),6至64个字符,不允许有空格',
'users.password.uppcase': '至少包含一个大写字母',
'users.password.lowercase': '至少包含一个小写字母',
'users.password.number': '至少包含一个数字',
'users.password.special': '至少包含一个特殊字符',
'users.password.length': '长度在6至12个字符之间',
'users.password.length': '长度在6至64个字符之间',
'users.password.modify.title': '修改密码',
'users.password.modify.description': '为了确保您的账户安全,请修改初始密码',
'users.password.confirm': '确认新密码',
@@ -36,5 +36,7 @@ export default {
'users.settings.title': '用户设置',
'users.status.activate': '启用账户',
'users.status.deactivate': '停用账户',
'users.status.inactiveAccount': '停用账户'
'users.status.inactiveAccount': '停用账户',
'users.login.getInitialPassword':
'在服务器端运行以下命令以获取初始管理员密码。'
};
+32 -40
View File
@@ -1,8 +1,28 @@
import useOverlayScroller from '@/hooks/use-overlay-scroller';
import React, { useCallback } from 'react';
import './style.less';
import styled from 'styled-components';
import { WrapperContext } from './use-wrapper-context';
const Wrapper = styled.div`
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
position: relative;
width: 100%;
`;
const ContentWrapper = styled.div`
flex: 1;
position: relative;
overflow-y: auto;
`;
const Footer = styled.div`
padding-block: 0;
background-color: var(--ant-color-bg-elevated);
`;
interface ColumnWrapperProps {
children: React.ReactNode;
footer?: React.ReactNode;
@@ -44,29 +64,8 @@ const ColumnWrapper: React.FC<ColumnWrapperProps> = ({
}
}, []);
React.useLayoutEffect(() => {
if (!scroller.current || !footerRef.current || !contentRef.current) return;
const updatePadding = () => {
const height = footerRef.current!.getBoundingClientRect().height;
contentRef.current!.style.paddingBottom = `${height - 22}px`;
};
updatePadding();
const observer = new ResizeObserver(updatePadding);
observer.observe(footerRef.current);
return () => observer.disconnect();
}, []);
const setContentPaddingBottom = useCallback((padding: number) => {
if (footerRef.current) {
const height = footerRef.current.getBoundingClientRect().height;
contentRef.current!.style.paddingBottom = `${height + padding - 22}px`;
} else {
contentRef.current!.style.paddingBottom = `${padding}px`;
}
contentRef.current!.style.paddingBottom = `${padding}px`;
}, []);
return (
@@ -81,25 +80,18 @@ const ColumnWrapper: React.FC<ColumnWrapperProps> = ({
setSScrollContentPaddingBottom: setContentPaddingBottom
}}
>
<div
className="column-wrapper-footer"
style={{ height: maxHeight || '100%', ...styles.wrapper }}
>
<div
className="column-wrapper"
<Wrapper style={{ height: maxHeight || '100%', ...styles.wrapper }}>
<ContentWrapper
ref={scroller}
style={{ padding: '16px 24px', ...styles.container }}
style={{
padding: '16px 24px',
...styles.container
}}
>
<div ref={contentRef} className="child-content">
{children}
</div>
</div>
{footer && (
<div className="footer" ref={footerRef}>
{footer}
</div>
)}
</div>
<div ref={contentRef}>{children}</div>
</ContentWrapper>
{footer && <Footer ref={footerRef}>{footer}</Footer>}
</Wrapper>
</WrapperContext.Provider>
);
};
@@ -1,32 +0,0 @@
.column-wrapper {
flex: 1;
position: relative;
height: 100%;
overflow-y: auto;
}
.simplebar-scrollbar::before {
width: var(--scrollbar-size);
background: var(--scrollbar-handle-bg);
}
.column-wrapper-footer {
flex: 1;
display: flex;
position: relative;
width: 100%;
.column-wrapper {
border-left: none;
}
.footer {
position: absolute;
bottom: 0;
left: 0;
right: 0;
z-index: 100;
padding-block: 0;
background-color: var(--ant-color-bg-elevated);
}
}
@@ -1,6 +1,6 @@
import { DoubleRightOutlined } from '@ant-design/icons';
import { useIntl } from '@umijs/max';
import { Button } from 'antd';
import { Button, FloatButton } from 'antd';
import React from 'react';
import {
@@ -16,7 +16,7 @@ const InfiniteScroller: React.FC<
const { observerRef, throttledLoadMore } = useInfiniteScroll(restProps);
return (
<div>
<div className="relative" style={{ width: '100%' }}>
{children}
<div
ref={observerRef}
@@ -45,6 +45,7 @@ const InfiniteScroller: React.FC<
</div>
)}
</div>
<FloatButton.BackTop visibilityHeight={1000} />
</div>
);
};
@@ -1,7 +1,7 @@
import { createContext, useContext } from 'react';
interface ScrollerContextProps {
total: number;
total: number; // total pages
current: number;
loading: boolean;
refresh: (nextPage: number) => void;
+22 -3
View File
@@ -1,4 +1,4 @@
import { Empty, EmptyProps, Typography } from 'antd';
import { Button, Empty, EmptyProps, Typography } from 'antd';
import _ from 'lodash';
import React, { useMemo } from 'react';
import styled from 'styled-components';
@@ -54,9 +54,19 @@ const NoResult: React.FC<
loading?: boolean;
loadend?: boolean;
dataSource?: any[];
buttonText?: React.ReactNode;
onClick?: () => void;
}
> = (props) => {
const { filters, noFoundText, loadend, loading, dataSource } = props;
const {
filters,
noFoundText,
loadend,
loading,
dataSource,
buttonText,
onClick
} = props;
const hasFilters = useMemo(() => {
const filterValues = _.omit(filters, ['page', 'perPage']);
@@ -70,6 +80,15 @@ const NoResult: React.FC<
});
}, [filters]);
const renderChildren = () => {
if (!buttonText || !onClick) return null;
return (
<Button color="primary" variant="filled" onClick={onClick}>
{buttonText}
</Button>
);
};
return (
<>
{!loading && loadend && !dataSource?.length ? (
@@ -96,7 +115,7 @@ const NoResult: React.FC<
</Description>
}
>
{!hasFilters && props.children}
{!hasFilters && renderChildren()}
</StyledEmpty>
) : (
<span></span>
@@ -1,7 +1,6 @@
.ant-pro-footer-bar {
padding-inline: 0;
border-block-start: 1px solid rgba(5, 5, 5, 6%);
// background-color: #f5f5f5;
border-radius: 4px 4px 0 0;
.ant-pro-footer-bar-left {
@@ -11,30 +10,4 @@
.ant-pro-footer-bar-right {
flex: 1;
}
.ant-tabs-nav {
.ant-tabs-tab {
border: none;
background-color: transparent;
.ant-tabs-tab-remove {
display: none;
}
&:hover {
.ant-tabs-tab-remove {
display: inline-block;
}
}
}
.ant-tabs-tab:not(.ant-tabs-tab-active) {
color: var(--ant-color-text-secondary);
}
.ant-tabs-tab-active {
background-color: var(--ant-color-bg-elevated);
color: var(--ant-color-text);
}
}
}
@@ -3,7 +3,6 @@ import TerminalTabs from './tabs';
export interface TerminalModalProps {
terminals: { url: string; name: string }[];
height: number;
open: boolean;
onClose: () => void;
currentActive?: string;
@@ -11,19 +10,16 @@ export interface TerminalModalProps {
const TerminalModal: React.FC<TerminalModalProps> = ({
terminals,
height,
open,
onClose,
currentActive
}) => {
return (
<div>
<TerminalTabs
terminals={terminals}
height={height}
currentActive={currentActive}
/>
</div>
<TerminalTabs
terminals={terminals}
currentActive={currentActive}
onClose={onClose}
/>
);
};

Some files were not shown because too many files have changed in this diff Show More