Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
66e8f83395 | ||
|
|
41ebceca8f | ||
|
|
632685672c | ||
|
|
bd2e4bb633 | ||
|
|
835633d44b | ||
|
|
75773e6b00 | ||
|
|
a21b9b5244 | ||
|
|
38bf76dc14 | ||
|
|
9461341104 | ||
|
|
5b5b2c6364 | ||
|
|
a006263e51 | ||
|
|
febdd38f7c | ||
|
|
1b4d335b09 | ||
|
|
725409a576 | ||
|
|
9b25cc51e4 | ||
|
|
9dcdb6b6e7 | ||
|
|
73b987358f | ||
|
|
84c2bf130c | ||
|
|
b25189feea | ||
|
|
a9b58a5564 | ||
|
|
530b20c633 | ||
|
|
fa0aeac10b | ||
|
|
1b661bb8cd | ||
|
|
530e8f7537 | ||
|
|
9e019a98f6 | ||
|
|
10c6cf9f7e | ||
|
|
480b238e44 | ||
|
|
32bdbc693a | ||
|
|
ab146b6b6b | ||
|
|
763929eafc | ||
|
|
711b3fee98 | ||
|
|
0d0592eee9 | ||
|
|
cc7948ba45 | ||
|
|
7e25b64689 | ||
|
|
d844cdadb5 | ||
|
|
e3b3f51e87 | ||
|
|
45efda7f1d | ||
|
|
db45c861e4 | ||
|
|
42f8e6a6d9 | ||
|
|
23a761226a | ||
|
|
6738dfe0d3 | ||
|
|
6e16b2eb0c | ||
|
|
707eb15465 | ||
|
|
8008099e71 | ||
|
|
79ccb96c95 | ||
|
|
bd76934e40 | ||
|
|
bb793a41fc | ||
|
|
ad048a92ed | ||
|
|
14d944ad2f | ||
|
|
e1b8b5696c | ||
|
|
78368e9979 | ||
|
|
e83bff6923 | ||
|
|
e6c12d81ea | ||
|
|
3a73d0c1b2 | ||
|
|
b524587a9a | ||
|
|
e4bd753355 | ||
|
|
30536cb04a | ||
|
|
da1efa9270 | ||
|
|
bd6f4b3f76 | ||
|
|
5e48d75ead | ||
|
|
bd46fcdc75 | ||
|
|
6968f9fa6b | ||
|
|
5225fd328d | ||
|
|
fa42474612 | ||
|
|
d2eda78af0 | ||
|
|
7a0ee6aac4 | ||
|
|
b327eedb45 | ||
|
|
a68e3ff9da | ||
|
|
0500dd2e35 | ||
|
|
cb43131ccb | ||
|
|
883e0ab115 | ||
|
|
b189160888 | ||
|
|
3dbb0de683 | ||
|
|
b38bc8a207 | ||
|
|
3328828da6 | ||
|
|
7fdabd90a5 | ||
|
|
d8452e98f5 | ||
|
|
dda2213593 | ||
|
|
04f995b80c | ||
|
|
a08f1e2512 | ||
|
|
af4849d6cc | ||
|
|
087675889f | ||
|
|
daa10617c0 | ||
|
|
c3b5c13cc5 | ||
|
|
1dd83645bd |
@@ -78,6 +78,9 @@ export default defineConfig({
|
|||||||
model: {},
|
model: {},
|
||||||
initialState: {},
|
initialState: {},
|
||||||
request: {},
|
request: {},
|
||||||
|
routePrefetch: {
|
||||||
|
defaultPrefetch: 'intent'
|
||||||
|
},
|
||||||
keepalive: keepAlive,
|
keepalive: keepAlive,
|
||||||
locale: {
|
locale: {
|
||||||
antd: true,
|
antd: true,
|
||||||
|
|||||||
@@ -344,3 +344,16 @@ textarea:hover {
|
|||||||
.text-success {
|
.text-success {
|
||||||
color: var(--ant-color-success);
|
color: var(--ant-color-success);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.desc-fill {
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--ant-color-text);
|
||||||
|
background-color: var(--ant-color-fill-content);
|
||||||
|
line-height: 1.5;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.line-6 {
|
||||||
|
line-height: 24px;
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,8 +3,7 @@
|
|||||||
.ant-layout-sider-children {
|
.ant-layout-sider-children {
|
||||||
border-inline: none;
|
border-inline: none;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
// padding-inline: 16px;
|
padding-block-end: 8px;
|
||||||
padding-block-end: 12px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.umi-plugin-layout-right {
|
.umi-plugin-layout-right {
|
||||||
|
|||||||
@@ -20,11 +20,33 @@ export const getRequestId = () => {
|
|||||||
return store.get(requestIdAtom);
|
return store.get(requestIdAtom);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// store for cluster list: res.items from api
|
||||||
export const clusterListAtom = atom<
|
export const clusterListAtom = atom<
|
||||||
{
|
{
|
||||||
label: string;
|
label: string;
|
||||||
value: number;
|
value: number;
|
||||||
|
provider: string;
|
||||||
|
state: string;
|
||||||
|
is_default: boolean;
|
||||||
|
workers: number;
|
||||||
|
ready_workers: number;
|
||||||
|
gpus: number;
|
||||||
|
}[]
|
||||||
|
>([]);
|
||||||
|
|
||||||
|
// store for worker list: res.items from api
|
||||||
|
export const workerListAtom = atom<
|
||||||
|
{
|
||||||
|
label: string;
|
||||||
|
value: number;
|
||||||
|
cluster_id: number;
|
||||||
|
state: string;
|
||||||
|
id: number;
|
||||||
|
labels: Record<string, any>;
|
||||||
|
name: string;
|
||||||
}[]
|
}[]
|
||||||
>([]);
|
>([]);
|
||||||
|
|
||||||
export const backendOptionsAtom = atom<BackendOption[]>([]);
|
export const backendOptionsAtom = atom<BackendOption[]>([]);
|
||||||
|
|
||||||
|
export const resourceOverviewAtom = atom<Record<string, any>>({});
|
||||||
|
|||||||
@@ -8,14 +8,16 @@ type UserSettings = {
|
|||||||
colorPrimary: string;
|
colorPrimary: string;
|
||||||
isDarkTheme: boolean;
|
isDarkTheme: boolean;
|
||||||
collapsed: boolean;
|
collapsed: boolean;
|
||||||
|
hideAddResourceModal?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const defaultSettings: UserSettings = {
|
export const defaultSettings: UserSettings = {
|
||||||
theme: 'light',
|
theme: 'light',
|
||||||
mode: 'auto',
|
mode: 'auto',
|
||||||
isDarkTheme: false,
|
isDarkTheme: false,
|
||||||
colorPrimary: COLOR_PRIMARY,
|
colorPrimary: COLOR_PRIMARY,
|
||||||
collapsed: false
|
collapsed: false,
|
||||||
|
hideAddResourceModal: false
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getStorageUserSettings = () => {
|
export const getStorageUserSettings = () => {
|
||||||
@@ -52,3 +54,4 @@ export const userSettingsHelperAtom = atom(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
export const hideModalTemporarilyAtom = atom<boolean>(false);
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
|
import { defaultSettings } from '@/atoms/settings';
|
||||||
import { getDefaultStore } from 'jotai';
|
import { getDefaultStore } from 'jotai';
|
||||||
|
|
||||||
export const clearStorageUserSettings = () => {
|
export const clearStorageUserSettings = () => {
|
||||||
|
try {
|
||||||
const savedSettings = JSON.parse(
|
const savedSettings = JSON.parse(
|
||||||
localStorage.getItem('userSettings') || '{}'
|
localStorage.getItem('userSettings') || '{}'
|
||||||
);
|
);
|
||||||
@@ -8,9 +10,27 @@ export const clearStorageUserSettings = () => {
|
|||||||
'userSettings',
|
'userSettings',
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
...savedSettings,
|
...savedSettings,
|
||||||
|
hideAddResourceModal: false,
|
||||||
colorPrimary: undefined
|
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) => {
|
export const clearAtomStorage = (atom: any) => {
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
import { WarningFilled } from '@ant-design/icons';
|
import {
|
||||||
|
CheckCircleFilled,
|
||||||
|
LoadingOutlined,
|
||||||
|
WarningFilled
|
||||||
|
} from '@ant-design/icons';
|
||||||
import { Typography } from 'antd';
|
import { Typography } from 'antd';
|
||||||
import { createStyles } from 'antd-style';
|
import { createStyles } from 'antd-style';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
@@ -122,6 +126,17 @@ const AlertInfo: React.FC<AlertInfoProps> = (props) => {
|
|||||||
overlayScrollerProps = {}
|
overlayScrollerProps = {}
|
||||||
} = props;
|
} = props;
|
||||||
const { styles } = useStyles();
|
const { styles } = useStyles();
|
||||||
|
|
||||||
|
const renderIcon = () => {
|
||||||
|
if (type === 'transition') {
|
||||||
|
return <LoadingOutlined />;
|
||||||
|
}
|
||||||
|
if (type === 'success') {
|
||||||
|
return <CheckCircleFilled />;
|
||||||
|
}
|
||||||
|
return <WarningFilled />;
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{message ? (
|
{message ? (
|
||||||
@@ -139,7 +154,7 @@ const AlertInfo: React.FC<AlertInfoProps> = (props) => {
|
|||||||
>
|
>
|
||||||
<div className={classNames('title', type)}>
|
<div className={classNames('title', type)}>
|
||||||
<span className={classNames('info-icon', type)}>
|
<span className={classNames('info-icon', type)}>
|
||||||
{icon ?? <WarningFilled />}
|
{icon ?? renderIcon()}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{title && (
|
{title && (
|
||||||
@@ -164,4 +179,4 @@ const AlertInfo: React.FC<AlertInfoProps> = (props) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default React.memo(AlertInfo);
|
export default AlertInfo;
|
||||||
|
|||||||
@@ -9,10 +9,11 @@ import echarts, { ECOption } from '.';
|
|||||||
|
|
||||||
const Chart: React.FC<{
|
const Chart: React.FC<{
|
||||||
options: ECOption;
|
options: ECOption;
|
||||||
|
chartHeight?: number;
|
||||||
height: number | string;
|
height: number | string;
|
||||||
width: number | string;
|
width: number | string;
|
||||||
ref?: any;
|
ref?: any;
|
||||||
}> = forwardRef(({ options, width, height }, ref) => {
|
}> = forwardRef(({ options, width, height, chartHeight }, ref) => {
|
||||||
const container = useRef<HTMLDivElement>(null);
|
const container = useRef<HTMLDivElement>(null);
|
||||||
const chart = useRef<echarts.EChartsType>();
|
const chart = useRef<echarts.EChartsType>();
|
||||||
const resizeable = useRef(false);
|
const resizeable = useRef(false);
|
||||||
@@ -138,7 +139,10 @@ const Chart: React.FC<{
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="chart-wrapper" style={{ width: width, height }}>
|
<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>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -220,6 +220,7 @@ export default function useChartConfig() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
token,
|
||||||
tooltip,
|
tooltip,
|
||||||
grid,
|
grid,
|
||||||
legend,
|
legend,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import Chart from '@/components/echarts/chart';
|
|||||||
import useChartConfig from '@/components/echarts/config';
|
import useChartConfig from '@/components/echarts/config';
|
||||||
import EmptyData from '@/components/empty-data';
|
import EmptyData from '@/components/empty-data';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import React, { memo, useMemo } from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import { ChartProps } from './types';
|
import { ChartProps } from './types';
|
||||||
|
|
||||||
const BarChart: React.FC<ChartProps & { maxItems?: number }> = (props) => {
|
const BarChart: React.FC<ChartProps & { maxItems?: number }> = (props) => {
|
||||||
@@ -17,6 +17,7 @@ const BarChart: React.FC<ChartProps & { maxItems?: number }> = (props) => {
|
|||||||
title
|
title
|
||||||
} = props;
|
} = props;
|
||||||
const {
|
const {
|
||||||
|
token,
|
||||||
grid,
|
grid,
|
||||||
legend,
|
legend,
|
||||||
title: titleConfig,
|
title: titleConfig,
|
||||||
@@ -44,17 +45,27 @@ const BarChart: React.FC<ChartProps & { maxItems?: number }> = (props) => {
|
|||||||
xAxis: {
|
xAxis: {
|
||||||
...xAxis,
|
...xAxis,
|
||||||
axisLabel: {
|
axisLabel: {
|
||||||
...xAxis.axisLabel,
|
...xAxis.axisLabel
|
||||||
formatter: labelFormatter
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
yAxis: {
|
yAxis: {
|
||||||
...yAxis,
|
...yAxis,
|
||||||
axisLabel: {
|
axisLabel: {
|
||||||
...yAxis.axisLabel,
|
...yAxis.axisLabel,
|
||||||
|
show: true,
|
||||||
overflow: 'truncate',
|
overflow: 'truncate',
|
||||||
width: 75,
|
width: 75,
|
||||||
ellipsis: '...'
|
ellipsis: '...'
|
||||||
|
// formatter(value: string, index: number) {
|
||||||
|
// return `{a|${index + 1}}`;
|
||||||
|
// },
|
||||||
|
// rich: {
|
||||||
|
// a: {
|
||||||
|
// fontWeight: 500,
|
||||||
|
// fontSize: 14,
|
||||||
|
// color: token?.colorTextSecondary
|
||||||
|
// }
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
legend: {
|
legend: {
|
||||||
@@ -72,7 +83,23 @@ const BarChart: React.FC<ChartProps & { maxItems?: number }> = (props) => {
|
|||||||
stack: 'Ad',
|
stack: 'Ad',
|
||||||
barGap: '30%',
|
barGap: '30%',
|
||||||
label: {
|
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: {
|
itemStyle: {
|
||||||
color: item.color
|
color: item.color
|
||||||
@@ -138,7 +165,10 @@ const BarChart: React.FC<ChartProps & { maxItems?: number }> = (props) => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{isEmpty ? (
|
{isEmpty ? (
|
||||||
<EmptyData height={height} title={title}></EmptyData>
|
<EmptyData
|
||||||
|
height={height}
|
||||||
|
title={_.get(title, 'text', title || '')}
|
||||||
|
></EmptyData>
|
||||||
) : (
|
) : (
|
||||||
<Chart
|
<Chart
|
||||||
height={height}
|
height={height}
|
||||||
@@ -150,4 +180,4 @@ const BarChart: React.FC<ChartProps & { maxItems?: number }> = (props) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default memo(BarChart);
|
export default BarChart;
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import useChartConfig from '@/components/echarts/config';
|
|||||||
import EmptyData from '@/components/empty-data';
|
import EmptyData from '@/components/empty-data';
|
||||||
import { genColors } from '@/utils';
|
import { genColors } from '@/utils';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import React, { memo, useMemo } from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import echarts from '.';
|
import echarts from '.';
|
||||||
import { ChartProps } from './types';
|
import { ChartProps } from './types';
|
||||||
|
|
||||||
@@ -169,4 +169,4 @@ const LineChart: React.FC<ChartProps> = (props) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default memo(LineChart);
|
export default LineChart;
|
||||||
|
|||||||
@@ -158,7 +158,10 @@ const MixLineBarChart: React.FC<
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{!lineSeriesData.length && !barSeriesData.length ? (
|
{!lineSeriesData.length && !barSeriesData.length ? (
|
||||||
<EmptyData height={height} title={title}></EmptyData>
|
<EmptyData
|
||||||
|
height={height}
|
||||||
|
title={_.get(title, 'text', title || '')}
|
||||||
|
></EmptyData>
|
||||||
) : (
|
) : (
|
||||||
<Chart
|
<Chart
|
||||||
height={height}
|
height={height}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { memo } from 'react';
|
|
||||||
import CodeViewer from './code-viewer';
|
import CodeViewer from './code-viewer';
|
||||||
import './styles/dark.less';
|
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 CodeViewer from './code-viewer';
|
||||||
import './styles/light.less';
|
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 classNames from 'classnames';
|
||||||
import hljs from 'highlight.js';
|
import hljs from 'highlight.js';
|
||||||
import { memo, useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import CopyButton from '../copy-button';
|
import CopyButton from '../copy-button';
|
||||||
import { escapeHtml } from './utils';
|
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;
|
white-space: pre-wrap;
|
||||||
|
|
||||||
code {
|
code {
|
||||||
line-height: 1.5;
|
line-height: 1.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.copyable {
|
&.copyable {
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
@font-face {
|
@font-face {
|
||||||
font-family: iconfont; /* Project id 4613488 */
|
font-family: iconfont; /* Project id 4613488 */
|
||||||
src: url('iconfont.woff2?t=1763436184844') format('woff2'),
|
src: url('iconfont.woff2?t=1766993792323') format('woff2'),
|
||||||
url('iconfont.woff?t=1763436184844') format('woff'),
|
url('iconfont.woff?t=1766993792323') format('woff'),
|
||||||
url('iconfont.ttf?t=1763436184844') format('truetype');
|
url('iconfont.ttf?t=1766993792323') format('truetype');
|
||||||
}
|
}
|
||||||
|
|
||||||
.iconfont {
|
.iconfont {
|
||||||
@@ -13,6 +13,18 @@
|
|||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.icon-cloud::before {
|
||||||
|
content: "\e6bc";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-server02::before {
|
||||||
|
content: "\e6bd";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-drag_handle::before {
|
||||||
|
content: "\e6b9";
|
||||||
|
}
|
||||||
|
|
||||||
.icon-basic::before {
|
.icon-basic::before {
|
||||||
content: "\e6bb";
|
content: "\e6bb";
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -5,6 +5,27 @@
|
|||||||
"css_prefix_text": "icon-",
|
"css_prefix_text": "icon-",
|
||||||
"description": "",
|
"description": "",
|
||||||
"glyphs": [
|
"glyphs": [
|
||||||
|
{
|
||||||
|
"icon_id": "46296038",
|
||||||
|
"name": "cloud",
|
||||||
|
"font_class": "cloud",
|
||||||
|
"unicode": "e6bc",
|
||||||
|
"unicode_decimal": 59068
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"icon_id": "46296037",
|
||||||
|
"name": "server",
|
||||||
|
"font_class": "server02",
|
||||||
|
"unicode": "e6bd",
|
||||||
|
"unicode_decimal": 59069
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"icon_id": "46272083",
|
||||||
|
"name": "drag_handle",
|
||||||
|
"font_class": "drag_handle",
|
||||||
|
"unicode": "e6b9",
|
||||||
|
"unicode_decimal": 59065
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"icon_id": "46055620",
|
"icon_id": "46055620",
|
||||||
"name": "basic",
|
"name": "basic",
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -10,6 +10,7 @@ import {
|
|||||||
KubernetesOutlined,
|
KubernetesOutlined,
|
||||||
ProfileOutlined,
|
ProfileOutlined,
|
||||||
RetweetOutlined,
|
RetweetOutlined,
|
||||||
|
StarOutlined,
|
||||||
ThunderboltOutlined
|
ThunderboltOutlined
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
@@ -58,7 +59,8 @@ const icons = {
|
|||||||
LockPerson: React.createElement(IconFont, { type: 'icon-lock_person' }),
|
LockPerson: React.createElement(IconFont, { type: 'icon-lock_person' }),
|
||||||
LockOpen: React.createElement(IconFont, { type: 'icon-lock_open' }),
|
LockOpen: React.createElement(IconFont, { type: 'icon-lock_open' }),
|
||||||
Permission: React.createElement(IconFont, { type: 'icon-permission' }),
|
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;
|
export default icons;
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import classNames from 'classnames';
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import {
|
import {
|
||||||
forwardRef,
|
forwardRef,
|
||||||
memo,
|
|
||||||
useEffect,
|
useEffect,
|
||||||
useImperativeHandle,
|
useImperativeHandle,
|
||||||
useRef,
|
useRef,
|
||||||
@@ -176,4 +175,4 @@ const LogsViewer: React.FC<LogsViewerProps> = forwardRef((props, ref) => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
export default memo(LogsViewer);
|
export default LogsViewer;
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -2,8 +2,20 @@ import { useEscHint } from '@/hooks/use-esc-hint';
|
|||||||
import { CloseOutlined } from '@ant-design/icons';
|
import { CloseOutlined } from '@ant-design/icons';
|
||||||
import { Button, Drawer, type DrawerProps } from 'antd';
|
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 ScrollerModal = (props: DrawerProps) => {
|
||||||
const { title, closable = true, maskClosable = false, ...restProps } = props;
|
const {
|
||||||
|
title,
|
||||||
|
closable = true,
|
||||||
|
maskClosable = false,
|
||||||
|
styles,
|
||||||
|
...restProps
|
||||||
|
} = props;
|
||||||
const { EscHint } = useEscHint({
|
const { EscHint } = useEscHint({
|
||||||
enabled: !props.keyboard && props.open
|
enabled: !props.keyboard && props.open
|
||||||
});
|
});
|
||||||
@@ -14,6 +26,19 @@ const ScrollerModal = (props: DrawerProps) => {
|
|||||||
<>
|
<>
|
||||||
<Drawer
|
<Drawer
|
||||||
{...restProps}
|
{...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}
|
closable={false}
|
||||||
maskClosable={maskClosable}
|
maskClosable={maskClosable}
|
||||||
title={
|
title={
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import useOverlayScroller from '@/hooks/use-overlay-scroller';
|
|||||||
import { Modal, type ModalProps } from 'antd';
|
import { Modal, type ModalProps } from 'antd';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
import { ScrollerContext } from './use-scroller-context';
|
||||||
|
|
||||||
const Wrapper = styled.div<{ $maxHeight?: number | string }>`
|
const Wrapper = styled.div<{ $maxHeight?: number | string }>`
|
||||||
max-height: ${({ $maxHeight }) =>
|
max-height: ${({ $maxHeight }) =>
|
||||||
@@ -11,12 +12,21 @@ const Wrapper = styled.div<{ $maxHeight?: number | string }>`
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const Title = styled.div`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
max-width: 360px;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
`;
|
||||||
|
|
||||||
const ScrollerModal = (
|
const ScrollerModal = (
|
||||||
props: ModalProps & { maxContentHeight?: number | string }
|
props: ModalProps & { maxContentHeight?: number | string }
|
||||||
) => {
|
) => {
|
||||||
const scroller = React.useRef<any>(null);
|
const scroller = React.useRef<any>(null);
|
||||||
const { saveScrollHeight, restoreScrollHeight } = useBodyScroll();
|
const { saveScrollHeight, restoreScrollHeight } = useBodyScroll();
|
||||||
const { initialize, destroyInstance } = useOverlayScroller();
|
const { initialize, destroyInstance, scrollToBottom } = useOverlayScroller();
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (props.open) {
|
if (props.open) {
|
||||||
@@ -47,6 +57,7 @@ const ScrollerModal = (
|
|||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
{...props}
|
{...props}
|
||||||
|
title={<Title>{props.title}</Title>}
|
||||||
destroyOnHidden={true}
|
destroyOnHidden={true}
|
||||||
styles={{
|
styles={{
|
||||||
content: {
|
content: {
|
||||||
@@ -68,6 +79,7 @@ const ScrollerModal = (
|
|||||||
: {}
|
: {}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
<ScrollerContext.Provider value={{ scrollToBottom }}>
|
||||||
<Wrapper
|
<Wrapper
|
||||||
ref={scroller}
|
ref={scroller}
|
||||||
data-overlayscrollbars-initialize
|
data-overlayscrollbars-initialize
|
||||||
@@ -78,6 +90,7 @@ const ScrollerModal = (
|
|||||||
>
|
>
|
||||||
{props.children}
|
{props.children}
|
||||||
</Wrapper>
|
</Wrapper>
|
||||||
|
</ScrollerContext.Provider>
|
||||||
</Modal>
|
</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);
|
||||||
@@ -131,7 +131,6 @@ const SealAutoComplete: React.FC<
|
|||||||
>
|
>
|
||||||
<AutoComplete
|
<AutoComplete
|
||||||
{...rest}
|
{...rest}
|
||||||
trim={trim}
|
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
placeholder={
|
placeholder={
|
||||||
isFocus || !label ? (
|
isFocus || !label ? (
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ const SealTextArea: React.FC<InputTextareaProps & SealFormItemProps> = (
|
|||||||
<InputWrapper>
|
<InputWrapper>
|
||||||
<Wrapper
|
<Wrapper
|
||||||
status={status}
|
status={status}
|
||||||
label={<LabelWrapper>{label}</LabelWrapper>}
|
label={label && <LabelWrapper>{label}</LabelWrapper>}
|
||||||
isFocus={alwaysFocus || isFocus}
|
isFocus={alwaysFocus || isFocus}
|
||||||
required={required}
|
required={required}
|
||||||
description={description}
|
description={description}
|
||||||
|
|||||||
@@ -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 { Col, Row } from 'antd';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { SealColumnProps } from '../types';
|
import { OnSortFn, SealColumnProps, TableOrder } from '../types';
|
||||||
import TableHeader from './table-header';
|
import TableHeader from './table-header';
|
||||||
|
|
||||||
interface HeaderProps {
|
interface HeaderProps {
|
||||||
columns: SealColumnProps[];
|
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 Header: React.FC<HeaderProps> = (props) => {
|
||||||
const { onSort } = props;
|
const { onSort, sortDirections, sorterList, showSorterTooltip } = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Row className="row">
|
<Row className="row">
|
||||||
@@ -29,9 +32,12 @@ const Header: React.FC<HeaderProps> = (props) => {
|
|||||||
<Col span={span} key={dataIndex || i}>
|
<Col span={span} key={dataIndex || i}>
|
||||||
<TableHeader
|
<TableHeader
|
||||||
onSort={onSort}
|
onSort={onSort}
|
||||||
|
showSorterTooltip={showSorterTooltip}
|
||||||
sorter={sorter}
|
sorter={sorter}
|
||||||
|
sorterList={sorterList}
|
||||||
dataIndex={dataIndex}
|
dataIndex={dataIndex}
|
||||||
sortOrder={sortOrder}
|
sortOrder={sortOrder}
|
||||||
|
sortDirections={sortDirections}
|
||||||
width={width}
|
width={width}
|
||||||
defaultSortOrder={defaultSortOrder}
|
defaultSortOrder={defaultSortOrder}
|
||||||
title={title}
|
title={title}
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ const CellWrapper = styled.div`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
const TableCell: React.FC<SealColumnProps> = (props) => {
|
const TableCell: React.FC<SealColumnProps> = (props) => {
|
||||||
const { dataIndex, render, align, editable } = props;
|
const { dataIndex, render, align, editable, dataField } = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CellWrapper
|
<CellWrapper
|
||||||
@@ -40,7 +40,7 @@ const TableCell: React.FC<SealColumnProps> = (props) => {
|
|||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<CellContent
|
<CellContent
|
||||||
dataIndex={dataIndex}
|
dataIndex={dataField || dataIndex}
|
||||||
render={render}
|
render={render}
|
||||||
editable={editable}
|
editable={editable}
|
||||||
></CellContent>
|
></CellContent>
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import AutoTooltip from '@/components/auto-tooltip';
|
import AutoTooltip from '@/components/auto-tooltip';
|
||||||
import { CaretDownOutlined, CaretUpOutlined } from '@ant-design/icons';
|
import { CaretDownOutlined, CaretUpOutlined } from '@ant-design/icons';
|
||||||
|
import { useIntl } from '@umijs/max';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import React from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import '../styles/header.less';
|
import '../styles/header.less';
|
||||||
|
|
||||||
import { TableHeaderProps } from '../types';
|
import { TableHeaderProps } from '../types';
|
||||||
@@ -14,19 +15,74 @@ const TableHeader: React.FC<TableHeaderProps> = (props) => {
|
|||||||
firstCell,
|
firstCell,
|
||||||
lastCell,
|
lastCell,
|
||||||
sortOrder,
|
sortOrder,
|
||||||
|
sortDirections = ['ascend', 'descend', null],
|
||||||
|
defaultSortOrder,
|
||||||
onSort,
|
onSort,
|
||||||
sorter,
|
sorterList,
|
||||||
|
sorter = false,
|
||||||
width,
|
width,
|
||||||
dataIndex
|
dataIndex
|
||||||
} = props;
|
} = 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 = () => {
|
const handleOnSort = () => {
|
||||||
if (sortOrder === 'ascend') {
|
setCurrentSortOrder((prev) => {
|
||||||
onSort?.(dataIndex, 'descend');
|
const next = getNextSortOrder(prev);
|
||||||
} else {
|
onSort?.(
|
||||||
onSort?.(dataIndex, 'ascend');
|
{
|
||||||
}
|
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 (
|
return (
|
||||||
<div
|
<div
|
||||||
style={{ width, ...style }}
|
style={{ width, ...style }}
|
||||||
@@ -47,12 +103,12 @@ const TableHeader: React.FC<TableHeaderProps> = (props) => {
|
|||||||
<span className="sorter">
|
<span className="sorter">
|
||||||
<CaretUpOutlined
|
<CaretUpOutlined
|
||||||
className={classNames('sorter-up', {
|
className={classNames('sorter-up', {
|
||||||
'sorter-active': sortOrder === 'ascend'
|
'sorter-active': currentSortOrder === 'ascend'
|
||||||
})}
|
})}
|
||||||
></CaretUpOutlined>
|
></CaretUpOutlined>
|
||||||
<CaretDownOutlined
|
<CaretDownOutlined
|
||||||
className={classNames('sorter-down', {
|
className={classNames('sorter-down', {
|
||||||
'sorter-active': sortOrder === 'descend'
|
'sorter-active': currentSortOrder === 'descend'
|
||||||
})}
|
})}
|
||||||
></CaretDownOutlined>
|
></CaretDownOutlined>
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import HeaderPrefix from './components/header-prefix';
|
|||||||
import TableBody from './components/table-body';
|
import TableBody from './components/table-body';
|
||||||
import './styles/index.less';
|
import './styles/index.less';
|
||||||
import { SealColumnProps, SealTableProps } from './types';
|
import { SealColumnProps, SealTableProps } from './types';
|
||||||
|
import useSorter from './use-sorter';
|
||||||
|
|
||||||
const Wrapper = styled.div<{ $token: any }>`
|
const Wrapper = styled.div<{ $token: any }>`
|
||||||
--ant-table-cell-padding-inline: ${(props) =>
|
--ant-table-cell-padding-inline: ${(props) =>
|
||||||
@@ -32,7 +33,7 @@ const SealTable: React.FC<SealTableProps & { pagination: PaginationProps }> = (
|
|||||||
childParentKey,
|
childParentKey,
|
||||||
onExpand,
|
onExpand,
|
||||||
onExpandAll,
|
onExpandAll,
|
||||||
onSort,
|
onTableSort,
|
||||||
onCell,
|
onCell,
|
||||||
expandedRowKeys,
|
expandedRowKeys,
|
||||||
loading,
|
loading,
|
||||||
@@ -43,10 +44,16 @@ const SealTable: React.FC<SealTableProps & { pagination: PaginationProps }> = (
|
|||||||
rowSelection,
|
rowSelection,
|
||||||
pagination,
|
pagination,
|
||||||
empty,
|
empty,
|
||||||
|
sortDirections,
|
||||||
|
showSorterTooltip,
|
||||||
renderChildren,
|
renderChildren,
|
||||||
loadChildren,
|
loadChildren,
|
||||||
loadChildrenAPI
|
loadChildrenAPI
|
||||||
} = props;
|
} = props;
|
||||||
|
const { handleOnTableSort, sorterList } = useSorter({
|
||||||
|
onTableSort,
|
||||||
|
columns
|
||||||
|
});
|
||||||
const { token } = theme.useToken();
|
const { token } = theme.useToken();
|
||||||
const parsedColumns = useMemo(() => {
|
const parsedColumns = useMemo(() => {
|
||||||
if (columns) return columns;
|
if (columns) return columns;
|
||||||
@@ -156,7 +163,13 @@ const SealTable: React.FC<SealTableProps & { pagination: PaginationProps }> = (
|
|||||||
disabled={!props.dataSource?.length}
|
disabled={!props.dataSource?.length}
|
||||||
hasColumns={parsedColumns.length > 0}
|
hasColumns={parsedColumns.length > 0}
|
||||||
></HeaderPrefix>
|
></HeaderPrefix>
|
||||||
<Header onSort={onSort} columns={parsedColumns}></Header>
|
<Header
|
||||||
|
onSort={handleOnTableSort}
|
||||||
|
columns={parsedColumns}
|
||||||
|
sortDirections={sortDirections}
|
||||||
|
sorterList={sorterList}
|
||||||
|
showSorterTooltip={showSorterTooltip}
|
||||||
|
></Header>
|
||||||
</div>
|
</div>
|
||||||
<Spin spinning={loading}>
|
<Spin spinning={loading}>
|
||||||
<TableBody
|
<TableBody
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
|
height: 50px;
|
||||||
padding-inline: var(--ant-table-cell-padding-inline);
|
padding-inline: var(--ant-table-cell-padding-inline);
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
@@ -40,6 +41,7 @@
|
|||||||
.sorter-header {
|
.sorter-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
width: 100%;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|||||||
@@ -1,5 +1,14 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
|
export type OnSortFn = (
|
||||||
|
order: {
|
||||||
|
columnKey: string;
|
||||||
|
field: string;
|
||||||
|
order: 'ascend' | 'descend' | null;
|
||||||
|
},
|
||||||
|
sorter: boolean | { multiple?: number }
|
||||||
|
) => void;
|
||||||
|
|
||||||
export interface CellContentProps {
|
export interface CellContentProps {
|
||||||
dataIndex: string;
|
dataIndex: string;
|
||||||
render?: (text: any, record: any) => React.ReactNode;
|
render?: (text: any, record: any) => React.ReactNode;
|
||||||
@@ -16,11 +25,12 @@ export interface SealColumnProps {
|
|||||||
render?: (text: any, record: any) => React.ReactNode;
|
render?: (text: any, record: any) => React.ReactNode;
|
||||||
dataIndex: string;
|
dataIndex: string;
|
||||||
key?: string;
|
key?: string;
|
||||||
|
dataField?: string; // Added dataField property, aviods conflict with dataIndex, because dataIndex maybe used in sorting
|
||||||
width?: number;
|
width?: number;
|
||||||
span: number;
|
span: number;
|
||||||
align?: 'left' | 'center' | 'right';
|
align?: 'left' | 'center' | 'right';
|
||||||
headerStyle?: React.CSSProperties;
|
headerStyle?: React.CSSProperties;
|
||||||
sorter?: boolean;
|
sorter?: boolean | { multiple?: number };
|
||||||
defaultSortOrder?: 'ascend' | 'descend';
|
defaultSortOrder?: 'ascend' | 'descend';
|
||||||
editable?:
|
editable?:
|
||||||
| boolean
|
| boolean
|
||||||
@@ -34,17 +44,25 @@ export interface SealColumnProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface TableHeaderProps {
|
export interface TableHeaderProps {
|
||||||
sorter?: boolean;
|
showSorterTooltip?: boolean;
|
||||||
defaultSortOrder?: 'ascend' | 'descend';
|
sorterList?: TableOrder | Array<TableOrder>;
|
||||||
|
sorter?: boolean | { multiple?: number };
|
||||||
|
sortDirections?: ('ascend' | 'descend' | null)[];
|
||||||
|
defaultSortOrder?: 'ascend' | 'descend' | null;
|
||||||
sortOrder?: 'ascend' | 'descend' | null;
|
sortOrder?: 'ascend' | 'descend' | null;
|
||||||
dataIndex: string;
|
dataIndex: string;
|
||||||
onSort?: (dataIndex: string, order: 'ascend' | 'descend') => void;
|
onSort?: OnSortFn;
|
||||||
title: React.ReactNode;
|
title: React.ReactNode;
|
||||||
style?: React.CSSProperties;
|
style?: React.CSSProperties;
|
||||||
firstCell?: boolean;
|
firstCell?: boolean;
|
||||||
lastCell?: boolean;
|
lastCell?: boolean;
|
||||||
align?: 'left' | 'center' | 'right';
|
align?: 'left' | 'center' | 'right';
|
||||||
width?: number | string;
|
width?: number | string;
|
||||||
|
sortedDataIndexList?: Array<{
|
||||||
|
columnKey: string;
|
||||||
|
field: string;
|
||||||
|
order: 'ascend' | 'descend' | null;
|
||||||
|
}>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RowSelectionProps {
|
export interface RowSelectionProps {
|
||||||
@@ -54,7 +72,15 @@ export interface RowSelectionProps {
|
|||||||
removeSelectedKeys: (rowKeys: React.Key[]) => void;
|
removeSelectedKeys: (rowKeys: React.Key[]) => void;
|
||||||
onChange: (selectedRowKeys: React.Key[], selectedRows: any[]) => void;
|
onChange: (selectedRowKeys: React.Key[], selectedRows: any[]) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type TableOrder = {
|
||||||
|
columnKey?: string;
|
||||||
|
field?: string;
|
||||||
|
order: 'ascend' | 'descend' | null;
|
||||||
|
};
|
||||||
export interface SealTableProps {
|
export interface SealTableProps {
|
||||||
|
showSorterTooltip?: boolean;
|
||||||
|
sortDirections?: ('ascend' | 'descend' | null)[];
|
||||||
columns?: SealColumnProps[];
|
columns?: SealColumnProps[];
|
||||||
childParentKey?: string;
|
childParentKey?: string;
|
||||||
expandedRowKeys?: React.Key[];
|
expandedRowKeys?: React.Key[];
|
||||||
@@ -67,8 +93,8 @@ export interface SealTableProps {
|
|||||||
watchChildren?: boolean;
|
watchChildren?: boolean;
|
||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
loadend?: boolean;
|
loadend?: boolean;
|
||||||
onCell?: (record: any, dataIndex: string) => void;
|
onCell?: (record: any, extra: any) => void;
|
||||||
onSort?: (dataIndex: string, order: 'ascend' | 'descend') => void;
|
onTableSort?: (order: TableOrder | Array<TableOrder>) => void;
|
||||||
onExpand?: (expanded: boolean, record: any, rowKey: any) => void;
|
onExpand?: (expanded: boolean, record: any, rowKey: any) => void;
|
||||||
onExpandAll?: (expanded: boolean) => void;
|
onExpandAll?: (expanded: boolean) => void;
|
||||||
renderChildren?: (
|
renderChildren?: (
|
||||||
|
|||||||
@@ -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
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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 InfiniteScroller from '@/pages/_components/infinite-scroller';
|
||||||
import { useScrollerContext } from '@/pages/_components/infinite-scroller/use-scroller-context';
|
import { useScrollerContext } from '@/pages/_components/infinite-scroller/use-scroller-context';
|
||||||
import { Col, Row, Spin } from 'antd';
|
import { Spin } from 'antd';
|
||||||
import _ from 'lodash';
|
import React from 'react';
|
||||||
import ResizeObserver from 'rc-resize-observer';
|
|
||||||
import React, { useCallback } from 'react';
|
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
|
||||||
const SpinWrapper = styled.div`
|
const SpinWrapper = styled.div`
|
||||||
@@ -30,22 +29,12 @@ interface CatalogListProps {
|
|||||||
activeId: Global.WithFalse<number>;
|
activeId: Global.WithFalse<number>;
|
||||||
isFirst: boolean;
|
isFirst: boolean;
|
||||||
renderItem: (data: any) => React.ReactNode;
|
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<{
|
const ListSkeleton: React.FC<{
|
||||||
span: number;
|
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
isFirst: boolean;
|
isFirst: boolean;
|
||||||
Skeleton: React.ComponentType<{ span: number }>;
|
}> = ({ loading, isFirst }) => {
|
||||||
}> = ({ span, loading, isFirst, Skeleton }) => {
|
|
||||||
const CatalogSkeleton = InnerSkeleton(Skeleton);
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{loading && (
|
{loading && (
|
||||||
@@ -57,7 +46,7 @@ const ListSkeleton: React.FC<{
|
|||||||
}}
|
}}
|
||||||
wrapperClassName="skelton-wrapper"
|
wrapperClassName="skelton-wrapper"
|
||||||
>
|
>
|
||||||
{isFirst && <CatalogSkeleton span={span}></CatalogSkeleton>}
|
{isFirst && <CardSkeleton></CardSkeleton>}
|
||||||
</Spin>
|
</Spin>
|
||||||
</SpinWrapper>
|
</SpinWrapper>
|
||||||
)}
|
)}
|
||||||
@@ -72,10 +61,8 @@ const CardList: React.FC<CatalogListProps> = (props) => {
|
|||||||
isFirst,
|
isFirst,
|
||||||
defaultSpan = 8,
|
defaultSpan = 8,
|
||||||
resizable = true,
|
resizable = true,
|
||||||
Skeleton,
|
|
||||||
renderItem
|
renderItem
|
||||||
} = props;
|
} = props;
|
||||||
const [span, setSpan] = React.useState(defaultSpan);
|
|
||||||
const {
|
const {
|
||||||
total,
|
total,
|
||||||
current,
|
current,
|
||||||
@@ -83,46 +70,21 @@ const CardList: React.FC<CatalogListProps> = (props) => {
|
|||||||
refresh
|
refresh
|
||||||
} = useScrollerContext();
|
} = 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 (
|
return (
|
||||||
<div className="relative" style={{ width: '100%' }}>
|
|
||||||
<ResizeObserver onResize={handleResize} disabled={!resizable}>
|
|
||||||
<InfiniteScroller
|
<InfiniteScroller
|
||||||
total={total}
|
total={total}
|
||||||
current={current}
|
current={current}
|
||||||
loading={contextLoading}
|
loading={contextLoading}
|
||||||
refresh={refresh}
|
refresh={refresh}
|
||||||
>
|
>
|
||||||
<Row gutter={[16, 16]}>
|
<ResizeContainer
|
||||||
{dataList.map((item: any, index) => {
|
dataList={dataList}
|
||||||
return (
|
renderItem={renderItem}
|
||||||
<Col span={span} key={item.id}>
|
defaultSpan={defaultSpan}
|
||||||
{renderItem?.(item)}
|
resizable={resizable}
|
||||||
</Col>
|
></ResizeContainer>
|
||||||
);
|
<ListSkeleton loading={loading} isFirst={isFirst} />
|
||||||
})}
|
|
||||||
</Row>
|
|
||||||
<ListSkeleton
|
|
||||||
span={span}
|
|
||||||
loading={loading}
|
|
||||||
isFirst={isFirst}
|
|
||||||
Skeleton={Skeleton}
|
|
||||||
/>
|
|
||||||
</InfiniteScroller>
|
</InfiniteScroller>
|
||||||
</ResizeObserver>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,20 +1,17 @@
|
|||||||
import { Col, Row, Skeleton } from 'antd';
|
import { Skeleton } from 'antd';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import ResizeContainer from '../resize-container';
|
||||||
|
|
||||||
interface CatalogSkeltonProps {
|
interface CatalogSkeltonProps {
|
||||||
span: number;
|
|
||||||
skeletonProps?: any;
|
skeletonProps?: any;
|
||||||
skeletonStyle?: React.CSSProperties;
|
skeletonStyle?: React.CSSProperties;
|
||||||
}
|
}
|
||||||
|
|
||||||
const CardSkelton: React.FC<CatalogSkeltonProps> = (props) => {
|
const CardSkelton: React.FC<CatalogSkeltonProps> = (props) => {
|
||||||
return (
|
return (
|
||||||
<Row gutter={[16, 16]}>
|
<ResizeContainer
|
||||||
{Array(6)
|
dataList={Array(6).fill({ label: 'skeleton' })}
|
||||||
.fill(1)
|
renderItem={() => (
|
||||||
.map((_, index) => {
|
|
||||||
return (
|
|
||||||
<Col span={props.span} key={index}>
|
|
||||||
<Skeleton
|
<Skeleton
|
||||||
avatar={{
|
avatar={{
|
||||||
size: 32
|
size: 32
|
||||||
@@ -28,11 +25,9 @@ const CardSkelton: React.FC<CatalogSkeltonProps> = (props) => {
|
|||||||
}}
|
}}
|
||||||
{...(props.skeletonProps || {})}
|
{...(props.skeletonProps || {})}
|
||||||
></Skeleton>
|
></Skeleton>
|
||||||
</Col>
|
)}
|
||||||
);
|
></ResizeContainer>
|
||||||
})}
|
|
||||||
</Row>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default React.memo(CardSkelton);
|
export default CardSkelton;
|
||||||
|
|||||||
+2
-2
@@ -49,7 +49,7 @@ export const WatchEventType = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const PasswordReg =
|
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])/;
|
export const uppercaseReg = /(?=.*[A-Z])/;
|
||||||
|
|
||||||
@@ -61,7 +61,7 @@ export const specialCharacterReg = /(?=.*[\W_])/;
|
|||||||
|
|
||||||
export const noSpaceReg = /(?=\S+$)/;
|
export const noSpaceReg = /(?=\S+$)/;
|
||||||
|
|
||||||
export const lengthReg = /^.{6,12}$/;
|
export const lengthReg = /^.{6,64}$/;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Model name rules:
|
* Model name rules:
|
||||||
|
|||||||
@@ -9,3 +9,24 @@ export const DEFAULT_ENTER_PAGE = {
|
|||||||
|
|
||||||
export const GPUSTACK_API_BASE_URL = 'v2';
|
export const GPUSTACK_API_BASE_URL = 'v2';
|
||||||
export const OPENAI_COMPATIBLE = 'v1';
|
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;
|
||||||
|
};
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ export default {
|
|||||||
inputFontSizeLG: 14
|
inputFontSizeLG: 14
|
||||||
},
|
},
|
||||||
Steps: {
|
Steps: {
|
||||||
descriptionMaxWidth: 200
|
descriptionMaxWidth: 200,
|
||||||
|
iconSizeSM: 20
|
||||||
},
|
},
|
||||||
Table: {
|
Table: {
|
||||||
headerBorderRadius: 4,
|
headerBorderRadius: 4,
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ export default {
|
|||||||
inputFontSizeLG: 14
|
inputFontSizeLG: 14
|
||||||
},
|
},
|
||||||
Steps: {
|
Steps: {
|
||||||
descriptionMaxWidth: 200
|
descriptionMaxWidth: 200,
|
||||||
|
iconSizeSM: 20
|
||||||
},
|
},
|
||||||
Table: {
|
Table: {
|
||||||
headerBorderRadius: 4,
|
headerBorderRadius: 4,
|
||||||
|
|||||||
+10
-2
@@ -203,7 +203,7 @@ body {
|
|||||||
|
|
||||||
// form item help
|
// form item help
|
||||||
.ant-form-item-with-help .ant-form-item-explain {
|
.ant-form-item-with-help .ant-form-item-explain {
|
||||||
padding-left: 16px;
|
// padding-left: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
// icon
|
// icon
|
||||||
@@ -218,6 +218,11 @@ body {
|
|||||||
background-color: var(--color-fill-1);
|
background-color: var(--color-fill-1);
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
||||||
|
.ant-pro-sider-footer {
|
||||||
|
padding-block: 2px 0;
|
||||||
|
padding-left: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
.ant-pro-sider-actions-list-item {
|
.ant-pro-sider-actions-list-item {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
|
||||||
@@ -228,7 +233,6 @@ body {
|
|||||||
|
|
||||||
.ant-pro-sider-logo {
|
.ant-pro-sider-logo {
|
||||||
position: relative;
|
position: relative;
|
||||||
padding-left: 18px;
|
|
||||||
padding-block: 12px;
|
padding-block: 12px;
|
||||||
border-block-end: none;
|
border-block-end: none;
|
||||||
}
|
}
|
||||||
@@ -788,3 +792,7 @@ body {
|
|||||||
.ant-page-header .ant-page-header-heading-extra {
|
.ant-page-header .ant-page-header-heading-extra {
|
||||||
margin-block: 0;
|
margin-block: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ant-pro-footer-bar {
|
||||||
|
padding-inline: 0 !important;
|
||||||
|
}
|
||||||
|
|||||||
@@ -82,7 +82,9 @@ export default function useOverlayScroller(data?: {
|
|||||||
...scrollbars,
|
...scrollbars,
|
||||||
theme:
|
theme:
|
||||||
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: {
|
events: {
|
||||||
@@ -166,14 +168,17 @@ export default function useOverlayScroller(data?: {
|
|||||||
|
|
||||||
// add wheel event
|
// add wheel event
|
||||||
const handleWheelEvent = () => {
|
const handleWheelEvent = () => {
|
||||||
scrollElementRef.current?.addEventListener?.('wheel', handleWheelCallback);
|
scrollElementRef.current?.addEventListener?.('wheel', handleWheelCallback, {
|
||||||
|
passive: true
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// remove wheel event
|
// remove wheel event
|
||||||
const removeWheelEvent = () => {
|
const removeWheelEvent = () => {
|
||||||
scrollElementRef.current?.removeEventListener?.(
|
scrollElementRef.current?.removeEventListener?.(
|
||||||
'wheel',
|
'wheel',
|
||||||
handleWheelCallback
|
handleWheelCallback,
|
||||||
|
{ passive: true }
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
|
import { TABLE_SORT_DIRECTIONS } from '@/config/settings';
|
||||||
import useSetChunkRequest from '@/hooks/use-chunk-request';
|
import useSetChunkRequest from '@/hooks/use-chunk-request';
|
||||||
import useTableRowSelection from '@/hooks/use-table-row-selection';
|
import useTableRowSelection from '@/hooks/use-table-row-selection';
|
||||||
import useTableSort from '@/hooks/use-table-sort';
|
|
||||||
import useUpdateChunkedList from '@/hooks/use-update-chunk-list';
|
import useUpdateChunkedList from '@/hooks/use-update-chunk-list';
|
||||||
import { handleBatchRequest } from '@/utils';
|
import { handleBatchRequest } from '@/utils';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import qs from 'query-string';
|
import qs from 'query-string';
|
||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
|
import { useTableMultiSort } from './use-table-sort';
|
||||||
|
|
||||||
type EventsType = 'CREATE' | 'UPDATE' | 'DELETE' | 'INSERT';
|
type EventsType = 'CREATE' | 'UPDATE' | 'DELETE' | 'INSERT';
|
||||||
|
|
||||||
@@ -52,9 +53,7 @@ export default function useTableFetch<T>(
|
|||||||
const chunkRequedtRef = useRef<any>(null);
|
const chunkRequedtRef = useRef<any>(null);
|
||||||
const modalRef = useRef<any>(null);
|
const modalRef = useRef<any>(null);
|
||||||
const rowSelection = useTableRowSelection();
|
const rowSelection = useTableRowSelection();
|
||||||
const { sortOrder, setSortOrder } = useTableSort({
|
const { sortOrder, handleMultiSortChange } = useTableMultiSort();
|
||||||
defaultSortOrder: 'descend'
|
|
||||||
});
|
|
||||||
|
|
||||||
// for skeleton loading
|
// for skeleton loading
|
||||||
const [extraStatus, setExtraStatus] = useState<Record<string, any>>({
|
const [extraStatus, setExtraStatus] = useState<Record<string, any>>({
|
||||||
@@ -78,6 +77,7 @@ export default function useTableFetch<T>(
|
|||||||
page: 1,
|
page: 1,
|
||||||
perPage: 10,
|
perPage: 10,
|
||||||
search: '',
|
search: '',
|
||||||
|
sort_by: '',
|
||||||
...defaultQueryParams
|
...defaultQueryParams
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -103,7 +103,6 @@ export default function useTableFetch<T>(
|
|||||||
|
|
||||||
const updateHandler = (list: any) => {
|
const updateHandler = (list: any) => {
|
||||||
_.each(list, (data: any) => {
|
_.each(list, (data: any) => {
|
||||||
console.log('list================:', list);
|
|
||||||
updateChunkedList(data);
|
updateChunkedList(data);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -234,8 +233,29 @@ export default function useTableFetch<T>(
|
|||||||
handleQueryChange({ page, perPage: pageSize || 10 });
|
handleQueryChange({ page, perPage: pageSize || 10 });
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleTableChange = (pagination: any, filters: any, sorter: any) => {
|
const handleTableChange = (
|
||||||
setSortOrder(sorter.order);
|
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 = () => {
|
const handleSearch = () => {
|
||||||
@@ -331,6 +351,7 @@ export default function useTableFetch<T>(
|
|||||||
queryParams,
|
queryParams,
|
||||||
modalRef,
|
modalRef,
|
||||||
extraStatus,
|
extraStatus,
|
||||||
|
TABLE_SORT_DIRECTIONS,
|
||||||
setQueryParams,
|
setQueryParams,
|
||||||
handleDelete,
|
handleDelete,
|
||||||
handleDeleteBatch,
|
handleDeleteBatch,
|
||||||
|
|||||||
@@ -23,3 +23,32 @@ export default function useTableSort({
|
|||||||
setSortOrder: handleSortChange
|
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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -15,7 +15,10 @@ export function useUpdateChunkedList(options: {
|
|||||||
events?: EventsType[];
|
events?: EventsType[];
|
||||||
dataList?: any[];
|
dataList?: any[];
|
||||||
limit?: number;
|
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;
|
callback?: (args: any) => void;
|
||||||
filterFun?: (args: any) => boolean;
|
filterFun?: (args: any) => boolean;
|
||||||
mapFun?: (args: any) => any;
|
mapFun?: (args: any) => any;
|
||||||
@@ -31,6 +34,7 @@ export function useUpdateChunkedList(options: {
|
|||||||
cacheDataListRef.current = [...(options.dataList || [])];
|
cacheDataListRef.current = [...(options.dataList || [])];
|
||||||
}, [options.dataList]);
|
}, [options.dataList]);
|
||||||
|
|
||||||
|
// use to clear the table row selection after delete
|
||||||
const setDeletedIds = (ids: number[]) => {
|
const setDeletedIds = (ids: number[]) => {
|
||||||
deletedIdsRef.current = new Set([...deletedIdsRef.current, ...ids]);
|
deletedIdsRef.current = new Set([...deletedIdsRef.current, ...ids]);
|
||||||
};
|
};
|
||||||
@@ -41,6 +45,7 @@ export function useUpdateChunkedList(options: {
|
|||||||
}, 200);
|
}, 200);
|
||||||
};
|
};
|
||||||
const updateChunkedList = (data: ChunkedCollection) => {
|
const updateChunkedList = (data: ChunkedCollection) => {
|
||||||
|
console.log('updateChunkedList data:', data);
|
||||||
let collections = data?.collection || [];
|
let collections = data?.collection || [];
|
||||||
if (options?.computedID) {
|
if (options?.computedID) {
|
||||||
collections = collections?.map((item: any) => {
|
collections = collections?.map((item: any) => {
|
||||||
@@ -55,6 +60,7 @@ export function useUpdateChunkedList(options: {
|
|||||||
collections = data?.collection?.map(options?.mapFun);
|
collections = data?.collection?.map(options?.mapFun);
|
||||||
}
|
}
|
||||||
const ids: any[] = data?.ids || [];
|
const ids: any[] = data?.ids || [];
|
||||||
|
|
||||||
// CREATE
|
// CREATE
|
||||||
if (data?.type === WatchEventType.CREATE && events.includes('CREATE')) {
|
if (data?.type === WatchEventType.CREATE && events.includes('CREATE')) {
|
||||||
const newDataList = collections.reduce((acc: any[], item: any) => {
|
const newDataList = collections.reduce((acc: any[], item: any) => {
|
||||||
@@ -70,11 +76,15 @@ export function useUpdateChunkedList(options: {
|
|||||||
|
|
||||||
return acc;
|
return acc;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
cacheDataListRef.current = [
|
cacheDataListRef.current = [
|
||||||
...newDataList,
|
...newDataList,
|
||||||
...cacheDataListRef.current
|
...cacheDataListRef.current
|
||||||
].slice(0, limit);
|
].slice(0, limit);
|
||||||
|
|
||||||
|
options.onCreate?.(newDataList);
|
||||||
}
|
}
|
||||||
|
|
||||||
// DELETE
|
// DELETE
|
||||||
if (data?.type === WatchEventType.DELETE && events.includes('DELETE')) {
|
if (data?.type === WatchEventType.DELETE && events.includes('DELETE')) {
|
||||||
cacheDataListRef.current = cacheDataListRef.current?.filter(
|
cacheDataListRef.current = cacheDataListRef.current?.filter(
|
||||||
@@ -87,6 +97,7 @@ export function useUpdateChunkedList(options: {
|
|||||||
deletedIds: [...deletedIdsRef.current]
|
deletedIds: [...deletedIdsRef.current]
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// UPDATE
|
// UPDATE
|
||||||
if (data?.type === WatchEventType.UPDATE && events.includes('UPDATE')) {
|
if (data?.type === WatchEventType.UPDATE && events.includes('UPDATE')) {
|
||||||
collections?.forEach((item: any) => {
|
collections?.forEach((item: any) => {
|
||||||
@@ -101,6 +112,7 @@ export function useUpdateChunkedList(options: {
|
|||||||
updateItem,
|
updateItem,
|
||||||
...cacheDataListRef.current.slice(0, limit - 1)
|
...cacheDataListRef.current.slice(0, limit - 1)
|
||||||
];
|
];
|
||||||
|
options.onCreate?.([updateItem]);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
+45
-45
@@ -10,30 +10,38 @@ import {
|
|||||||
DiscordOutlined,
|
DiscordOutlined,
|
||||||
GithubOutlined,
|
GithubOutlined,
|
||||||
HomeOutlined,
|
HomeOutlined,
|
||||||
InfoCircleOutlined,
|
|
||||||
LogoutOutlined,
|
LogoutOutlined,
|
||||||
ReadOutlined,
|
ReadOutlined,
|
||||||
SettingOutlined
|
SettingOutlined
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import { history, useIntl, useNavigate } from '@umijs/max';
|
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 { useAtom } from 'jotai';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import { DEFAULT_ENTER_PAGE } from '../config/settings';
|
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`
|
const NewLabel = styled.span`
|
||||||
position: relative;
|
position: relative;
|
||||||
bottom: 3px;
|
top: -4px;
|
||||||
|
right: 4px;
|
||||||
|
padding: 2px 4px;
|
||||||
display: flex;
|
display: flex;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
width: 36px;
|
|
||||||
height: 15px;
|
height: 15px;
|
||||||
margin-left: 5px;
|
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background-color: var(--ant-orange-5);
|
background-color: var(--ant-orange-5);
|
||||||
border-radius: 8px 8px 8px 0;
|
border-radius: 6px 6px 6px 0;
|
||||||
transform: scale(0.9);
|
transform: scale(0.9);
|
||||||
|
|
||||||
.text {
|
.text {
|
||||||
@@ -117,6 +125,7 @@ export const ExtraContent = (props: { isDarkTheme?: boolean }) => {
|
|||||||
|
|
||||||
const loginPath = DEFAULT_ENTER_PAGE.login;
|
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(() => {
|
const showUpgrade = useMemo(() => {
|
||||||
return (
|
return (
|
||||||
initialState?.currentUser?.is_admin &&
|
initialState?.currentUser?.is_admin &&
|
||||||
@@ -186,11 +195,6 @@ export const ExtraContent = (props: { isDarkTheme?: boolean }) => {
|
|||||||
icon: <ReadOutlined />,
|
icon: <ReadOutlined />,
|
||||||
label: intl.formatMessage({ id: 'common.button.docs' }),
|
label: intl.formatMessage({ id: 'common.button.docs' }),
|
||||||
url: externalLinks.documentation
|
url: externalLinks.documentation
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'version',
|
|
||||||
icon: <InfoCircleOutlined />,
|
|
||||||
label: intl.formatMessage({ id: 'common.button.version' })
|
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -198,36 +202,16 @@ export const ExtraContent = (props: { isDarkTheme?: boolean }) => {
|
|||||||
items: helpList.map((item) => ({
|
items: helpList.map((item) => ({
|
||||||
key: item.key,
|
key: item.key,
|
||||||
label: (
|
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
|
<a
|
||||||
className="m-l-8 "
|
className="flex flex-center gap-8"
|
||||||
href={item.url}
|
href={item.url}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
>
|
>
|
||||||
|
{item.icon}
|
||||||
{item.label}
|
{item.label}
|
||||||
</a>
|
</a>
|
||||||
)}
|
)
|
||||||
</span>
|
|
||||||
),
|
|
||||||
onClick() {
|
|
||||||
if (item.key === 'version') {
|
|
||||||
showVersion();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}))
|
}))
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -300,21 +284,37 @@ export const ExtraContent = (props: { isDarkTheme?: boolean }) => {
|
|||||||
return (
|
return (
|
||||||
<Wrapper>
|
<Wrapper>
|
||||||
{contextHolder}
|
{contextHolder}
|
||||||
<DropDownActions menu={{ ...helpMenu }} popupRender={helpPopupRender}>
|
<div
|
||||||
<IconWrapper>
|
|
||||||
<IconFont type="icon-help" className="font-size-20" />
|
|
||||||
{showUpgrade && (
|
|
||||||
<span
|
|
||||||
className="m-l-5"
|
|
||||||
style={{
|
style={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
width: 8,
|
alignItems: 'center'
|
||||||
height: 8,
|
|
||||||
borderRadius: 4,
|
|
||||||
backgroundColor: 'var(--ant-orange-5)'
|
|
||||||
}}
|
}}
|
||||||
></span>
|
>
|
||||||
|
<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"
|
||||||
|
style={{ color: 'var(--ant-color-text-tertiary)' }}
|
||||||
|
/>
|
||||||
</IconWrapper>
|
</IconWrapper>
|
||||||
</DropDownActions>
|
</DropDownActions>
|
||||||
<DropDownActions menu={{ ...userMenu }} popupRender={userPopupRender}>
|
<DropDownActions menu={{ ...userMenu }} popupRender={userPopupRender}>
|
||||||
|
|||||||
+40
-3
@@ -1,13 +1,14 @@
|
|||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
|
|
||||||
import { routeCacheAtom, setRouteCache } from '@/atoms/route-cache';
|
import { routeCacheAtom, setRouteCache } from '@/atoms/route-cache';
|
||||||
import { userAtom } from '@/atoms/user';
|
import { GPUStackVersionAtom, userAtom } from '@/atoms/user';
|
||||||
import DarkMask from '@/components/dark-mask';
|
import DarkMask from '@/components/dark-mask';
|
||||||
import IconFont from '@/components/icon-font';
|
import IconFont from '@/components/icon-font';
|
||||||
import routeCachekey from '@/config/route-cachekey';
|
import routeCachekey from '@/config/route-cachekey';
|
||||||
import { DEFAULT_ENTER_PAGE } from '@/config/settings';
|
import { DEFAULT_ENTER_PAGE } from '@/config/settings';
|
||||||
import useOverlayScroller from '@/hooks/use-overlay-scroller';
|
import useOverlayScroller from '@/hooks/use-overlay-scroller';
|
||||||
import useUserSettings from '@/hooks/use-user-settings';
|
import useUserSettings from '@/hooks/use-user-settings';
|
||||||
|
import useAddResource from '@/pages/dashboard/hooks/use-add-resource';
|
||||||
import { logout } from '@/pages/login/apis';
|
import { logout } from '@/pages/login/apis';
|
||||||
import { useAccessMarkedRoutes } from '@@/plugin-access';
|
import { useAccessMarkedRoutes } from '@@/plugin-access';
|
||||||
import { useModel } from '@@/plugin-model';
|
import { useModel } from '@@/plugin-model';
|
||||||
@@ -27,7 +28,7 @@ import { Button, ConfigProvider, Modal, theme } from 'antd';
|
|||||||
import 'driver.js/dist/driver.css';
|
import 'driver.js/dist/driver.css';
|
||||||
import { useAtom } from 'jotai';
|
import { useAtom } from 'jotai';
|
||||||
import 'overlayscrollbars/overlayscrollbars.css';
|
import 'overlayscrollbars/overlayscrollbars.css';
|
||||||
import { useEffect, useMemo } from 'react';
|
import { useEffect, useMemo, useRef } from 'react';
|
||||||
import { PageContainerInner } from '../pages/_components/page-box';
|
import { PageContainerInner } from '../pages/_components/page-box';
|
||||||
import Exception from './Exception';
|
import Exception from './Exception';
|
||||||
import './Layout.css';
|
import './Layout.css';
|
||||||
@@ -48,6 +49,14 @@ const NO_CONTAINER_PAGES = [
|
|||||||
'clusterCreate'
|
'clusterCreate'
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const CHECK_RESOURCE_PATH = [
|
||||||
|
'/resources/workers',
|
||||||
|
'/cluster-management/clusters/list',
|
||||||
|
'/cluster-management/credentials',
|
||||||
|
'/cluster-management/clusters/create',
|
||||||
|
''
|
||||||
|
];
|
||||||
|
|
||||||
const loginPath = DEFAULT_ENTER_PAGE.login;
|
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
|
// Filter out the routes that need to be displayed, where filterFn indicates the levels that should not be shown
|
||||||
@@ -112,6 +121,19 @@ export default (props: any) => {
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const { clientRoutes } = useAppData();
|
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')) || {
|
const initialInfo = (useModel && useModel('@@initialState')) || {
|
||||||
initialState: undefined,
|
initialState: undefined,
|
||||||
@@ -236,10 +258,20 @@ export default (props: any) => {
|
|||||||
return <SiderMenu {...menuProps}></SiderMenu>;
|
return <SiderMenu {...menuProps}></SiderMenu>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const onPageChange = (route: any) => {
|
const onPageChange = async (route: any) => {
|
||||||
const { location } = history;
|
const { location } = history;
|
||||||
const { pathname } = location;
|
const { pathname } = location;
|
||||||
|
|
||||||
|
if (
|
||||||
|
!CHECK_RESOURCE_PATH.includes(pathname) &&
|
||||||
|
initialState?.currentUser?.is_admin &&
|
||||||
|
!requestResourceRef.current &&
|
||||||
|
!userSettings.hideAddResourceModal
|
||||||
|
) {
|
||||||
|
requestResourceRef.current = true;
|
||||||
|
await fetchResourceData();
|
||||||
|
}
|
||||||
|
|
||||||
initRouteCacheValue(pathname);
|
initRouteCacheValue(pathname);
|
||||||
dropRouteCache(pathname);
|
dropRouteCache(pathname);
|
||||||
|
|
||||||
@@ -271,6 +303,10 @@ export default (props: any) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const onCollapse = (value: boolean) => {
|
const onCollapse = (value: boolean) => {
|
||||||
|
// only trigger by window resize
|
||||||
|
if (!value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
setUserSettings({
|
setUserSettings({
|
||||||
...userSettings,
|
...userSettings,
|
||||||
collapsed: value
|
collapsed: value
|
||||||
@@ -346,6 +382,7 @@ export default (props: any) => {
|
|||||||
</PageContainerInner>
|
</PageContainerInner>
|
||||||
)}
|
)}
|
||||||
</Exception>
|
</Exception>
|
||||||
|
{NoResourceModal}
|
||||||
</ProLayout>
|
</ProLayout>
|
||||||
{contextHolder}
|
{contextHolder}
|
||||||
</ConfigProvider>
|
</ConfigProvider>
|
||||||
|
|||||||
@@ -164,6 +164,7 @@ const SiderMenu: React.FC<SiderMenuProps> = (props) => {
|
|||||||
key={key}
|
key={key}
|
||||||
>
|
>
|
||||||
<Link
|
<Link
|
||||||
|
prefetch="intent"
|
||||||
to={menuItem.path.replace('/*', '')}
|
to={menuItem.path.replace('/*', '')}
|
||||||
target={menuItem.target}
|
target={menuItem.target}
|
||||||
className={cx(styles.menuItemWrapper, 'menu-item', {
|
className={cx(styles.menuItemWrapper, 'menu-item', {
|
||||||
|
|||||||
@@ -19,8 +19,7 @@ export default {
|
|||||||
'backend.mode.yaml': 'YAML Mode',
|
'backend.mode.yaml': 'YAML Mode',
|
||||||
'backend.form.healthCheckPath': 'Health Check Path',
|
'backend.form.healthCheckPath': 'Health Check Path',
|
||||||
'backend.form.defaultExecuteCommand': 'Default Execution Command',
|
'backend.form.defaultExecuteCommand': 'Default Execution Command',
|
||||||
'backend.form.defaultExecuteCommand.tips':
|
'backend.form.defaultExecuteCommand.tips': `'{{'model_path'}}', '{{'port'}}', '{{'worker_ip'}}' and '{{'model_name'}}' are placeholders that will be substituted with the actual values during deployment.`,
|
||||||
'{{model_path}}, {{port}}, {{worker_ip}} and {{model_name}} are placeholders that will be substituted with the actual values during deployment.',
|
|
||||||
'backend.form.defaultBackendParameters': 'Default Backend Parameters',
|
'backend.form.defaultBackendParameters': 'Default Backend Parameters',
|
||||||
'backend.form.versionConfig': 'Versions Config',
|
'backend.form.versionConfig': 'Versions Config',
|
||||||
'backend.form.addParameter': 'Add Parameter',
|
'backend.form.addParameter': 'Add Parameter',
|
||||||
@@ -32,5 +31,11 @@ export default {
|
|||||||
'The built-in backend’s custom version name must end with "-custom".',
|
'The built-in backend’s custom version name must end with "-custom".',
|
||||||
'backend.backend.rules.custom':
|
'backend.backend.rules.custom':
|
||||||
'The custom backend name must end with "-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.'
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ export default {
|
|||||||
'clusters.create.configBasic': 'Basic Configuration',
|
'clusters.create.configBasic': 'Basic Configuration',
|
||||||
'clusters.create.execCommand': 'Execute Command',
|
'clusters.create.execCommand': 'Execute Command',
|
||||||
'clusters.create.supportedGpu': 'Supported GPUs',
|
'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.noImages': 'No images available',
|
||||||
'clusters.create.noInstanceTypes': 'No instance types available',
|
'clusters.create.noInstanceTypes': 'No instance types available',
|
||||||
'clusters.create.noRegions': 'No regions available',
|
'clusters.create.noRegions': 'No regions available',
|
||||||
@@ -51,8 +51,7 @@ export default {
|
|||||||
'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>.',
|
'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>.',
|
||||||
'clusters.addworker.nvidiaNotes-02':
|
'clusters.addworker.nvidiaNotes-02':
|
||||||
'If a model directory already exists on the worker, you can specify the path to mount it.',
|
'If a model directory already exists on the worker, you can specify the path to mount it.',
|
||||||
'clusters.addworker.hygonNotes': `If the <span class="bold-text">/opt/hyhal</span> directory does not exist, create a symbolic link to the Hygon installation path.
|
'clusters.addworker.hygonNotes': `If <span class="bold-text">/opt/hyhal</span> or <span class="bold-text">/opt/dtk</span> does not exist, create symbolic links pointing to the corresponding Hygon installation paths, for example: <span class="desc-fill">ln -s /path/to/hyhal /opt/hyhal</span> <span class="desc-fill">ln -s /path/to/dtk /opt/dtk</span>.`,
|
||||||
Same applies to the <span class="bold-text">/opt/dtk</span> directory.`,
|
|
||||||
'clusters.addworker.corexNotes': `If the <span class="bold-text">/lib/modules</span> directory does not exist, create a symbolic link to the Iluvatar installation path:
|
'clusters.addworker.corexNotes': `If the <span class="bold-text">/lib/modules</span> directory does not exist, create a symbolic link to the Iluvatar installation path:
|
||||||
<span class="bold-text">ln -s /path/to/corex /lib/modules</span>. Same applies to the <span class="bold-text">/usr/local/corex</span> directory.`,
|
<span class="bold-text">ln -s /path/to/corex /lib/modules</span>. Same applies to the <span class="bold-text">/usr/local/corex</span> directory.`,
|
||||||
'clusters.addworker.metaxNotes': `If the <span class="bold-text">/opt/mxdriver</span> directory does not exist, create a symbolic link to the MetaX installation path:
|
'clusters.addworker.metaxNotes': `If the <span class="bold-text">/opt/mxdriver</span> directory does not exist, create a symbolic link to the MetaX installation path:
|
||||||
@@ -68,20 +67,53 @@ Same applies to the <span class="bold-text">/opt/dtk</span> directory.`,
|
|||||||
'clusters.addworker.checkEnv': 'Check Environment',
|
'clusters.addworker.checkEnv': 'Check Environment',
|
||||||
'clusters.addworker.specifyArgs': 'Specify Arguments',
|
'clusters.addworker.specifyArgs': 'Specify Arguments',
|
||||||
'clusters.addworker.runCommand': 'Run Command',
|
'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.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': 'Enter worker IP',
|
||||||
'clusters.addworker.enterWorkerIP.error': 'Please enter the 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.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.configSummary': 'Configuration Summary',
|
||||||
'clusters.addworker.gpuVendor': 'GPU Vendor',
|
'clusters.addworker.gpuVendor': 'GPU Vendor',
|
||||||
'clusters.addworker.workerIP': 'Worker IP',
|
'clusters.addworker.workerIP': 'Worker IP',
|
||||||
|
'clusters.addworker.workerExternalIP': 'Worker External Address',
|
||||||
'clusters.addworker.notSpecified': 'Not Specified',
|
'clusters.addworker.notSpecified': 'Not Specified',
|
||||||
'clusters.addworker.autoDetect': 'Auto-detect',
|
'clusters.addworker.autoDetect': 'Auto',
|
||||||
'clusters.addworker.extraVolume.holder':
|
'clusters.addworker.extraVolume.holder':
|
||||||
'e.g. /data/models (path must start with /)',
|
'e.g. /data/models (path must start with /)',
|
||||||
|
'clusters.addworker.cacheVolume.holder':
|
||||||
|
'e.g. /data/cache (path must start with /)',
|
||||||
'clusters.addworker.vendorNotes.title': 'Notes for {vendor} Device',
|
'clusters.addworker.vendorNotes.title': 'Notes for {vendor} Device',
|
||||||
'clusters.button.genToken':
|
'clusters.button.genToken':
|
||||||
'Need to create a new token? Click <a href="{link}" target="_blank">here</a>.',
|
'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.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.'
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -247,7 +247,7 @@ export default {
|
|||||||
'common.appearance.theme': 'Theme',
|
'common.appearance.theme': 'Theme',
|
||||||
'common.page.wentwrong': 'Something went wrong.',
|
'common.page.wentwrong': 'Something went wrong.',
|
||||||
'common.page.refresh.tips':
|
'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':
|
'common.tips.escape.disable':
|
||||||
'Click Cancel or the X at the top right to close.',
|
'Click Cancel or the X at the top right to close.',
|
||||||
'common.button.clearSelection': 'Clear Selection',
|
'common.button.clearSelection': 'Clear Selection',
|
||||||
@@ -265,5 +265,9 @@ export default {
|
|||||||
'common.filter.status': 'Filter by status',
|
'common.filter.status': 'Filter by status',
|
||||||
'common.form.rule.selectInput': 'Please select or enter a {name}',
|
'common.form.rule.selectInput': 'Please select or enter a {name}',
|
||||||
'common.tag.experimental': 'Experimental',
|
'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'
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -216,8 +216,8 @@ export default {
|
|||||||
'models.form.gpusAllocationType.custom.tips':
|
'models.form.gpusAllocationType.custom.tips':
|
||||||
'You can specify the exact number of GPUs per replica.',
|
'You can specify the exact number of GPUs per replica.',
|
||||||
'models.mymodels.status.inactive': 'Stopped',
|
'models.mymodels.status.inactive': 'Stopped',
|
||||||
'models.mymodels.status.degrade': 'Abnormal',
|
'models.mymodels.status.degrade': 'Not Ready',
|
||||||
'models.mymodels.status.active': 'Active',
|
'models.mymodels.status.active': 'Ready',
|
||||||
'models.form.kvCache.tips':
|
'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.',
|
'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':
|
'models.form.kvCache.tips2':
|
||||||
@@ -268,5 +268,9 @@ export default {
|
|||||||
'Accessible to all authenticated platform users.',
|
'Accessible to all authenticated platform users.',
|
||||||
'models.accessSettings.allowedUsers.tips':
|
'models.accessSettings.allowedUsers.tips':
|
||||||
'Only designated users can access the model.',
|
'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.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'
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export default {
|
|||||||
'noresult.workers.title': 'No Workers',
|
'noresult.workers.title': 'No Workers',
|
||||||
'noresult.workers.subTitle': 'No workers found in any clusters.',
|
'noresult.workers.subTitle': 'No workers found in any clusters.',
|
||||||
'noresult.workers.nofound': 'No matching workers found.',
|
'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.title': 'No Model Files',
|
||||||
'noresult.modelfiles.subTitle': 'No model files have been added yet.',
|
'noresult.modelfiles.subTitle': 'No model files have been added yet.',
|
||||||
'noresult.modelfiles.nofound': 'No matching model files found.',
|
'noresult.modelfiles.nofound': 'No matching model files found.',
|
||||||
|
|||||||
@@ -19,12 +19,12 @@ export default {
|
|||||||
'users.form.currentpassword': 'Current Password',
|
'users.form.currentpassword': 'Current Password',
|
||||||
'users.form.updatepassword': 'Modify Password',
|
'users.form.updatepassword': 'Modify Password',
|
||||||
'users.form.rule.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.uppcase': 'At least one uppercase letter',
|
||||||
'users.password.lowercase': 'At least one lowercase letter',
|
'users.password.lowercase': 'At least one lowercase letter',
|
||||||
'users.password.number': 'At least one number',
|
'users.password.number': 'At least one number',
|
||||||
'users.password.special': 'At least one special character',
|
'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.title': 'Modify Password',
|
||||||
'users.password.modify.description':
|
'users.password.modify.description':
|
||||||
"For your account's security, please change your initial password.",
|
"For your account's security, please change your initial password.",
|
||||||
@@ -37,5 +37,7 @@ export default {
|
|||||||
'users.settings.title': 'User Settings',
|
'users.settings.title': 'User Settings',
|
||||||
'users.status.activate': 'Activate Account',
|
'users.status.activate': 'Activate Account',
|
||||||
'users.status.deactivate': 'Deactivate 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.'
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -19,8 +19,7 @@ export default {
|
|||||||
'backend.mode.yaml': 'YAML Mode',
|
'backend.mode.yaml': 'YAML Mode',
|
||||||
'backend.form.healthCheckPath': 'Health Check Path',
|
'backend.form.healthCheckPath': 'Health Check Path',
|
||||||
'backend.form.defaultExecuteCommand': 'Default Execution Command',
|
'backend.form.defaultExecuteCommand': 'Default Execution Command',
|
||||||
'backend.form.defaultExecuteCommand.tips':
|
'backend.form.defaultExecuteCommand.tips': `'{{'model_path'}}', '{{'port'}}', '{{'worker_ip'}}' and '{{'model_name'}}' are placeholders that will be substituted with the actual values during deployment.`,
|
||||||
'{{model_path}}, {{port}}, {{worker_ip}} and {{model_name}} are placeholders that will be substituted with the actual values during deployment.',
|
|
||||||
'backend.form.defaultBackendParameters': 'Default Backend Parameters',
|
'backend.form.defaultBackendParameters': 'Default Backend Parameters',
|
||||||
'backend.form.versionConfig': 'Versions Config',
|
'backend.form.versionConfig': 'Versions Config',
|
||||||
'backend.form.addParameter': 'Add Parameter',
|
'backend.form.addParameter': 'Add Parameter',
|
||||||
@@ -32,5 +31,11 @@ export default {
|
|||||||
'The built-in backend’s custom version name must end with "-custom".',
|
'The built-in backend’s custom version name must end with "-custom".',
|
||||||
'backend.backend.rules.custom':
|
'backend.backend.rules.custom':
|
||||||
'The custom backend name must end with "-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.'
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ export default {
|
|||||||
'clusters.create.configBasic': 'Basic Configuration',
|
'clusters.create.configBasic': 'Basic Configuration',
|
||||||
'clusters.create.execCommand': 'Execute Command',
|
'clusters.create.execCommand': 'Execute Command',
|
||||||
'clusters.create.supportedGpu': 'Supported GPUs',
|
'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.noImages': 'No images available',
|
||||||
'clusters.create.noInstanceTypes': 'No instance types available',
|
'clusters.create.noInstanceTypes': 'No instance types available',
|
||||||
'clusters.create.noRegions': 'No regions available',
|
'clusters.create.noRegions': 'No regions available',
|
||||||
@@ -51,8 +51,7 @@ export default {
|
|||||||
'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>.',
|
'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>.',
|
||||||
'clusters.addworker.nvidiaNotes-02':
|
'clusters.addworker.nvidiaNotes-02':
|
||||||
'If a model directory already exists on the worker, you can specify the path to mount it.',
|
'If a model directory already exists on the worker, you can specify the path to mount it.',
|
||||||
'clusters.addworker.hygonNotes': `If the <span class="bold-text">/opt/hyhal</span> directory does not exist, create a symbolic link to the Hygon installation path.
|
'clusters.addworker.hygonNotes': `If <span class="bold-text">/opt/hyhal</span> or <span class="bold-text">/opt/dtk</span> does not exist, create symbolic links pointing to the corresponding Hygon installation paths, for example: <span class="desc-fill">ln -s /path/to/hyhal /opt/hyhal</span> <span class="desc-fill">ln -s /path/to/dtk /opt/dtk</span>.`,
|
||||||
Same applies to the <span class="bold-text">/opt/dtk</span> directory.`,
|
|
||||||
'clusters.addworker.corexNotes': `If the <span class="bold-text">/lib/modules</span> directory does not exist, create a symbolic link to the Iluvatar installation path:
|
'clusters.addworker.corexNotes': `If the <span class="bold-text">/lib/modules</span> directory does not exist, create a symbolic link to the Iluvatar installation path:
|
||||||
<span class="bold-text">ln -s /path/to/corex /lib/modules</span>. Same applies to the <span class="bold-text">/usr/local/corex</span> directory.`,
|
<span class="bold-text">ln -s /path/to/corex /lib/modules</span>. Same applies to the <span class="bold-text">/usr/local/corex</span> directory.`,
|
||||||
'clusters.addworker.metaxNotes': `If the <span class="bold-text">/opt/mxdriver</span> directory does not exist, create a symbolic link to the MetaX installation path:
|
'clusters.addworker.metaxNotes': `If the <span class="bold-text">/opt/mxdriver</span> directory does not exist, create a symbolic link to the MetaX installation path:
|
||||||
@@ -68,22 +67,55 @@ Same applies to the <span class="bold-text">/opt/dtk</span> directory.`,
|
|||||||
'clusters.addworker.checkEnv': 'Check Environment',
|
'clusters.addworker.checkEnv': 'Check Environment',
|
||||||
'clusters.addworker.specifyArgs': 'Specify Arguments',
|
'clusters.addworker.specifyArgs': 'Specify Arguments',
|
||||||
'clusters.addworker.runCommand': 'Run Command',
|
'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.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': 'Enter worker IP',
|
||||||
'clusters.addworker.enterWorkerIP.error': 'Please enter the 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.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.configSummary': 'Configuration Summary',
|
||||||
'clusters.addworker.gpuVendor': 'GPU Vendor',
|
'clusters.addworker.gpuVendor': 'GPU Vendor',
|
||||||
'clusters.addworker.workerIP': 'Worker IP',
|
'clusters.addworker.workerIP': 'Worker IP',
|
||||||
|
'clusters.addworker.workerExternalIP': 'Worker External Address',
|
||||||
'clusters.addworker.notSpecified': 'Not Specified',
|
'clusters.addworker.notSpecified': 'Not Specified',
|
||||||
'clusters.addworker.autoDetect': 'Auto-detect',
|
'clusters.addworker.autoDetect': 'Auto',
|
||||||
'clusters.addworker.extraVolume.holder':
|
'clusters.addworker.extraVolume.holder':
|
||||||
'e.g. /data/models (path must start with /)',
|
'e.g. /data/models (path must start with /)',
|
||||||
|
'clusters.addworker.cacheVolume.holder':
|
||||||
|
'e.g. /data/cache (path must start with /)',
|
||||||
'clusters.addworker.vendorNotes.title': 'Notes for {vendor} Device',
|
'clusters.addworker.vendorNotes.title': 'Notes for {vendor} Device',
|
||||||
'clusters.button.genToken':
|
'clusters.button.genToken':
|
||||||
'Need to create a new token? Click <a href="{link}" target="_blank">here</a>.',
|
'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.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) ==========
|
// ========== To-Do: Translate Keys (Remove After Translation) ==========
|
||||||
@@ -120,7 +152,7 @@ Same applies to the <span class="bold-text">/opt/dtk</span> directory.`,
|
|||||||
// 31. 'clusters.create.configBasic': 'Basic Configuration',
|
// 31. 'clusters.create.configBasic': 'Basic Configuration',
|
||||||
// 32. 'clusters.create.execCommand': 'Execute Command',
|
// 32. 'clusters.create.execCommand': 'Execute Command',
|
||||||
// 33. 'clusters.create.supportedGpu': 'Supported GPUs',
|
// 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',
|
// 35. 'clusters.create.noImages': 'No images available',
|
||||||
// 36. 'clusters.create.noInstanceTypes': 'No instance types available',
|
// 36. 'clusters.create.noInstanceTypes': 'No instance types available',
|
||||||
// 37. 'clusters.create.noRegions': 'No regions available',
|
// 37. 'clusters.create.noRegions': 'No regions available',
|
||||||
@@ -132,7 +164,7 @@ Same applies to the <span class="bold-text">/opt/dtk</span> directory.`,
|
|||||||
// 43. 'cluster.provider.comingsoon': 'Coming soon',
|
// 43. 'cluster.provider.comingsoon': 'Coming soon',
|
||||||
// 44. '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>.',
|
// 44. '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>.',
|
||||||
// 45. 'clusters.addworker.nvidiaNotes-02': 'If a model directory already exists on the worker, you can specify the path to mount it.',
|
// 45. 'clusters.addworker.nvidiaNotes-02': 'If a model directory already exists on the worker, you can specify the path to mount it.',
|
||||||
// 46. 'clusters.addworker.hygonNotes': 'If the <span class="bold-text">/opt/hyhal</span> directory does not exist, create a symbolic link to the Hygon installation path. Same applies to the <span class="bold-text">/opt/dtk</span> directory.',
|
// 46. 'clusters.addworker.hygonNotes': `If <span class="bold-text">/opt/hyhal</span> or <span class="bold-text">/opt/dtk</span> does not exist, create symbolic links pointing to the corresponding Hygon installation paths, for example: <span class="desc-fill">ln -s /path/to/hyhal /opt/hyhal</span> <span class="desc-fill">ln -s /path/to/dtk /opt/dtk</span>.`,
|
||||||
// 47. 'clusters.addworker.corexNotes': 'If the <span class="bold-text">/lib/modules</span> directory does not exist, create a symbolic link to the Iluvatar installation path: <span class="bold-text">ln -s /path/to/corex /lib/modules</span>. Same applies to the <span class="bold-text">/usr/local/corex</span> directory.',
|
// 47. 'clusters.addworker.corexNotes': 'If the <span class="bold-text">/lib/modules</span> directory does not exist, create a symbolic link to the Iluvatar installation path: <span class="bold-text">ln -s /path/to/corex /lib/modules</span>. Same applies to the <span class="bold-text">/usr/local/corex</span> directory.',
|
||||||
// 48. 'clusters.addworker.metaxNotes': 'If the <span class="bold-text">/opt/mxdriver</span> directory does not exist, create a symbolic link to the MetaX installation path: <span class="bold-text">ln -s /path/to/metax /opt/mxdriver</span>. Same applies to the <span class="bold-text">/opt/maca</span> directory.',
|
// 48. 'clusters.addworker.metaxNotes': 'If the <span class="bold-text">/opt/mxdriver</span> directory does not exist, create a symbolic link to the MetaX installation path: <span class="bold-text">ln -s /path/to/metax /opt/mxdriver</span>. Same applies to the <span class="bold-text">/opt/maca</span> directory.',
|
||||||
// 49. 'clusters.addworker.cambriconNotes': 'If the <span class="bold-text">/usr/local/neuware</span> directory does not exist, create a symbolic link to the Cambricon installation path: <span class="bold-text">ln -s /path/to/neuware /usr/local/neuware</span>.'
|
// 49. 'clusters.addworker.cambriconNotes': 'If the <span class="bold-text">/usr/local/neuware</span> directory does not exist, create a symbolic link to the Cambricon installation path: <span class="bold-text">ln -s /path/to/neuware /usr/local/neuware</span>.'
|
||||||
@@ -143,7 +175,7 @@ Same applies to the <span class="bold-text">/opt/dtk</span> directory.`,
|
|||||||
// 54. 'clusters.addworker.checkEnv': 'Check Environment',
|
// 54. 'clusters.addworker.checkEnv': 'Check Environment',
|
||||||
// 55. 'clusters.addworker.specifyArgs': 'Specify Arguments',
|
// 55. 'clusters.addworker.specifyArgs': 'Specify Arguments',
|
||||||
// 56. 'clusters.addworker.runCommand': 'Run Command',
|
// 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',
|
// 58. 'clusters.addworker.detectWorkerIP': 'Auto-detect Worker IP',
|
||||||
// 59. 'clusters.addworker.enterWorkerIP': 'Enter worker IP',
|
// 59. 'clusters.addworker.enterWorkerIP': 'Enter worker IP',
|
||||||
// 60. 'clusters.addworker.enterWorkerIP.error': 'Please enter the worker IP.',
|
// 60. 'clusters.addworker.enterWorkerIP.error': 'Please enter the worker IP.',
|
||||||
@@ -151,10 +183,30 @@ Same applies to the <span class="bold-text">/opt/dtk</span> directory.`,
|
|||||||
// 62. 'clusters.addworker.configSummary': 'Configuration Summary',
|
// 62. 'clusters.addworker.configSummary': 'Configuration Summary',
|
||||||
// 63. 'clusters.addworker.gpuVendor': 'GPU Vendor',
|
// 63. 'clusters.addworker.gpuVendor': 'GPU Vendor',
|
||||||
// 64. 'clusters.addworker.workerIP': 'Worker IP',
|
// 64. 'clusters.addworker.workerIP': 'Worker IP',
|
||||||
|
// 65. 'clusters.addworker.workerExternalIP': 'Worker External Address',
|
||||||
// 65. 'clusters.addworker.notSpecified': 'Not Specified',
|
// 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 /)'
|
// 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>.',
|
// 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>.`
|
// 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 ==========
|
// ========== End of To-Do List ==========
|
||||||
|
|||||||
@@ -247,7 +247,7 @@ export default {
|
|||||||
'common.appearance.theme': 'Theme',
|
'common.appearance.theme': 'Theme',
|
||||||
'common.page.wentwrong': 'Something went wrong.',
|
'common.page.wentwrong': 'Something went wrong.',
|
||||||
'common.page.refresh.tips':
|
'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':
|
'common.tips.escape.disable':
|
||||||
'Click Cancel or the X at the top right to close.',
|
'Click Cancel or the X at the top right to close.',
|
||||||
'common.button.clearSelection': 'Clear Selection',
|
'common.button.clearSelection': 'Clear Selection',
|
||||||
@@ -265,7 +265,11 @@ export default {
|
|||||||
'common.filter.status': 'Filter by status',
|
'common.filter.status': 'Filter by status',
|
||||||
'common.form.rule.selectInput': 'Please select or enter a {name}',
|
'common.form.rule.selectInput': 'Please select or enter a {name}',
|
||||||
'common.tag.experimental': 'Experimental',
|
'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) ==========
|
// ========== To-Do: Translate Keys (Remove After Translation) ==========
|
||||||
@@ -280,7 +284,7 @@ export default {
|
|||||||
// 9. 'common.button.forgotpassword': 'Forgot Password?'
|
// 9. 'common.button.forgotpassword': 'Forgot Password?'
|
||||||
// 10. 'common.appearance.theme': 'Theme',
|
// 10. 'common.appearance.theme': 'Theme',
|
||||||
// 11. 'common.page.wentwrong': 'Something went wrong.',
|
// 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.'
|
// 13. 'common.tips.escape.disable': 'Click Cancel or the X at the top right to close.'
|
||||||
// 14. 'common.button.clearSelection': 'Clear Selection',
|
// 14. 'common.button.clearSelection': 'Clear Selection',
|
||||||
// 15. 'common.select.count': '{count} selected',
|
// 15. 'common.select.count': '{count} selected',
|
||||||
@@ -296,5 +300,9 @@ export default {
|
|||||||
// 25. 'common.filter.status': 'Filter by status'
|
// 25. 'common.filter.status': 'Filter by status'
|
||||||
// 26. 'common.form.rule.selectInput': 'Please select or enter a {name}',
|
// 26. 'common.form.rule.selectInput': 'Please select or enter a {name}',
|
||||||
// 27. 'common.tag.experimental': 'Experimental',
|
// 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 ==========
|
// ========== End of To-Do List ==========
|
||||||
|
|||||||
@@ -216,8 +216,8 @@ export default {
|
|||||||
'models.form.gpusAllocationType.custom.tips':
|
'models.form.gpusAllocationType.custom.tips':
|
||||||
'You can specify the exact number of GPUs per replica.',
|
'You can specify the exact number of GPUs per replica.',
|
||||||
'models.mymodels.status.inactive': 'Stopped',
|
'models.mymodels.status.inactive': 'Stopped',
|
||||||
'models.mymodels.status.degrade': 'Abnormal',
|
'models.mymodels.status.degrade': 'Not Ready',
|
||||||
'models.mymodels.status.active': 'Active',
|
'models.mymodels.status.active': 'Ready',
|
||||||
'models.form.kvCache.tips':
|
'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.',
|
'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':
|
'models.form.kvCache.tips2':
|
||||||
@@ -268,7 +268,11 @@ export default {
|
|||||||
'Accessible to all authenticated platform users.',
|
'Accessible to all authenticated platform users.',
|
||||||
'models.accessSettings.allowedUsers.tips':
|
'models.accessSettings.allowedUsers.tips':
|
||||||
'Only designated users can access the model.',
|
'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.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) ==========
|
// ========== To-Do: Translate Keys (Remove After Translation) ==========
|
||||||
@@ -310,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.',
|
// 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.',
|
// 38. 'models.form.gpusAllocationType.custom.tips': 'You can specify the exact number of GPUs per replica.',
|
||||||
// 39. 'models.mymodels.status.inactive': 'Stopped',
|
// 39. 'models.mymodels.status.inactive': 'Stopped',
|
||||||
// 41. 'models.mymodels.status.degrade': 'Abnormal',
|
// 41. 'models.mymodels.status.degrade': 'Not Ready',
|
||||||
// 42. 'models.mymodels.status.active': 'Active'
|
// 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.',
|
// 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.',
|
// 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).',
|
// 45. 'models.form.kvCache.tips2': 'Only supported when using built-in inference backends (vLLM or SGLang).',
|
||||||
@@ -360,5 +364,8 @@ export default {
|
|||||||
// 69. 'models.accessSettings.public.desc': 'Accessible to anyone without authentication.',
|
// 69. 'models.accessSettings.public.desc': 'Accessible to anyone without authentication.',
|
||||||
// 70. 'models.accessSettings.authed.tips': 'Accessible to all authenticated platform users.',
|
// 70. 'models.accessSettings.authed.tips': 'Accessible to all authenticated platform users.',
|
||||||
// 71.'models.accessSettings.allowedUsers.tips': 'Only designated users can access the model.',
|
// 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.`
|
// 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 ==========
|
// ========== End of To-Do List ==========
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export default {
|
|||||||
'noresult.workers.title': 'No Workers',
|
'noresult.workers.title': 'No Workers',
|
||||||
'noresult.workers.subTitle': 'No workers found in any clusters.',
|
'noresult.workers.subTitle': 'No workers found in any clusters.',
|
||||||
'noresult.workers.nofound': 'No matching workers found.',
|
'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.title': 'No Model Files',
|
||||||
'noresult.modelfiles.subTitle': 'No model files have been added yet.',
|
'noresult.modelfiles.subTitle': 'No model files have been added yet.',
|
||||||
'noresult.modelfiles.nofound': 'No matching model files found.',
|
'noresult.modelfiles.nofound': 'No matching model files found.',
|
||||||
|
|||||||
@@ -20,12 +20,12 @@ export default {
|
|||||||
'users.form.currentpassword': '現在のパスワード',
|
'users.form.currentpassword': '現在のパスワード',
|
||||||
'users.form.updatepassword': 'パスワードを変更',
|
'users.form.updatepassword': 'パスワードを変更',
|
||||||
'users.form.rule.password':
|
'users.form.rule.password':
|
||||||
'大文字と小文字、数字、特殊文字(!@#$%^&*_+)を含む6〜12文字で、スペースは使用できません。',
|
'大文字と小文字、数字、特殊文字(!@#$%^&*_+)を含む6〜64文字で、スペースは使用できません。',
|
||||||
'users.password.uppcase': '少なくとも1つの大文字を含む',
|
'users.password.uppcase': '少なくとも1つの大文字を含む',
|
||||||
'users.password.lowercase': '少なくとも1つの小文字を含む',
|
'users.password.lowercase': '少なくとも1つの小文字を含む',
|
||||||
'users.password.number': '少なくとも1つの数字を含む',
|
'users.password.number': '少なくとも1つの数字を含む',
|
||||||
'users.password.special': '少なくとも1つの特殊文字を含む',
|
'users.password.special': '少なくとも1つの特殊文字を含む',
|
||||||
'users.password.length': '6〜12文字の長さ',
|
'users.password.length': '6〜64文字の長さ',
|
||||||
'users.password.modify.title': 'パスワードを変更',
|
'users.password.modify.title': 'パスワードを変更',
|
||||||
'users.password.modify.description':
|
'users.password.modify.description':
|
||||||
'アカウントのセキュリティのため、初期パスワードを変更してください。',
|
'アカウントのセキュリティのため、初期パスワードを変更してください。',
|
||||||
@@ -38,12 +38,15 @@ export default {
|
|||||||
'users.settings.title': 'User Settings',
|
'users.settings.title': 'User Settings',
|
||||||
'users.status.activate': 'Activate Account',
|
'users.status.activate': 'Activate Account',
|
||||||
'users.status.deactivate': 'Deactivate 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) ==========
|
// ========== To-Do: Translate Keys (Remove After Translation) ==========
|
||||||
// 1. 'users.settings.title': 'User Settings',
|
// 1. 'users.settings.title': 'User Settings',
|
||||||
// 2. 'users.status.activate': 'Activate Account',
|
// 2. 'users.status.activate': 'Activate Account',
|
||||||
// 3. 'users.status.deactivate': 'Deactivate 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 ==========
|
// ========== End of To-Do List ==========
|
||||||
|
|||||||
@@ -19,8 +19,7 @@ export default {
|
|||||||
'backend.mode.yaml': 'Режим YAML',
|
'backend.mode.yaml': 'Режим YAML',
|
||||||
'backend.form.healthCheckPath': 'Путь проверки здоровья',
|
'backend.form.healthCheckPath': 'Путь проверки здоровья',
|
||||||
'backend.form.defaultExecuteCommand': 'Команда выполнения по умолчанию',
|
'backend.form.defaultExecuteCommand': 'Команда выполнения по умолчанию',
|
||||||
'backend.form.defaultExecuteCommand.tips':
|
'backend.form.defaultExecuteCommand.tips': `'{{'model_path'}}', '{{'port'}}', '{{'worker_ip'}}' и '{{'model_name'}}' заполняются реальными значениями во время запуска`,
|
||||||
'{{model_path}}, {{port}}, {{worker_ip}} и {{model_name}} заполняются реальными значениями во время запуска',
|
|
||||||
'backend.form.defaultBackendParameters': 'Параметры бэкенда по умолчанию',
|
'backend.form.defaultBackendParameters': 'Параметры бэкенда по умолчанию',
|
||||||
'backend.form.versionConfig': 'Конфигурация версий',
|
'backend.form.versionConfig': 'Конфигурация версий',
|
||||||
'backend.form.addParameter': 'Добавить параметр',
|
'backend.form.addParameter': 'Добавить параметр',
|
||||||
@@ -32,9 +31,19 @@ export default {
|
|||||||
'Пользовательское название версии встроенного бэкенда должно оканчиваться на «-custom».',
|
'Пользовательское название версии встроенного бэкенда должно оканчиваться на «-custom».',
|
||||||
'backend.backend.rules.custom':
|
'backend.backend.rules.custom':
|
||||||
'Пользовательское название бэкенда должно оканчиваться на «-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) ==========
|
// ========== 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 ==========
|
// ========== End of To-Do List ==========
|
||||||
|
|||||||
@@ -51,8 +51,7 @@ export default {
|
|||||||
'Если существует несколько исходящих IP-адресов, укажите тот, который должен использовать воркер. Пожалуйста, перепроверьте с помощью <span class="bold-text">hostname -I | xargs -n1</span>.',
|
'Если существует несколько исходящих IP-адресов, укажите тот, который должен использовать воркер. Пожалуйста, перепроверьте с помощью <span class="bold-text">hostname -I | xargs -n1</span>.',
|
||||||
'clusters.addworker.nvidiaNotes-02':
|
'clusters.addworker.nvidiaNotes-02':
|
||||||
'Если директория с моделями уже существует на воркере, вы можете указать путь для её монтирования.',
|
'Если директория с моделями уже существует на воркере, вы можете указать путь для её монтирования.',
|
||||||
'clusters.addworker.hygonNotes':
|
'clusters.addworker.hygonNotes': `If <span class="bold-text">/opt/hyhal</span> or <span class="bold-text">/opt/dtk</span> does not exist, create symbolic links pointing to the corresponding Hygon installation paths, for example: <span class="desc-fill">ln -s /path/to/hyhal /opt/hyhal</span> <span class="desc-fill">ln -s /path/to/dtk /opt/dtk</span>.`,
|
||||||
'Если директория <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':
|
'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> не существует, создайте символическую ссылку на путь установки Iluvatar: <span class="bold-text">ln -s /path/to/corex /lib/modules</span>. Аналогично для директории <span class="bold-text">/usr/local/corex</span>.',
|
||||||
'clusters.addworker.metaxNotes':
|
'clusters.addworker.metaxNotes':
|
||||||
@@ -69,24 +68,57 @@ export default {
|
|||||||
'clusters.addworker.specifyArgs': 'Указать аргументы',
|
'clusters.addworker.specifyArgs': 'Указать аргументы',
|
||||||
'clusters.addworker.runCommand': 'Выполнить команду',
|
'clusters.addworker.runCommand': 'Выполнить команду',
|
||||||
'clusters.addworker.specifyWorkerIP': 'Указать IP воркера',
|
'clusters.addworker.specifyWorkerIP': 'Указать IP воркера',
|
||||||
'clusters.addworker.detectWorkerIP': 'Автоопределение 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': 'Введите IP воркера',
|
||||||
'clusters.addworker.enterWorkerIP.error': 'Пожалуйста, введите 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.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.configSummary': 'Сводка конфигурации',
|
||||||
'clusters.addworker.gpuVendor': 'Производитель GPU',
|
'clusters.addworker.gpuVendor': 'Производитель GPU',
|
||||||
'clusters.addworker.workerIP': 'IP воркера',
|
'clusters.addworker.workerIP': 'IP воркера',
|
||||||
|
'clusters.addworker.workerExternalIP': 'Внешний адрес воркера',
|
||||||
'clusters.addworker.notSpecified': 'Не указано',
|
'clusters.addworker.notSpecified': 'Не указано',
|
||||||
'clusters.addworker.autoDetect': 'Автоопределение',
|
'clusters.addworker.autoDetect': 'Автоопределение',
|
||||||
'clusters.addworker.extraVolume.holder':
|
'clusters.addworker.extraVolume.holder':
|
||||||
'напр. /data/models (путь должен начинаться с /)',
|
'напр. /data/models (путь должен начинаться с /)',
|
||||||
|
'clusters.addworker.cacheVolume.holder':
|
||||||
|
'напр. /data/cache (путь должен начинаться с /)',
|
||||||
'clusters.addworker.vendorNotes.title': 'Примечания для устройств {vendor}',
|
'clusters.addworker.vendorNotes.title': 'Примечания для устройств {vendor}',
|
||||||
'clusters.button.genToken':
|
'clusters.button.genToken':
|
||||||
'Need to create a new token? Click <a href="{link}" target="_blank">here</a>.',
|
'Нужен новый токен? Нажмите <a href="{link}" target="_blank">здесь</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.amdNotes-01': `Если директория <span class="bold-text">/opt/rocm</span> не существует, создайте символическую ссылку на путь установки ROCm: <span class="bold-text">ln -s /путь/к/rocm /opt/rocm</span>.`,
|
||||||
|
'clusters.addworker.message.success_single':
|
||||||
|
'{count} новый воркер был добавлен в кластер.',
|
||||||
|
'clusters.addworker.message.success_multiple':
|
||||||
|
'{count} новых воркеров были добавлены в кластер.',
|
||||||
|
'clusters.create.serverUrl': 'URL сервера',
|
||||||
|
'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':
|
||||||
|
'Укажите URL сервера, доступный из вашего облачного провайдера.',
|
||||||
|
'clusters.form.setDefault': 'Установить по умолчанию',
|
||||||
|
'clusters.form.setDefault.tips':
|
||||||
|
'Использовать по умолчанию для развертывания.'
|
||||||
};
|
};
|
||||||
|
|
||||||
// ========== To-Do: Translate Keys (Remove After Translation) ==========
|
// ========== To-Do: Translate Keys (Remove After Translation) ==========
|
||||||
// 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>.`,
|
// 1. 'clusters.addworker.hygonNotes': `If <span class="bold-text">/opt/hyhal</span> or <span class="bold-text">/opt/dtk</span> does not exist, create symbolic links pointing to the corresponding Hygon installation paths, for example: <span class="desc-fill">ln -s /path/to/hyhal /opt/hyhal</span> <span class="desc-fill">ln -s /path/to/dtk /opt/dtk</span>.`,
|
||||||
// 2. 'clusters.button.genToken': 'Need to create a new token? Click <a href="{link}" target="_blank">here</a>.',
|
|
||||||
// ================================================================
|
// ================================================================
|
||||||
|
|||||||
@@ -246,7 +246,7 @@ export default {
|
|||||||
'common.appearance.theme': 'Тема',
|
'common.appearance.theme': 'Тема',
|
||||||
'common.page.wentwrong': 'Что-то пошло не так.',
|
'common.page.wentwrong': 'Что-то пошло не так.',
|
||||||
'common.page.refresh.tips':
|
'common.page.refresh.tips':
|
||||||
'Упс! Что-то пошло не так. Попробуйте обновить страницу.',
|
'Страница может нуждаться в обновлении. Попробуйте обновить её!',
|
||||||
'common.tips.escape.disable':
|
'common.tips.escape.disable':
|
||||||
'Чтобы закрыть, нажмите "Отмена" или крестик (X) в правом верхнем углу.',
|
'Чтобы закрыть, нажмите "Отмена" или крестик (X) в правом верхнем углу.',
|
||||||
'common.button.clearSelection': 'Сбросить выбор',
|
'common.button.clearSelection': 'Сбросить выбор',
|
||||||
@@ -264,7 +264,11 @@ export default {
|
|||||||
'common.filter.status': 'Фильтровать по статусу',
|
'common.filter.status': 'Фильтровать по статусу',
|
||||||
'common.form.rule.selectInput': 'Выберите или введите {name}',
|
'common.form.rule.selectInput': 'Выберите или введите {name}',
|
||||||
'common.tag.experimental': 'Экспериментальный',
|
'common.tag.experimental': 'Экспериментальный',
|
||||||
'common.title.example': 'Пример'
|
'common.title.example': 'Пример',
|
||||||
|
'common.button.dontshowagain': "Больше не показывать",
|
||||||
|
'common.sorter.tips.ascend': 'Нажмите для сортировки по возрастанию',
|
||||||
|
'common.sorter.tips.descend': 'Нажмите для сортировки по убыванию',
|
||||||
|
'common.sorter.tips.cancel': 'Нажмите, чтобы отменить сортировку'
|
||||||
};
|
};
|
||||||
|
|
||||||
// ========== To-Do: Translate Keys (Remove After Translation) ==========
|
// ========== To-Do: Translate Keys (Remove After Translation) ==========
|
||||||
|
|||||||
+11
-10
@@ -220,8 +220,8 @@ export default {
|
|||||||
'models.form.gpusAllocationType.custom.tips':
|
'models.form.gpusAllocationType.custom.tips':
|
||||||
'Вы можете указать точное количество GPU на реплику.',
|
'Вы можете указать точное количество GPU на реплику.',
|
||||||
'models.mymodels.status.inactive': 'Остановлен',
|
'models.mymodels.status.inactive': 'Остановлен',
|
||||||
'models.mymodels.status.degrade': 'Аномальный',
|
'models.mymodels.status.degrade': 'Не готов',
|
||||||
'models.mymodels.status.active': 'Активен',
|
'models.mymodels.status.active': 'Готов',
|
||||||
'models.form.kvCache.tips':
|
'models.form.kvCache.tips':
|
||||||
'Расширенный KV-кэш и спекулятивное декодирование доступны только со встроенными бэкендами (vLLM / SGLang). Переключите бэкенд в <span class="bold-text">Расширенных</span> настройках, чтобы включить их.',
|
'Расширенный KV-кэш и спекулятивное декодирование доступны только со встроенными бэкендами (vLLM / SGLang). Переключите бэкенд в <span class="bold-text">Расширенных</span> настройках, чтобы включить их.',
|
||||||
'models.form.kvCache.tips2':
|
'models.form.kvCache.tips2':
|
||||||
@@ -268,17 +268,18 @@ export default {
|
|||||||
'Используйте следующий префикс пути и укажите имя модели либо в заголовке запроса <span class="bold-text">X-GPUStack-Model</span>, либо в поле model в теле запроса. Все запросы с этим префиксом пути будут перенаправлены в бэкенд вывода.',
|
'Используйте следующий префикс пути и укажите имя модели либо в заголовке запроса <span class="bold-text">X-GPUStack-Model</span>, либо в поле model в теле запроса. Все запросы с этим префиксом пути будут перенаправлены в бэкенд вывода.',
|
||||||
'models.form.backendVersion.deprecated': 'Устаревший',
|
'models.form.backendVersion.deprecated': 'Устаревший',
|
||||||
'models.accessSettings.public.desc':
|
'models.accessSettings.public.desc':
|
||||||
'Accessible to anyone without authentication.',
|
'Доступно всем без аутентификации.',
|
||||||
'models.accessSettings.authed.tips':
|
'models.accessSettings.authed.tips':
|
||||||
'Accessible to all authenticated platform users.',
|
'Доступно всем аутентифицированным пользователям платформы.',
|
||||||
'models.accessSettings.allowedUsers.tips':
|
'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.form.backendVersions.tips': `Чтобы использовать больше версий, перейдите на страницу {link} и отредактируйте бэкенд для добавления версий.`,
|
||||||
|
'models.catalog.nogpus.tips':
|
||||||
|
'В выбранном кластере нет доступных GPU, совместимых с этой моделью.',
|
||||||
|
'models.form.modelfile.notfound': `Указанный путь к файлу модели не существует на сервере GPUStack. Рекомендуется размещать файл модели по одному и тому же пути как на сервере GPUStack, так и на воркерах GPUStack. Это поможет системе принимать лучшие решения по распределению ресурсов.`,
|
||||||
|
'models.form.readyWorkers': 'воркеров готово'
|
||||||
};
|
};
|
||||||
|
|
||||||
// ========== To-Do: Translate Keys (Remove After Translation) ==========
|
// ========== To-Do: Translate Keys (Remove After Translation) ==========
|
||||||
// 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.`
|
|
||||||
// ========== End of To-Do List ==========
|
// ========== End of To-Do List ==========
|
||||||
|
|||||||
@@ -20,12 +20,12 @@ export default {
|
|||||||
'users.form.currentpassword': 'Текущий пароль',
|
'users.form.currentpassword': 'Текущий пароль',
|
||||||
'users.form.updatepassword': 'Изменить пароль',
|
'users.form.updatepassword': 'Изменить пароль',
|
||||||
'users.form.rule.password':
|
'users.form.rule.password':
|
||||||
'Должен содержать заглавные и строчные буквы, цифры и спецсимволы (!@#$%^&*_+), длина 6-12 символов, без пробелов.',
|
'Должен содержать заглавные и строчные буквы, цифры и спецсимволы (!@#$%^&*_+), длина 6-64 символов, без пробелов.',
|
||||||
'users.password.uppcase': 'Минимум одна заглавная буква',
|
'users.password.uppcase': 'Минимум одна заглавная буква',
|
||||||
'users.password.lowercase': 'Минимум одна строчная буква',
|
'users.password.lowercase': 'Минимум одна строчная буква',
|
||||||
'users.password.number': 'Минимум одна цифра',
|
'users.password.number': 'Минимум одна цифра',
|
||||||
'users.password.special': 'Минимум один спецсимвол',
|
'users.password.special': 'Минимум один спецсимвол',
|
||||||
'users.password.length': 'Длина от 6 до 12 символов',
|
'users.password.length': 'Длина от 6 до 64 символов',
|
||||||
'users.password.modify.title': 'Смена пароля',
|
'users.password.modify.title': 'Смена пароля',
|
||||||
'users.password.modify.description':
|
'users.password.modify.description':
|
||||||
'В целях безопасности измените первоначальный пароль.',
|
'В целях безопасности измените первоначальный пароль.',
|
||||||
@@ -38,7 +38,9 @@ export default {
|
|||||||
'users.settings.title': 'Настройки пользователя',
|
'users.settings.title': 'Настройки пользователя',
|
||||||
'users.status.activate': 'Активировать аккаунт',
|
'users.status.activate': 'Активировать аккаунт',
|
||||||
'users.status.deactivate': 'Деактивировать аккаунт',
|
'users.status.deactivate': 'Деактивировать аккаунт',
|
||||||
'users.status.inactiveAccount': 'Неактивный аккаунт'
|
'users.status.inactiveAccount': 'Неактивный аккаунт',
|
||||||
|
'users.login.getInitialPassword':
|
||||||
|
'Выполните следующую команду на вашем сервере, чтобы получить начальный пароль администратора.'
|
||||||
};
|
};
|
||||||
|
|
||||||
// ========== To-Do: Translate Keys (Remove After Translation) ==========
|
// ========== To-Do: Translate Keys (Remove After Translation) ==========
|
||||||
|
|||||||
@@ -19,8 +19,7 @@ export default {
|
|||||||
'backend.mode.yaml': 'YAML 模式',
|
'backend.mode.yaml': 'YAML 模式',
|
||||||
'backend.form.healthCheckPath': '健康检查路径',
|
'backend.form.healthCheckPath': '健康检查路径',
|
||||||
'backend.form.defaultExecuteCommand': '默认执行命令',
|
'backend.form.defaultExecuteCommand': '默认执行命令',
|
||||||
'backend.form.defaultExecuteCommand.tips':
|
'backend.form.defaultExecuteCommand.tips': `'{{'model_path'}}'、'{{'port'}}'、'{{'worker_ip'}}' 和 '{{'model_name'}}' 都是占位符,在部署过程中会被替换为实际的值。`,
|
||||||
'{{model_path}}、{{port}}、{{worker_ip}} 和 {{model_name}} 都是占位符,在部署过程中会被替换为实际的值。',
|
|
||||||
'backend.form.defaultBackendParameters': '默认后端参数',
|
'backend.form.defaultBackendParameters': '默认后端参数',
|
||||||
'backend.form.versionConfig': '版本配置',
|
'backend.form.versionConfig': '版本配置',
|
||||||
'backend.form.addParameter': '添加参数',
|
'backend.form.addParameter': '添加参数',
|
||||||
@@ -30,5 +29,10 @@ export default {
|
|||||||
'backend.version.rules.builtin': '须以 "-custom" 结尾',
|
'backend.version.rules.builtin': '须以 "-custom" 结尾',
|
||||||
'backend.version.no.tips': '内置后端自定义版本名称必须以 "-custom" 结尾。',
|
'backend.version.no.tips': '内置后端自定义版本名称必须以 "-custom" 结尾。',
|
||||||
'backend.backend.rules.custom': '自定义后端名称须以 "-custom" 结尾。',
|
'backend.backend.rules.custom': '自定义后端名称须以 "-custom" 结尾。',
|
||||||
'backend.quickConfig': '快速配置'
|
'backend.quickConfig': '快速配置',
|
||||||
|
'backend.version.default.not.exists': '默认版本不存在于 {versions}。',
|
||||||
|
'backend.replaceEntrypoint': '覆盖镜像入口命令',
|
||||||
|
'backend.entrypoint': '镜像入口命令',
|
||||||
|
'backend.entrypoint.tips':
|
||||||
|
'如指定,镜像中定义的 ENTRYPOINT 将被忽略,下面的命令将作为容器启动入口命令使用。'
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -48,15 +48,16 @@ export default {
|
|||||||
'clusters.addworker.nvidiaNotes-01':
|
'clusters.addworker.nvidiaNotes-01':
|
||||||
'如果节点有多个出站 IP 地址,请填写 <span class="bold-text">WORKER_IP</span>,以确保使用指定的 IP。可通过命令 <span class="bold-text">hostname -I | xargs -n1</span> 进行确认。',
|
'如果节点有多个出站 IP 地址,请填写 <span class="bold-text">WORKER_IP</span>,以确保使用指定的 IP。可通过命令 <span class="bold-text">hostname -I | xargs -n1</span> 进行确认。',
|
||||||
'clusters.addworker.nvidiaNotes-02':
|
'clusters.addworker.nvidiaNotes-02':
|
||||||
'如果模型目录已存在于节点上,请添加 <span class="bold-text">--volume</span> 参数进行挂载。',
|
'如果节点上已经存在模型目录,你可以指定该路径进行挂载。',
|
||||||
'clusters.addworker.hygonNotes':
|
'clusters.addworker.hygonNotes': `如果 <span class="bold-text">/opt/hyhal</span> 或 <span class="bold-text">/opt/dtk</span> 不存在,请创建指向对应海光安装路径的符号链接,例如:
|
||||||
'如果 <span class="bold-text">/opt/hyhal</span> 目录不存在,请创建指向 Hygon 安装路径的符号链接:<span class="bold-text">/opt/hyhal</span>。与 <span class="bold-text">/opt/dtk</span> 目录相同。',
|
<span class="desc-fill line-6">ln -s /path/to/hyhal /opt/hyhal</span>
|
||||||
|
<span class="desc-fill line-6">ln -s /path/to/dtk /opt/dtk</span>`,
|
||||||
'clusters.addworker.corexNotes':
|
'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':
|
'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':
|
'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':
|
'clusters.addworker.hygonNotes-02':
|
||||||
'如果未能检测到设备,请尝试移除 <span class="bold-text">--env ROCM_SMI_LIB_PATH=/opt/hyhal/lib</span>。',
|
'如果未能检测到设备,请尝试移除 <span class="bold-text">--env ROCM_SMI_LIB_PATH=/opt/hyhal/lib</span>。',
|
||||||
'clusters.addworker.selectCluster': '选择集群',
|
'clusters.addworker.selectCluster': '选择集群',
|
||||||
@@ -66,21 +67,50 @@ export default {
|
|||||||
'clusters.addworker.checkEnv': '检查环境',
|
'clusters.addworker.checkEnv': '检查环境',
|
||||||
'clusters.addworker.specifyArgs': '指定参数',
|
'clusters.addworker.specifyArgs': '指定参数',
|
||||||
'clusters.addworker.runCommand': '运行指令',
|
'clusters.addworker.runCommand': '运行指令',
|
||||||
'clusters.addworker.specifyWorkerIP': '指定 Worker IP',
|
'clusters.addworker.specifyWorkerIP': '节点 IP',
|
||||||
'clusters.addworker.detectWorkerIP': '自动检测 Worker 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': '输入节点 IP',
|
||||||
'clusters.addworker.enterWorkerIP.error': '请输入节点 IP',
|
'clusters.addworker.enterWorkerIP.error': '请输入节点 IP',
|
||||||
|
'clusters.addworker.enterWorkerAddress': '输入节点外部地址',
|
||||||
|
'clusters.addworker.enterWorkerAddress.error': '请输入节点外部地址',
|
||||||
'clusters.addworker.extraVolume': '额外卷挂载',
|
'clusters.addworker.extraVolume': '额外卷挂载',
|
||||||
|
'clusters.addworker.cacheVolume': '缓存卷挂载',
|
||||||
|
'clusters.addworker.cacheVolume.tips':
|
||||||
|
'如果要自定义模型缓存目录,可以指定路径进行挂载。',
|
||||||
'clusters.addworker.configSummary': '配置摘要',
|
'clusters.addworker.configSummary': '配置摘要',
|
||||||
'clusters.addworker.gpuVendor': 'GPU 厂商',
|
'clusters.addworker.gpuVendor': 'GPU 厂商',
|
||||||
'clusters.addworker.workerIP': '节点 IP',
|
'clusters.addworker.workerIP': '节点 IP',
|
||||||
|
'clusters.addworker.workerExternalIP': '节点外部地址',
|
||||||
'clusters.addworker.notSpecified': '未指定',
|
'clusters.addworker.notSpecified': '未指定',
|
||||||
'clusters.addworker.autoDetect': '自动检测',
|
'clusters.addworker.autoDetect': '自动检测',
|
||||||
'clusters.addworker.extraVolume.holder':
|
'clusters.addworker.extraVolume.holder':
|
||||||
'例如:/data/models(路径需以 / 开头)',
|
'例如:/data/models(路径需以 / 开头)',
|
||||||
|
'clusters.addworker.cacheVolume.holder':
|
||||||
|
'例如:/data/cache(路径需以 / 开头)',
|
||||||
'clusters.addworker.vendorNotes.title': '{vendor}设备注意事项',
|
'clusters.addworker.vendorNotes.title': '{vendor}设备注意事项',
|
||||||
'clusters.button.genToken':
|
'clusters.button.genToken':
|
||||||
'需要创建令牌?点击<a href="{link}" target="_blank">这里</a>。',
|
'需要创建令牌?点击<a href="{link}" target="_blank">这里</a>。',
|
||||||
'clusters.addworker.amdNotes-01':
|
'clusters.addworker.amdNotes-01':
|
||||||
'如果 <span class="bold-text">/opt/rocm</span> 目录不存在,请创建一个指向已安装 ROCm 路径的符号链接:<span class="bold-text">ln -s /path/to/rocm /opt/rocm</span>。'
|
'如果 <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': '部署时的默认集群。'
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -241,7 +241,7 @@ export default {
|
|||||||
'common.button.forgotpassword': '忘记密码?',
|
'common.button.forgotpassword': '忘记密码?',
|
||||||
'common.appearance.theme': '主题',
|
'common.appearance.theme': '主题',
|
||||||
'common.page.wentwrong': '哎呀,出了点问题',
|
'common.page.wentwrong': '哎呀,出了点问题',
|
||||||
'common.page.refresh.tips': '出了点问题,试试刷新页面吧!',
|
'common.page.refresh.tips': '页面似乎需要更新,刷新一下试试吧!',
|
||||||
'common.tips.escape.disable': '请点击「取消」按钮或右上角 X 关闭窗口',
|
'common.tips.escape.disable': '请点击「取消」按钮或右上角 X 关闭窗口',
|
||||||
'common.button.clearSelection': '清除选择',
|
'common.button.clearSelection': '清除选择',
|
||||||
'common.select.count': '已选 {count} 项',
|
'common.select.count': '已选 {count} 项',
|
||||||
@@ -257,5 +257,9 @@ export default {
|
|||||||
'common.filter.status': '按状态筛选',
|
'common.filter.status': '按状态筛选',
|
||||||
'common.form.rule.selectInput': '请选择或输入{name}',
|
'common.form.rule.selectInput': '请选择或输入{name}',
|
||||||
'common.tag.experimental': '实验性',
|
'common.tag.experimental': '实验性',
|
||||||
'common.title.example': '示例'
|
'common.title.example': '示例',
|
||||||
|
'common.button.dontshowagain': '不再提示',
|
||||||
|
'common.sorter.tips.ascend': '点击升序',
|
||||||
|
'common.sorter.tips.descend': '点击降序',
|
||||||
|
'common.sorter.tips.cancel': '取消排序'
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -252,5 +252,9 @@ export default {
|
|||||||
'models.accessSettings.public.desc': '任何人无需认证即可访问。',
|
'models.accessSettings.public.desc': '任何人无需认证即可访问。',
|
||||||
'models.accessSettings.authed.tips': '平台内所有已认证用户可访问。',
|
'models.accessSettings.authed.tips': '平台内所有已认证用户可访问。',
|
||||||
'models.accessSettings.allowedUsers.tips': '仅允许选定的特定用户访问。',
|
'models.accessSettings.allowedUsers.tips': '仅允许选定的特定用户访问。',
|
||||||
'models.form.backendVersions.tips': `如需使用更多版本,请前往{link}页面并编辑对应的后端以添加版本。`
|
'models.form.backendVersions.tips': `如需使用更多版本,请前往{link}页面并编辑对应的后端以添加版本。`,
|
||||||
|
'models.catalog.nogpus.tips': '所选集群中没有兼容该模型的 GPU。',
|
||||||
|
'models.form.modelfile.notfound':
|
||||||
|
'你指定的模型文件路径在 GPUStack 服务器上不存在。建议在 GPUStack 服务器和 GPUStack 节点上使用相同的模型文件路径,这有助于 GPUStack 做出更优的调度与决策。',
|
||||||
|
'models.form.readyWorkers': '节点就绪'
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export default {
|
|||||||
'noresult.workers.title': '暂无节点',
|
'noresult.workers.title': '暂无节点',
|
||||||
'noresult.workers.subTitle': '当前集群中没有任何节点',
|
'noresult.workers.subTitle': '当前集群中没有任何节点',
|
||||||
'noresult.workers.nofound': '未找到匹配的节点',
|
'noresult.workers.nofound': '未找到匹配的节点',
|
||||||
'noresult.workers.button.add': '前往添加节点',
|
'noresult.workers.button.add': '添加节点',
|
||||||
'noresult.modelfiles.title': '暂无模型文件',
|
'noresult.modelfiles.title': '暂无模型文件',
|
||||||
'noresult.modelfiles.subTitle': '尚未添加任何模型文件。',
|
'noresult.modelfiles.subTitle': '尚未添加任何模型文件。',
|
||||||
'noresult.modelfiles.nofound': '未找到匹配的模型文件',
|
'noresult.modelfiles.nofound': '未找到匹配的模型文件',
|
||||||
|
|||||||
@@ -19,12 +19,12 @@ export default {
|
|||||||
'users.form.currentpassword': '当前密码',
|
'users.form.currentpassword': '当前密码',
|
||||||
'users.form.updatepassword': '修改密码',
|
'users.form.updatepassword': '修改密码',
|
||||||
'users.form.rule.password':
|
'users.form.rule.password':
|
||||||
'包含大小写字母、数字和特殊字符(!@#$%^&*_+),6至12个字符,不允许有空格',
|
'包含大小写字母、数字和特殊字符(!@#$%^&*_+),6至64个字符,不允许有空格',
|
||||||
'users.password.uppcase': '至少包含一个大写字母',
|
'users.password.uppcase': '至少包含一个大写字母',
|
||||||
'users.password.lowercase': '至少包含一个小写字母',
|
'users.password.lowercase': '至少包含一个小写字母',
|
||||||
'users.password.number': '至少包含一个数字',
|
'users.password.number': '至少包含一个数字',
|
||||||
'users.password.special': '至少包含一个特殊字符',
|
'users.password.special': '至少包含一个特殊字符',
|
||||||
'users.password.length': '长度在6至12个字符之间',
|
'users.password.length': '长度在6至64个字符之间',
|
||||||
'users.password.modify.title': '修改密码',
|
'users.password.modify.title': '修改密码',
|
||||||
'users.password.modify.description': '为了确保您的账户安全,请修改初始密码',
|
'users.password.modify.description': '为了确保您的账户安全,请修改初始密码',
|
||||||
'users.password.confirm': '确认新密码',
|
'users.password.confirm': '确认新密码',
|
||||||
@@ -36,5 +36,7 @@ export default {
|
|||||||
'users.settings.title': '用户设置',
|
'users.settings.title': '用户设置',
|
||||||
'users.status.activate': '启用账户',
|
'users.status.activate': '启用账户',
|
||||||
'users.status.deactivate': '停用账户',
|
'users.status.deactivate': '停用账户',
|
||||||
'users.status.inactiveAccount': '停用账户'
|
'users.status.inactiveAccount': '停用账户',
|
||||||
|
'users.login.getInitialPassword':
|
||||||
|
'在服务器端运行以下命令以获取初始管理员密码。'
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,8 +1,28 @@
|
|||||||
import useOverlayScroller from '@/hooks/use-overlay-scroller';
|
import useOverlayScroller from '@/hooks/use-overlay-scroller';
|
||||||
import React, { useCallback } from 'react';
|
import React, { useCallback } from 'react';
|
||||||
import './style.less';
|
import styled from 'styled-components';
|
||||||
import { WrapperContext } from './use-wrapper-context';
|
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 {
|
interface ColumnWrapperProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
footer?: 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) => {
|
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 (
|
return (
|
||||||
@@ -81,25 +80,18 @@ const ColumnWrapper: React.FC<ColumnWrapperProps> = ({
|
|||||||
setSScrollContentPaddingBottom: setContentPaddingBottom
|
setSScrollContentPaddingBottom: setContentPaddingBottom
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<Wrapper style={{ height: maxHeight || '100%', ...styles.wrapper }}>
|
||||||
className="column-wrapper-footer"
|
<ContentWrapper
|
||||||
style={{ height: maxHeight || '100%', ...styles.wrapper }}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="column-wrapper"
|
|
||||||
ref={scroller}
|
ref={scroller}
|
||||||
style={{ padding: '16px 24px', ...styles.container }}
|
style={{
|
||||||
|
padding: '16px 24px',
|
||||||
|
...styles.container
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<div ref={contentRef} className="child-content">
|
<div ref={contentRef}>{children}</div>
|
||||||
{children}
|
</ContentWrapper>
|
||||||
</div>
|
{footer && <Footer ref={footerRef}>{footer}</Footer>}
|
||||||
</div>
|
</Wrapper>
|
||||||
{footer && (
|
|
||||||
<div className="footer" ref={footerRef}>
|
|
||||||
{footer}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</WrapperContext.Provider>
|
</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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -16,7 +16,7 @@ const InfiniteScroller: React.FC<
|
|||||||
const { observerRef, throttledLoadMore } = useInfiniteScroll(restProps);
|
const { observerRef, throttledLoadMore } = useInfiniteScroll(restProps);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="relative" style={{ width: '100%' }}>
|
||||||
{children}
|
{children}
|
||||||
<div
|
<div
|
||||||
ref={observerRef}
|
ref={observerRef}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
.ant-pro-footer-bar {
|
.ant-pro-footer-bar {
|
||||||
padding-inline: 0;
|
padding-inline: 0;
|
||||||
border-block-start: 1px solid rgba(5, 5, 5, 6%);
|
border-block-start: 1px solid rgba(5, 5, 5, 6%);
|
||||||
// background-color: #f5f5f5;
|
|
||||||
border-radius: 4px 4px 0 0;
|
border-radius: 4px 4px 0 0;
|
||||||
|
|
||||||
.ant-pro-footer-bar-left {
|
.ant-pro-footer-bar-left {
|
||||||
@@ -11,30 +10,4 @@
|
|||||||
.ant-pro-footer-bar-right {
|
.ant-pro-footer-bar-right {
|
||||||
flex: 1;
|
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 {
|
export interface TerminalModalProps {
|
||||||
terminals: { url: string; name: string }[];
|
terminals: { url: string; name: string }[];
|
||||||
height: number;
|
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
currentActive?: string;
|
currentActive?: string;
|
||||||
@@ -11,19 +10,16 @@ export interface TerminalModalProps {
|
|||||||
|
|
||||||
const TerminalModal: React.FC<TerminalModalProps> = ({
|
const TerminalModal: React.FC<TerminalModalProps> = ({
|
||||||
terminals,
|
terminals,
|
||||||
height,
|
|
||||||
open,
|
open,
|
||||||
onClose,
|
onClose,
|
||||||
currentActive
|
currentActive
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<div>
|
|
||||||
<TerminalTabs
|
<TerminalTabs
|
||||||
terminals={terminals}
|
terminals={terminals}
|
||||||
height={height}
|
|
||||||
currentActive={currentActive}
|
currentActive={currentActive}
|
||||||
|
onClose={onClose}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,98 @@
|
|||||||
|
import { HolderOutlined } from '@ant-design/icons';
|
||||||
|
import { useIntl } from '@umijs/max';
|
||||||
|
import { Button } from 'antd';
|
||||||
|
import { Resizable, ResizableProps } from 're-resizable';
|
||||||
|
import React, { forwardRef, useImperativeHandle, useRef } from 'react';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
const StyledButton = styled(Button)`
|
||||||
|
position: absolute;
|
||||||
|
padding: 0;
|
||||||
|
top: -12px;
|
||||||
|
font-size: var(--font-size-middle);
|
||||||
|
left: calc(50% + 10px);
|
||||||
|
transform: translateX(-50%);
|
||||||
|
background: none !important;
|
||||||
|
cursor: ns-resize;
|
||||||
|
&::hover {
|
||||||
|
background: none !important;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const ResizeContainer: React.FC<
|
||||||
|
React.PropsWithChildren<
|
||||||
|
ResizableProps & {
|
||||||
|
ref?: any;
|
||||||
|
defaultHeight?: number;
|
||||||
|
defaultWidth?: number;
|
||||||
|
minHeight?: number;
|
||||||
|
maxHeight?: number;
|
||||||
|
onReSize?: (
|
||||||
|
e: MouseEvent | TouchEvent,
|
||||||
|
dir: any,
|
||||||
|
refToElement: HTMLElement
|
||||||
|
) => void;
|
||||||
|
onReSizeStop?: (
|
||||||
|
e: MouseEvent | TouchEvent,
|
||||||
|
dir: any,
|
||||||
|
refToElement: HTMLElement,
|
||||||
|
delta: { width: number; height: number }
|
||||||
|
) => void;
|
||||||
|
}
|
||||||
|
>
|
||||||
|
> = forwardRef((props, ref) => {
|
||||||
|
const {
|
||||||
|
defaultWidth,
|
||||||
|
defaultHeight = 180,
|
||||||
|
minHeight = 180,
|
||||||
|
maxHeight = 400,
|
||||||
|
children,
|
||||||
|
onReSize,
|
||||||
|
onReSizeStop,
|
||||||
|
...restProps
|
||||||
|
} = props;
|
||||||
|
const intl = useIntl();
|
||||||
|
const resizeRef = useRef<Resizable>(null);
|
||||||
|
|
||||||
|
useImperativeHandle(ref, () => ({
|
||||||
|
container: resizeRef.current
|
||||||
|
}));
|
||||||
|
|
||||||
|
const dragHanle = (
|
||||||
|
<StyledButton
|
||||||
|
size="small"
|
||||||
|
type="text"
|
||||||
|
icon={
|
||||||
|
<HolderOutlined
|
||||||
|
rotate={90}
|
||||||
|
style={{ fontSize: 'var(--font-size-14)' }}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
></StyledButton>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Resizable
|
||||||
|
ref={resizeRef}
|
||||||
|
enable={{
|
||||||
|
top: true
|
||||||
|
}}
|
||||||
|
defaultSize={{
|
||||||
|
height: defaultHeight,
|
||||||
|
width: defaultWidth
|
||||||
|
}}
|
||||||
|
handleComponent={{
|
||||||
|
top: undefined
|
||||||
|
}}
|
||||||
|
maxHeight={maxHeight}
|
||||||
|
minHeight={minHeight}
|
||||||
|
onResize={onReSize}
|
||||||
|
onResizeStop={onReSizeStop}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Resizable>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default ResizeContainer;
|
||||||
@@ -1,22 +1,90 @@
|
|||||||
import XTerminal from '@/components/x-terminal';
|
import XTerminal from '@/components/x-terminal';
|
||||||
import { Tabs } from 'antd';
|
import { CloseOutlined } from '@ant-design/icons';
|
||||||
|
import { Button, Tabs } from 'antd';
|
||||||
|
import { throttle } from 'lodash';
|
||||||
import React, { useEffect, useMemo, useState } from 'react';
|
import React, { useEffect, useMemo, useState } from 'react';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
import ResizeContainer from './resize-container';
|
||||||
import { TerminalProps } from './types';
|
import { TerminalProps } from './types';
|
||||||
|
|
||||||
|
const TabsContainer = styled.div`
|
||||||
|
.ant-tabs {
|
||||||
|
.ant-tabs-tab {
|
||||||
|
positon: relative;
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
width: 1px;
|
||||||
|
border-right: 1px solid var(--ant-color-split);
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.ant-tabs-nav {
|
||||||
|
.ant-tabs-tab {
|
||||||
|
border: none;
|
||||||
|
background-color: transparent;
|
||||||
|
|
||||||
|
.ant-tabs-tab-remove .anticon-close {
|
||||||
|
// display: none;
|
||||||
|
height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
display: none;
|
||||||
|
content: '';
|
||||||
|
left: 5px;
|
||||||
|
top: 5px;
|
||||||
|
bottom: 5px;
|
||||||
|
right: 5px;
|
||||||
|
position: absolute;
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: var(--ant-color-fill-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
&::before {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.ant-tabs-tab-remove .anticon-close {
|
||||||
|
// display: inline-block;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
export interface TerminalTabsProps {
|
export interface TerminalTabsProps {
|
||||||
terminals: TerminalProps[];
|
terminals: TerminalProps[];
|
||||||
height: number;
|
|
||||||
currentActive?: string;
|
currentActive?: string;
|
||||||
|
onClose: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const TerminalTabs: React.FC<TerminalTabsProps> = ({
|
const TerminalTabs: React.FC<TerminalTabsProps> = ({
|
||||||
terminals,
|
terminals,
|
||||||
height,
|
currentActive,
|
||||||
currentActive
|
onClose
|
||||||
}) => {
|
}) => {
|
||||||
const [activeKey, setActiveKey] = useState(
|
const [activeKey, setActiveKey] = useState(
|
||||||
currentActive || terminals[0]?.url || ''
|
currentActive || terminals[0]?.url || ''
|
||||||
);
|
);
|
||||||
|
const [height, setHeight] = useState(300);
|
||||||
|
const resizeRef = React.useRef<any>(null);
|
||||||
|
|
||||||
const items = useMemo(() => {
|
const items = useMemo(() => {
|
||||||
return terminals.map((terminal) => {
|
return terminals.map((terminal) => {
|
||||||
@@ -26,7 +94,13 @@ const TerminalTabs: React.FC<TerminalTabsProps> = ({
|
|||||||
children: <XTerminal height={height} url={terminal.url} />
|
children: <XTerminal height={height} url={terminal.url} />
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}, [terminals]);
|
}, [terminals, height]);
|
||||||
|
|
||||||
|
const handleOnResize = throttle(() => {
|
||||||
|
const newHeight = resizeRef.current?.container?.state?.height;
|
||||||
|
console.log('newHeight', newHeight);
|
||||||
|
setHeight(newHeight - 43); // 43 is the height of the tabs header
|
||||||
|
}, 200);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (currentActive) {
|
if (currentActive) {
|
||||||
@@ -35,6 +109,8 @@ const TerminalTabs: React.FC<TerminalTabsProps> = ({
|
|||||||
}, [currentActive]);
|
}, [currentActive]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<ResizeContainer onReSize={handleOnResize} ref={resizeRef}>
|
||||||
|
<TabsContainer>
|
||||||
<Tabs
|
<Tabs
|
||||||
type="editable-card"
|
type="editable-card"
|
||||||
hideAdd
|
hideAdd
|
||||||
@@ -42,7 +118,20 @@ const TerminalTabs: React.FC<TerminalTabsProps> = ({
|
|||||||
onChange={setActiveKey}
|
onChange={setActiveKey}
|
||||||
tabBarStyle={{ marginBottom: 0 }}
|
tabBarStyle={{ marginBottom: 0 }}
|
||||||
items={items}
|
items={items}
|
||||||
|
tabBarExtraContent={{
|
||||||
|
right: (
|
||||||
|
<Button
|
||||||
|
icon={<CloseOutlined />}
|
||||||
|
type="text"
|
||||||
|
size="small"
|
||||||
|
style={{ marginRight: 8 }}
|
||||||
|
onClick={onClose}
|
||||||
|
></Button>
|
||||||
|
)
|
||||||
|
}}
|
||||||
></Tabs>
|
></Tabs>
|
||||||
|
</TabsContainer>
|
||||||
|
</ResizeContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+7
-20
@@ -1,6 +1,4 @@
|
|||||||
import EditorWrap from '@/components/editor-wrap';
|
import EditorWrap from '@/components/editor-wrap';
|
||||||
import { PageAction } from '@/config';
|
|
||||||
import { PageActionType } from '@/config/types';
|
|
||||||
import { LoadingOutlined } from '@ant-design/icons';
|
import { LoadingOutlined } from '@ant-design/icons';
|
||||||
import Editor, { loader } from '@monaco-editor/react';
|
import Editor, { loader } from '@monaco-editor/react';
|
||||||
import * as monaco from 'monaco-editor';
|
import * as monaco from 'monaco-editor';
|
||||||
@@ -11,15 +9,11 @@ import React, {
|
|||||||
useImperativeHandle,
|
useImperativeHandle,
|
||||||
useRef
|
useRef
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import createSchema from '../config/schema/create.json';
|
|
||||||
import updateBuiltinSchema from '../config/schema/update-builtin.json';
|
|
||||||
import udpateCustomSchema from '../config/schema/update-custom.json';
|
|
||||||
|
|
||||||
loader.config({ monaco });
|
loader.config({ monaco });
|
||||||
|
|
||||||
interface ViewerProps {
|
interface ViewerProps {
|
||||||
ref?: any;
|
ref?: any;
|
||||||
lang: string;
|
|
||||||
defaultLang?: string;
|
defaultLang?: string;
|
||||||
config?: any;
|
config?: any;
|
||||||
value: string;
|
value: string;
|
||||||
@@ -28,22 +22,19 @@ interface ViewerProps {
|
|||||||
header?: React.ReactNode;
|
header?: React.ReactNode;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
variant?: 'bordered' | 'borderless';
|
variant?: 'bordered' | 'borderless';
|
||||||
actionStatus: {
|
schema?: any;
|
||||||
action: PageActionType;
|
|
||||||
isBuiltIn: boolean;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const path = 'inmemory://model/config.yaml';
|
const path = 'inmemory://model/config.yaml';
|
||||||
|
|
||||||
const ViewerEditor: React.FC<ViewerProps> = forwardRef((props, ref) => {
|
const EditorInner: React.FC<ViewerProps> = forwardRef((props, ref) => {
|
||||||
const {
|
const {
|
||||||
value,
|
value,
|
||||||
height = 380,
|
height = 380,
|
||||||
theme = 'vs-dark',
|
theme = 'vs-dark',
|
||||||
header,
|
header,
|
||||||
variant = 'borderless',
|
variant = 'borderless',
|
||||||
actionStatus,
|
schema,
|
||||||
placeholder
|
placeholder
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
@@ -55,16 +46,12 @@ const ViewerEditor: React.FC<ViewerProps> = forwardRef((props, ref) => {
|
|||||||
const yamlUri = monaco.Uri.parse(path);
|
const yamlUri = monaco.Uri.parse(path);
|
||||||
|
|
||||||
configureMonacoYaml(monaco, {
|
configureMonacoYaml(monaco, {
|
||||||
|
hover: false,
|
||||||
schemas: [
|
schemas: [
|
||||||
{
|
{
|
||||||
uri: 'http://example.com/schema-name.json',
|
uri: 'http://example.com/schema-name.json',
|
||||||
fileMatch: [yamlUri.toString()],
|
fileMatch: [yamlUri.toString()],
|
||||||
schema:
|
schema: schema
|
||||||
actionStatus.action === PageAction.CREATE
|
|
||||||
? createSchema
|
|
||||||
: actionStatus.isBuiltIn
|
|
||||||
? updateBuiltinSchema
|
|
||||||
: udpateCustomSchema
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
@@ -88,12 +75,12 @@ const ViewerEditor: React.FC<ViewerProps> = forwardRef((props, ref) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Currently, do not use this function, but keep it for future validation needs
|
||||||
const getMarkers = () => {
|
const getMarkers = () => {
|
||||||
const uri = editorRef.current?.getModel()?.uri;
|
const uri = editorRef.current?.getModel()?.uri;
|
||||||
const markers = monacoRef.current?.editor.getModelMarkers({
|
const markers = monacoRef.current?.editor.getModelMarkers({
|
||||||
resource: uri
|
resource: uri
|
||||||
});
|
});
|
||||||
console.log('Validation markers:', uri, markers);
|
|
||||||
return markers;
|
return markers;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -151,4 +138,4 @@ const ViewerEditor: React.FC<ViewerProps> = forwardRef((props, ref) => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
export default ViewerEditor;
|
export default EditorInner;
|
||||||
@@ -0,0 +1,174 @@
|
|||||||
|
import useUserSettings from '@/hooks/use-user-settings';
|
||||||
|
import { ImportOutlined } from '@ant-design/icons';
|
||||||
|
import { loader } from '@monaco-editor/react';
|
||||||
|
import { useIntl } from '@umijs/max';
|
||||||
|
import { Button, message, Typography, Upload } from 'antd';
|
||||||
|
import { RcFile } from 'antd/lib/upload';
|
||||||
|
import * as monaco from 'monaco-editor';
|
||||||
|
import React, {
|
||||||
|
forwardRef,
|
||||||
|
useEffect,
|
||||||
|
useImperativeHandle,
|
||||||
|
useRef
|
||||||
|
} from 'react';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
import EditorInner from './editor';
|
||||||
|
|
||||||
|
const { Text } = Typography;
|
||||||
|
|
||||||
|
loader.config({ monaco });
|
||||||
|
|
||||||
|
const Container = styled.div<{ $minHeight: string | number }>`
|
||||||
|
min-height: ${({ $minHeight }) =>
|
||||||
|
typeof $minHeight === 'number' ? `${$minHeight}px` : $minHeight};
|
||||||
|
position: relative;
|
||||||
|
border: 1px solid var(--ant-color-border);
|
||||||
|
border-radius: var(--ant-border-radius);
|
||||||
|
.monaco-editor .scroll-decoration {
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const ErrorText = styled(Text)`
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
padding: 4px 6px;
|
||||||
|
background-color: var(--ant-color-bg-elevated);
|
||||||
|
border-radius: 0 0 var(--ant-border-radius) var(--ant-border-radius);
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Header = styled.div`
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
height: 40px;
|
||||||
|
padding-inline: 10px;
|
||||||
|
font-size: 14px;
|
||||||
|
border-bottom: 1px solid var(--ant-color-border);
|
||||||
|
background-color: var(--ant-color-fill-tertiary);
|
||||||
|
`;
|
||||||
|
|
||||||
|
interface ViewerProps {
|
||||||
|
ref?: any;
|
||||||
|
title?: React.ReactNode;
|
||||||
|
defaultLang?: string;
|
||||||
|
config?: any;
|
||||||
|
value: string;
|
||||||
|
height?: string | number;
|
||||||
|
placeholder?: string;
|
||||||
|
variant?: 'bordered' | 'borderless';
|
||||||
|
validateMessage?: React.ReactNode;
|
||||||
|
schema?: any;
|
||||||
|
onUpload?: (content: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const YamlEditor: React.FC<ViewerProps> = forwardRef((props, ref) => {
|
||||||
|
const {
|
||||||
|
value,
|
||||||
|
height = 380,
|
||||||
|
variant = 'borderless',
|
||||||
|
schema,
|
||||||
|
placeholder,
|
||||||
|
validateMessage,
|
||||||
|
title,
|
||||||
|
onUpload
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const intl = useIntl();
|
||||||
|
const { userSettings } = useUserSettings();
|
||||||
|
|
||||||
|
const editorRef = useRef<any>(null);
|
||||||
|
|
||||||
|
const setContent = (val: string) => {
|
||||||
|
editorRef.current?.setValue?.(val);
|
||||||
|
};
|
||||||
|
|
||||||
|
const beforeUpload = (file: RcFile) => {
|
||||||
|
const isYaml =
|
||||||
|
file.type === 'application/x-yaml' ||
|
||||||
|
file.type === 'text/yaml' ||
|
||||||
|
file.name.endsWith('.yaml') ||
|
||||||
|
file.name.endsWith('.yml');
|
||||||
|
if (!isYaml) {
|
||||||
|
message.error('You can only upload YAML file!');
|
||||||
|
}
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (e) => {
|
||||||
|
const content = e.target?.result;
|
||||||
|
if (typeof content === 'string') {
|
||||||
|
onUpload?.(content);
|
||||||
|
setContent(content);
|
||||||
|
} else {
|
||||||
|
message.error('Failed to read file content!');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
reader.readAsText(file);
|
||||||
|
// Prevent upload
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderHeader = () => {
|
||||||
|
return (
|
||||||
|
<Header>
|
||||||
|
<span className="title">{title || 'YAML'}</span>
|
||||||
|
<Upload
|
||||||
|
name="file"
|
||||||
|
multiple={false}
|
||||||
|
beforeUpload={beforeUpload}
|
||||||
|
showUploadList={false}
|
||||||
|
accept=".yaml,.yml,text/yaml,application/x-yaml"
|
||||||
|
>
|
||||||
|
<Button icon={<ImportOutlined />} type="text" size="small">
|
||||||
|
{intl.formatMessage({ id: 'common.button.import' })}
|
||||||
|
</Button>
|
||||||
|
</Upload>
|
||||||
|
</Header>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
useImperativeHandle(ref, () => ({
|
||||||
|
format: () => {
|
||||||
|
editorRef.current?.format();
|
||||||
|
},
|
||||||
|
getValue: () => {
|
||||||
|
return editorRef.current?.getValue?.();
|
||||||
|
},
|
||||||
|
setValue: (val: string) => {
|
||||||
|
editorRef.current?.setValue?.(val);
|
||||||
|
},
|
||||||
|
dispose: () => {
|
||||||
|
editorRef.current?.dispose?.();
|
||||||
|
},
|
||||||
|
validate() {
|
||||||
|
return editorRef.current?.validate();
|
||||||
|
},
|
||||||
|
editor: editorRef.current
|
||||||
|
}));
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
editorRef.current?.format();
|
||||||
|
}, [value]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container $minHeight={height}>
|
||||||
|
<EditorInner
|
||||||
|
ref={editorRef}
|
||||||
|
header={renderHeader()}
|
||||||
|
variant={variant}
|
||||||
|
height={height}
|
||||||
|
theme={userSettings?.isDarkTheme ? 'vs-dark' : 'vs-light'}
|
||||||
|
value={value}
|
||||||
|
placeholder={placeholder}
|
||||||
|
schema={schema}
|
||||||
|
/>
|
||||||
|
{validateMessage && (
|
||||||
|
<ErrorText type="danger">{validateMessage}</ErrorText>
|
||||||
|
)}
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default YamlEditor;
|
||||||
@@ -16,6 +16,12 @@ import { expirationOptions } from '../../config';
|
|||||||
import { FormData, ListItem } from '../../config/types';
|
import { FormData, ListItem } from '../../config/types';
|
||||||
import APIKeyForm from './form';
|
import APIKeyForm from './form';
|
||||||
|
|
||||||
|
const ModalFooterStyle = {
|
||||||
|
padding: '16px 24px 8px',
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'flex-end'
|
||||||
|
};
|
||||||
|
|
||||||
type AddModalProps = {
|
type AddModalProps = {
|
||||||
title: string;
|
title: string;
|
||||||
action: PageActionType;
|
action: PageActionType;
|
||||||
@@ -204,31 +210,20 @@ const AddModal: React.FC<AddModalProps> = ({
|
|||||||
closeIcon={false}
|
closeIcon={false}
|
||||||
maskClosable={false}
|
maskClosable={false}
|
||||||
keyboard={false}
|
keyboard={false}
|
||||||
styles={{
|
|
||||||
body: {
|
|
||||||
height: 'calc(100vh - 57px)',
|
|
||||||
padding: '16px 0',
|
|
||||||
overflowX: 'hidden'
|
|
||||||
},
|
|
||||||
content: {
|
|
||||||
borderRadius: '6px 0 0 6px'
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
width={600}
|
width={600}
|
||||||
footer={false}
|
footer={false}
|
||||||
>
|
>
|
||||||
<ColumnWrapper
|
<ColumnWrapper
|
||||||
styles={{
|
styles={{
|
||||||
container: { paddingTop: 0 }
|
container: { paddingBlock: 0 }
|
||||||
}}
|
}}
|
||||||
footer={
|
footer={
|
||||||
!showKey ? (
|
!showKey ? (
|
||||||
<>
|
<>
|
||||||
{isChanged && (
|
{isChanged && (
|
||||||
<div style={{ marginInline: 24 }}>
|
<div style={{ marginInline: 24, paddingTop: 8 }}>
|
||||||
<AlertBlockInfo
|
<AlertBlockInfo
|
||||||
type="warning"
|
type="warning"
|
||||||
style={{ marginBottom: 16 }}
|
|
||||||
contentStyle={{ paddingInline: 0 }}
|
contentStyle={{ paddingInline: 0 }}
|
||||||
message={intl.formatMessage({
|
message={intl.formatMessage({
|
||||||
id: 'models.button.accessSettings.tips'
|
id: 'models.button.accessSettings.tips'
|
||||||
@@ -240,11 +235,7 @@ const AddModal: React.FC<AddModalProps> = ({
|
|||||||
onOk={handleSumit}
|
onOk={handleSumit}
|
||||||
onCancel={onCancel}
|
onCancel={onCancel}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
style={{
|
style={ModalFooterStyle}
|
||||||
padding: '16px 24px',
|
|
||||||
display: 'flex',
|
|
||||||
justifyContent: 'flex-end'
|
|
||||||
}}
|
|
||||||
></ModalFooter>
|
></ModalFooter>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
@@ -255,11 +246,7 @@ const AddModal: React.FC<AddModalProps> = ({
|
|||||||
loading={loading}
|
loading={loading}
|
||||||
okText={intl.formatMessage({ id: 'common.button.done' })}
|
okText={intl.formatMessage({ id: 'common.button.done' })}
|
||||||
showCancelBtn={false}
|
showCancelBtn={false}
|
||||||
style={{
|
style={ModalFooterStyle}
|
||||||
padding: '16px 24px',
|
|
||||||
display: 'flex',
|
|
||||||
justifyContent: 'flex-end'
|
|
||||||
}}
|
|
||||||
></ModalFooter>
|
></ModalFooter>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
import AutoTooltip from '@/components/auto-tooltip';
|
import AutoTooltip from '@/components/auto-tooltip';
|
||||||
import DropdownButtons from '@/components/drop-down-buttons';
|
import DropdownButtons from '@/components/drop-down-buttons';
|
||||||
import icons from '@/components/icon-font/icons';
|
import icons from '@/components/icon-font/icons';
|
||||||
|
import { tableSorter } from '@/config/settings';
|
||||||
import { useIntl } from '@umijs/max';
|
import { useIntl } from '@umijs/max';
|
||||||
import type { SortOrder } from 'antd/es/table/interface';
|
|
||||||
import { ColumnsType } from 'antd/lib/table';
|
import { ColumnsType } from 'antd/lib/table';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
@@ -11,7 +11,7 @@ import { ListItem } from '../config/types';
|
|||||||
|
|
||||||
interface ColumnsHookProps {
|
interface ColumnsHookProps {
|
||||||
handleSelect: (val: string, record: ListItem) => void;
|
handleSelect: (val: string, record: ListItem) => void;
|
||||||
sortOrder: SortOrder;
|
sortOrder: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const actionList: Global.ActionItem[] = [
|
const actionList: Global.ActionItem[] = [
|
||||||
@@ -40,6 +40,7 @@ const useModelsColumns = ({
|
|||||||
title: intl.formatMessage({ id: 'common.table.name' }),
|
title: intl.formatMessage({ id: 'common.table.name' }),
|
||||||
dataIndex: 'name',
|
dataIndex: 'name',
|
||||||
key: 'name',
|
key: 'name',
|
||||||
|
sorter: tableSorter(1),
|
||||||
render: (text: string, record: ListItem) => (
|
render: (text: string, record: ListItem) => (
|
||||||
<AutoTooltip ghost style={{ maxWidth: 400 }}>
|
<AutoTooltip ghost style={{ maxWidth: 400 }}>
|
||||||
{text}
|
{text}
|
||||||
@@ -50,6 +51,7 @@ const useModelsColumns = ({
|
|||||||
title: intl.formatMessage({ id: 'apikeys.form.expiretime' }),
|
title: intl.formatMessage({ id: 'apikeys.form.expiretime' }),
|
||||||
dataIndex: 'expires_at',
|
dataIndex: 'expires_at',
|
||||||
key: 'expires_at',
|
key: 'expires_at',
|
||||||
|
sorter: tableSorter(2),
|
||||||
render: (text: string, record: ListItem) => (
|
render: (text: string, record: ListItem) => (
|
||||||
<AutoTooltip ghost>
|
<AutoTooltip ghost>
|
||||||
{text
|
{text
|
||||||
@@ -90,9 +92,7 @@ const useModelsColumns = ({
|
|||||||
title: intl.formatMessage({ id: 'common.table.createTime' }),
|
title: intl.formatMessage({ id: 'common.table.createTime' }),
|
||||||
dataIndex: 'created_at',
|
dataIndex: 'created_at',
|
||||||
key: 'created_at',
|
key: 'created_at',
|
||||||
defaultSortOrder: 'descend',
|
sorter: tableSorter(3),
|
||||||
sortOrder,
|
|
||||||
sorter: false,
|
|
||||||
ellipsis: {
|
ellipsis: {
|
||||||
showTitle: false
|
showTitle: false
|
||||||
},
|
},
|
||||||
@@ -115,7 +115,7 @@ const useModelsColumns = ({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
}, [sortOrder, intl, handleSelect]);
|
}, [intl, handleSelect]);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default useModelsColumns;
|
export default useModelsColumns;
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import useTableFetch from '@/hooks/use-table-fetch';
|
|||||||
import { useIntl } from '@umijs/max';
|
import { useIntl } from '@umijs/max';
|
||||||
import useMemoizedFn from 'ahooks/lib/useMemoizedFn';
|
import useMemoizedFn from 'ahooks/lib/useMemoizedFn';
|
||||||
import { ConfigProvider, Table } from 'antd';
|
import { ConfigProvider, Table } from 'antd';
|
||||||
|
import _ from 'lodash';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import NoResult from '../_components/no-result';
|
import NoResult from '../_components/no-result';
|
||||||
import PageBox from '../_components/page-box';
|
import PageBox from '../_components/page-box';
|
||||||
@@ -17,6 +18,7 @@ import useKeysColumns from './hooks/use-keys-columns';
|
|||||||
|
|
||||||
const APIKeys: React.FC = () => {
|
const APIKeys: React.FC = () => {
|
||||||
const {
|
const {
|
||||||
|
TABLE_SORT_DIRECTIONS,
|
||||||
dataSource,
|
dataSource,
|
||||||
rowSelection,
|
rowSelection,
|
||||||
queryParams,
|
queryParams,
|
||||||
@@ -105,7 +107,7 @@ const APIKeys: React.FC = () => {
|
|||||||
loadend={dataSource.loadend}
|
loadend={dataSource.loadend}
|
||||||
dataSource={dataSource.dataList}
|
dataSource={dataSource.dataList}
|
||||||
image={<IconFont type="icon-key" />}
|
image={<IconFont type="icon-key" />}
|
||||||
filters={queryParams}
|
filters={_.omit(queryParams, ['sort_by'])}
|
||||||
noFoundText={intl.formatMessage({
|
noFoundText={intl.formatMessage({
|
||||||
id: 'noresult.keys.nofound'
|
id: 'noresult.keys.nofound'
|
||||||
})}
|
})}
|
||||||
@@ -139,6 +141,8 @@ const APIKeys: React.FC = () => {
|
|||||||
dataSource={dataSource.dataList}
|
dataSource={dataSource.dataList}
|
||||||
rowSelection={rowSelection}
|
rowSelection={rowSelection}
|
||||||
loading={dataSource.loading}
|
loading={dataSource.loading}
|
||||||
|
sortDirections={TABLE_SORT_DIRECTIONS}
|
||||||
|
showSorterTooltip={false}
|
||||||
rowKey="id"
|
rowKey="id"
|
||||||
onChange={handleTableChange}
|
onChange={handleTableChange}
|
||||||
pagination={{
|
pagination={{
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import BackendForm from '../forms';
|
|||||||
import ImportYAML from './import-yaml';
|
import ImportYAML from './import-yaml';
|
||||||
|
|
||||||
const ModalFooterStyle = {
|
const ModalFooterStyle = {
|
||||||
padding: '16px 24px',
|
padding: '16px 24px 8px',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
justifyContent: 'flex-end'
|
justifyContent: 'flex-end'
|
||||||
};
|
};
|
||||||
@@ -51,6 +51,13 @@ const AddModal: React.FC<AddModalProps> = (props) => {
|
|||||||
const [yamlContent, setYamlContent] = useState<string>('');
|
const [yamlContent, setYamlContent] = useState<string>('');
|
||||||
const [formContent, setFormContent] = useState<FormData>({} as FormData);
|
const [formContent, setFormContent] = useState<FormData>({} as FormData);
|
||||||
|
|
||||||
|
const versionFields = [
|
||||||
|
'image_name',
|
||||||
|
'run_command',
|
||||||
|
'custom_framework',
|
||||||
|
'entrypoint'
|
||||||
|
];
|
||||||
|
|
||||||
// remove '-custom' suffix from version_no in currentData, when action is EDIT
|
// remove '-custom' suffix from version_no in currentData, when action is EDIT
|
||||||
const genertateCurrentVersionData = (values: ListItem): ListItem => {
|
const genertateCurrentVersionData = (values: ListItem): ListItem => {
|
||||||
const data = { ...values };
|
const data = { ...values };
|
||||||
@@ -82,9 +89,7 @@ const AddModal: React.FC<AddModalProps> = (props) => {
|
|||||||
(acc: Record<string, any>, curr) => {
|
(acc: Record<string, any>, curr) => {
|
||||||
if (curr.version_no) {
|
if (curr.version_no) {
|
||||||
acc[curr.version_no] = {
|
acc[curr.version_no] = {
|
||||||
custom_framework: curr.custom_framework,
|
..._.pick(curr, versionFields)
|
||||||
image_name: curr.image_name,
|
|
||||||
run_command: curr.run_command
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return acc;
|
return acc;
|
||||||
@@ -110,10 +115,8 @@ const AddModal: React.FC<AddModalProps> = (props) => {
|
|||||||
const versionConfigs = Object.keys(values.version_configs || {}).map(
|
const versionConfigs = Object.keys(values.version_configs || {}).map(
|
||||||
(key: string) => ({
|
(key: string) => ({
|
||||||
version_no: key,
|
version_no: key,
|
||||||
image_name: values.version_configs?.[key]?.image_name,
|
is_default: key === values.default_version,
|
||||||
run_command: values.version_configs?.[key]?.run_command,
|
..._.pick(values.version_configs?.[key], versionFields)
|
||||||
custom_framework: values.version_configs?.[key]?.custom_framework,
|
|
||||||
is_default: key === values.default_version
|
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -122,12 +125,15 @@ const AddModal: React.FC<AddModalProps> = (props) => {
|
|||||||
values.built_in_version_configs || {}
|
values.built_in_version_configs || {}
|
||||||
).map((key) => ({
|
).map((key) => ({
|
||||||
version_no: key,
|
version_no: key,
|
||||||
image_name: values.built_in_version_configs?.[key]?.image_name,
|
|
||||||
run_command: values.built_in_version_configs?.[key]?.run_command,
|
|
||||||
is_default: key === values.default_version,
|
is_default: key === values.default_version,
|
||||||
built_in_frameworks:
|
built_in_frameworks:
|
||||||
values.built_in_version_configs?.[key]?.built_in_frameworks || [],
|
values.built_in_version_configs?.[key]?.built_in_frameworks || [],
|
||||||
is_built_in: true
|
is_built_in: true,
|
||||||
|
..._.pick(values.built_in_version_configs?.[key], [
|
||||||
|
'image_name',
|
||||||
|
'run_command',
|
||||||
|
'entrypoint'
|
||||||
|
])
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -220,7 +226,7 @@ const AddModal: React.FC<AddModalProps> = (props) => {
|
|||||||
maxHeight={'calc(100vh - 140px)'}
|
maxHeight={'calc(100vh - 140px)'}
|
||||||
styles={{
|
styles={{
|
||||||
container: {
|
container: {
|
||||||
paddingTop: 0
|
paddingBlock: 0
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
footer={
|
footer={
|
||||||
|
|||||||
@@ -18,6 +18,15 @@ import {
|
|||||||
} from '../config';
|
} from '../config';
|
||||||
import { ListItem } from '../config/types';
|
import { ListItem } from '../config/types';
|
||||||
|
|
||||||
|
const StyledCard = styled(Card)`
|
||||||
|
&:hover {
|
||||||
|
.operations {
|
||||||
|
background-color: var(--ant-color-fill-tertiary);
|
||||||
|
border-radius: var(--ant-border-radius);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
const TagInner = styled(Tag)`
|
const TagInner = styled(Tag)`
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -33,6 +42,7 @@ const Header = styled.div`
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
height: 24px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
.title {
|
.title {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -187,7 +197,7 @@ const BackendCard: React.FC<BackendCardProps> = ({ data, onSelect }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<StyledCard
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
clickable={true}
|
clickable={true}
|
||||||
hoverable={true}
|
hoverable={true}
|
||||||
@@ -197,7 +207,7 @@ const BackendCard: React.FC<BackendCardProps> = ({ data, onSelect }) => {
|
|||||||
header={
|
header={
|
||||||
<Header>
|
<Header>
|
||||||
<div className="title">{renderIcon()}</div>
|
<div className="title">{renderIcon()}</div>
|
||||||
<span onClick={onClick}>
|
<span onClick={onClick} className="operations">
|
||||||
<DropDownActions
|
<DropDownActions
|
||||||
menu={{
|
menu={{
|
||||||
items: actions,
|
items: actions,
|
||||||
@@ -230,7 +240,7 @@ const BackendCard: React.FC<BackendCardProps> = ({ data, onSelect }) => {
|
|||||||
</CardName>
|
</CardName>
|
||||||
{renderFrameworks()}
|
{renderFrameworks()}
|
||||||
</Content>
|
</Content>
|
||||||
</Card>
|
</StyledCard>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
|
import ResizeContainer from '@/components/resize-container';
|
||||||
import CardSkeleton from '@/components/templates/card-skelton';
|
import CardSkeleton from '@/components/templates/card-skelton';
|
||||||
import breakpoints from '@/config/breakpoints';
|
|
||||||
import InfiniteScroller from '@/pages/_components/infinite-scroller';
|
import InfiniteScroller from '@/pages/_components/infinite-scroller';
|
||||||
import { useScrollerContext } from '@/pages/_components/infinite-scroller/use-scroller-context';
|
import { useScrollerContext } from '@/pages/_components/infinite-scroller/use-scroller-context';
|
||||||
import { Col, Row, Spin } from 'antd';
|
import { Spin } from 'antd';
|
||||||
import _ from 'lodash';
|
import React from 'react';
|
||||||
import ResizeObserver from 'rc-resize-observer';
|
|
||||||
import React, { useCallback } from 'react';
|
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import BackendCard from './backend-card';
|
import BackendCard from './backend-card';
|
||||||
|
|
||||||
@@ -49,10 +47,9 @@ interface BackendListProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const ListSkeleton: React.FC<{
|
const ListSkeleton: React.FC<{
|
||||||
span: number;
|
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
isFirst: boolean;
|
isFirst: boolean;
|
||||||
}> = ({ span, loading, isFirst }) => {
|
}> = ({ loading, isFirst }) => {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{loading && (
|
{loading && (
|
||||||
@@ -67,7 +64,6 @@ const ListSkeleton: React.FC<{
|
|||||||
{isFirst && (
|
{isFirst && (
|
||||||
<SkeletonWrapper>
|
<SkeletonWrapper>
|
||||||
<CardSkeleton
|
<CardSkeleton
|
||||||
span={span}
|
|
||||||
skeletonProps={{
|
skeletonProps={{
|
||||||
title: false
|
title: false
|
||||||
}}
|
}}
|
||||||
@@ -95,7 +91,6 @@ const CardList: React.FC<BackendListProps> = (props) => {
|
|||||||
resizable = true,
|
resizable = true,
|
||||||
onSelect
|
onSelect
|
||||||
} = props;
|
} = props;
|
||||||
const [span, setSpan] = React.useState(defaultSpan);
|
|
||||||
const {
|
const {
|
||||||
total,
|
total,
|
||||||
current,
|
current,
|
||||||
@@ -104,22 +99,7 @@ const CardList: React.FC<BackendListProps> = (props) => {
|
|||||||
throttleDelay
|
throttleDelay
|
||||||
} = useScrollerContext();
|
} = 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 (
|
return (
|
||||||
<div className="relative" style={{ width: '100%' }}>
|
|
||||||
<ResizeObserver onResize={handleResize} disabled={!resizable}>
|
|
||||||
<InfiniteScroller
|
<InfiniteScroller
|
||||||
total={total}
|
total={total}
|
||||||
current={current}
|
current={current}
|
||||||
@@ -127,19 +107,14 @@ const CardList: React.FC<BackendListProps> = (props) => {
|
|||||||
refresh={refresh}
|
refresh={refresh}
|
||||||
throttleDelay={throttleDelay}
|
throttleDelay={throttleDelay}
|
||||||
>
|
>
|
||||||
<Row gutter={[16, 16]}>
|
<ResizeContainer
|
||||||
{dataList.map((item: any) => {
|
defaultSpan={defaultSpan}
|
||||||
return (
|
resizable={resizable}
|
||||||
<Col span={span} key={item.id}>
|
dataList={dataList}
|
||||||
<BackendCard data={item} onSelect={onSelect} />
|
renderItem={(item) => <BackendCard data={item} onSelect={onSelect} />}
|
||||||
</Col>
|
></ResizeContainer>
|
||||||
);
|
<ListSkeleton loading={loading} isFirst={isFirst} />
|
||||||
})}
|
|
||||||
</Row>
|
|
||||||
<ListSkeleton span={span} loading={loading} isFirst={isFirst} />
|
|
||||||
</InfiniteScroller>
|
</InfiniteScroller>
|
||||||
</ResizeObserver>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,51 +1,17 @@
|
|||||||
import { PageAction } from '@/config';
|
import { PageAction } from '@/config';
|
||||||
import { PageActionType } from '@/config/types';
|
import { PageActionType } from '@/config/types';
|
||||||
import useUserSettings from '@/hooks/use-user-settings';
|
import YamlEditor from '@/pages/_components/yaml-editor';
|
||||||
import { ImportOutlined } from '@ant-design/icons';
|
|
||||||
import { useIntl } from '@umijs/max';
|
import { useIntl } from '@umijs/max';
|
||||||
import { Button, message, Typography, Upload } from 'antd';
|
|
||||||
import { RcFile } from 'antd/lib/upload';
|
|
||||||
import React, {
|
import React, {
|
||||||
forwardRef,
|
forwardRef,
|
||||||
useImperativeHandle,
|
useImperativeHandle,
|
||||||
useRef,
|
useRef,
|
||||||
useState
|
useState
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import styled from 'styled-components';
|
|
||||||
import { yaml2Json, yamlTemplate } from '../config';
|
import { yaml2Json, yamlTemplate } from '../config';
|
||||||
import YamlEditor from './yaml-editor';
|
import createSchema from '../config/schema/create.json';
|
||||||
|
import updateBuiltinSchema from '../config/schema/update-builtin.json';
|
||||||
const { Text } = Typography;
|
import udpateCustomSchema from '../config/schema/update-custom.json';
|
||||||
|
|
||||||
const ErrorText = styled(Text)`
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
padding: 4px 6px;
|
|
||||||
background-color: var(--ant-color-bg-elevated);
|
|
||||||
border-radius: 0 0 var(--ant-border-radius) var(--ant-border-radius);
|
|
||||||
`;
|
|
||||||
|
|
||||||
const Header = styled.div`
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
height: 40px;
|
|
||||||
padding-inline: 10px;
|
|
||||||
border-bottom: 1px solid var(--ant-color-border);
|
|
||||||
background-color: var(--ant-color-fill-tertiary);
|
|
||||||
`;
|
|
||||||
|
|
||||||
const Container = styled.div`
|
|
||||||
position: relative;
|
|
||||||
border: 1px solid var(--ant-color-border);
|
|
||||||
border-radius: var(--ant-border-radius);
|
|
||||||
.monaco-editor .scroll-decoration {
|
|
||||||
box-shadow: none;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
interface ImportYAMLProps {
|
interface ImportYAMLProps {
|
||||||
ref?: any;
|
ref?: any;
|
||||||
@@ -60,7 +26,6 @@ interface ImportYAMLProps {
|
|||||||
const ImportYAML: React.FC<ImportYAMLProps> = forwardRef(
|
const ImportYAML: React.FC<ImportYAMLProps> = forwardRef(
|
||||||
({ actionStatus, content = '' }, ref) => {
|
({ actionStatus, content = '' }, ref) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const { userSettings } = useUserSettings();
|
|
||||||
const editorRef = useRef<any>(null);
|
const editorRef = useRef<any>(null);
|
||||||
const [fileContent, setFileContent] = useState<string>(
|
const [fileContent, setFileContent] = useState<string>(
|
||||||
actionStatus.action === PageAction.CREATE ? yamlTemplate : content
|
actionStatus.action === PageAction.CREATE ? yamlTemplate : content
|
||||||
@@ -71,30 +36,6 @@ const ImportYAML: React.FC<ImportYAMLProps> = forwardRef(
|
|||||||
editorRef.current?.setValue?.(val);
|
editorRef.current?.setValue?.(val);
|
||||||
};
|
};
|
||||||
|
|
||||||
const beforeUpload = (file: RcFile) => {
|
|
||||||
const isYaml =
|
|
||||||
file.type === 'application/x-yaml' ||
|
|
||||||
file.type === 'text/yaml' ||
|
|
||||||
file.name.endsWith('.yaml') ||
|
|
||||||
file.name.endsWith('.yml');
|
|
||||||
if (!isYaml) {
|
|
||||||
message.error('You can only upload YAML file!');
|
|
||||||
}
|
|
||||||
const reader = new FileReader();
|
|
||||||
reader.onload = (e) => {
|
|
||||||
const content = e.target?.result;
|
|
||||||
if (typeof content === 'string') {
|
|
||||||
setFileContent(content);
|
|
||||||
setContent(content);
|
|
||||||
} else {
|
|
||||||
message.error('Failed to read file content!');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
reader.readAsText(file);
|
|
||||||
// Prevent upload
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
const validate = () => {
|
const validate = () => {
|
||||||
const markers = editorRef.current?.validate();
|
const markers = editorRef.current?.validate();
|
||||||
if (markers && markers.length > 0) {
|
if (markers && markers.length > 0) {
|
||||||
@@ -110,45 +51,46 @@ const ImportYAML: React.FC<ImportYAMLProps> = forwardRef(
|
|||||||
const jsonData = yaml2Json(content);
|
const jsonData = yaml2Json(content);
|
||||||
const exsistingVersions = Object.keys(jsonData.version_configs || {});
|
const exsistingVersions = Object.keys(jsonData.version_configs || {});
|
||||||
|
|
||||||
|
// Check backend version rules
|
||||||
if (
|
if (
|
||||||
actionStatus.isBuiltIn &&
|
actionStatus.isBuiltIn &&
|
||||||
exsistingVersions.find((v) => !v.endsWith('-custom'))
|
exsistingVersions.find((v) => !v?.endsWith('-custom'))
|
||||||
) {
|
) {
|
||||||
setError(intl.formatMessage({ id: 'backend.version.no.tips' }));
|
setError(intl.formatMessage({ id: 'backend.version.no.tips' }));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check custom backend name rule
|
||||||
if (
|
if (
|
||||||
!actionStatus.isBuiltIn &&
|
!actionStatus.isBuiltIn &&
|
||||||
actionStatus.action === PageAction.CREATE &&
|
actionStatus.action === PageAction.CREATE &&
|
||||||
!jsonData.backend_name.endsWith('-custom')
|
!jsonData.backend_name?.endsWith('-custom')
|
||||||
) {
|
) {
|
||||||
setError(intl.formatMessage({ id: 'backend.backend.rules.custom' }));
|
setError(intl.formatMessage({ id: 'backend.backend.rules.custom' }));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return content;
|
// Check default version exists
|
||||||
} catch (error) {
|
if (
|
||||||
|
jsonData.default_version &&
|
||||||
|
!exsistingVersions.includes(jsonData.default_version) &&
|
||||||
|
!actionStatus.isBuiltIn
|
||||||
|
) {
|
||||||
|
// the default_version must be in [existingVersions]
|
||||||
|
setError(
|
||||||
|
intl.formatMessage(
|
||||||
|
{ id: 'backend.version.default.not.exists' },
|
||||||
|
{ versions: exsistingVersions.join(', ') }
|
||||||
|
)
|
||||||
|
);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
const renderHeader = () => {
|
return content;
|
||||||
return (
|
} catch (error) {
|
||||||
<Header>
|
setError((error as Error).message);
|
||||||
<span className="title">YAML</span>
|
return false;
|
||||||
<Upload
|
}
|
||||||
name="file"
|
|
||||||
multiple={false}
|
|
||||||
beforeUpload={beforeUpload}
|
|
||||||
showUploadList={false}
|
|
||||||
accept=".yaml,.yml,text/yaml,application/x-yaml"
|
|
||||||
>
|
|
||||||
<Button icon={<ImportOutlined />} type="text" size="small">
|
|
||||||
{intl.formatMessage({ id: 'common.button.import' })}
|
|
||||||
</Button>
|
|
||||||
</Upload>
|
|
||||||
</Header>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
useImperativeHandle(ref, () => ({
|
useImperativeHandle(ref, () => ({
|
||||||
@@ -162,18 +104,23 @@ const ImportYAML: React.FC<ImportYAMLProps> = forwardRef(
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
|
||||||
<YamlEditor
|
<YamlEditor
|
||||||
ref={editorRef}
|
ref={editorRef}
|
||||||
lang="yaml"
|
|
||||||
actionStatus={actionStatus}
|
|
||||||
theme={userSettings?.isDarkTheme ? 'vs-dark' : 'vs-light'}
|
|
||||||
value={fileContent}
|
value={fileContent}
|
||||||
height={'calc(100vh - 260px)'}
|
height={'calc(100vh - 260px)'}
|
||||||
header={renderHeader()}
|
validateMessage={error}
|
||||||
|
onUpload={(content) => {
|
||||||
|
setError('');
|
||||||
|
setContent(content);
|
||||||
|
}}
|
||||||
|
schema={
|
||||||
|
actionStatus.action === PageAction.CREATE
|
||||||
|
? createSchema
|
||||||
|
: actionStatus.isBuiltIn
|
||||||
|
? updateBuiltinSchema
|
||||||
|
: udpateCustomSchema
|
||||||
|
}
|
||||||
></YamlEditor>
|
></YamlEditor>
|
||||||
{error && <ErrorText type="danger">{error}</ErrorText>}
|
|
||||||
</Container>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ const VersionInfoModal: React.FC<VersionInfoModalProps> = ({
|
|||||||
version_no: key,
|
version_no: key,
|
||||||
image_name: value.image_name,
|
image_name: value.image_name,
|
||||||
run_command: value.run_command,
|
run_command: value.run_command,
|
||||||
|
entrypoint: value.entrypoint,
|
||||||
is_default: key === currentData.default_version,
|
is_default: key === currentData.default_version,
|
||||||
availableFrameworks: [
|
availableFrameworks: [
|
||||||
...(value.built_in_frameworks || []),
|
...(value.built_in_frameworks || []),
|
||||||
|
|||||||
@@ -55,12 +55,18 @@ export const backendActions = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
export const json2Yaml = (obj: Record<string, any>) => {
|
export const json2Yaml = (obj: Record<string, any>) => {
|
||||||
|
try {
|
||||||
if (!obj || !Object.keys(obj).length) return '';
|
if (!obj || !Object.keys(obj).length) return '';
|
||||||
const res = jsYaml.dump(JSON.parse(JSON.stringify(obj)));
|
const res = jsYaml.dump(JSON.parse(JSON.stringify(obj)));
|
||||||
return res;
|
return res;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('json2Yaml error:', error);
|
||||||
|
return '';
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const yaml2Json = (input: string) => {
|
export const yaml2Json = (input: string) => {
|
||||||
|
try {
|
||||||
const str = trim(input);
|
const str = trim(input);
|
||||||
const obj = jsYaml.load(str, { schema: SEAL_SCHEMA });
|
const obj = jsYaml.load(str, { schema: SEAL_SCHEMA });
|
||||||
if (typeof obj !== 'object' || obj === null) {
|
if (typeof obj !== 'object' || obj === null) {
|
||||||
@@ -68,6 +74,10 @@ export const yaml2Json = (input: string) => {
|
|||||||
}
|
}
|
||||||
const jsonStr = JSON.stringify(obj);
|
const jsonStr = JSON.stringify(obj);
|
||||||
return JSON.parse(jsonStr);
|
return JSON.parse(jsonStr);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('yaml2Json error:', error);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const customColors = [
|
export const customColors = [
|
||||||
@@ -199,6 +209,7 @@ export const yamlTemplate = `# ----------------------------------------
|
|||||||
# version_configs:
|
# version_configs:
|
||||||
# - image_name: required
|
# - image_name: required
|
||||||
# - run_command: optional
|
# - run_command: optional
|
||||||
|
# - entrypoint: optional
|
||||||
# - custom_framework:
|
# - custom_framework:
|
||||||
# - optional
|
# - optional
|
||||||
# - choose from: ${Object.values(GPUDriverMap).join(', ')}, CPU
|
# - choose from: ${Object.values(GPUDriverMap).join(', ')}, CPU
|
||||||
@@ -214,9 +225,11 @@ version_configs:
|
|||||||
v0.11.0:
|
v0.11.0:
|
||||||
image_name: lm/vllm:latest
|
image_name: lm/vllm:latest
|
||||||
run_command: vllm serve {{model_path}} --port {{port}} --host {{worker_ip}} --served-model-name {{model_name}}
|
run_command: vllm serve {{model_path}} --port {{port}} --host {{worker_ip}} --served-model-name {{model_name}}
|
||||||
|
entrypoint: "/bin/sh -c"
|
||||||
custom_framework: cuda
|
custom_framework: cuda
|
||||||
v0.10.0:
|
v0.10.0:
|
||||||
image_name: lm/vllm:test
|
image_name: lm/vllm:test
|
||||||
|
entrypoint:
|
||||||
run_command:
|
run_command:
|
||||||
custom_framework:
|
custom_framework:
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -48,6 +48,10 @@
|
|||||||
"type": ["string", "null"],
|
"type": ["string", "null"],
|
||||||
"description": "start command"
|
"description": "start command"
|
||||||
},
|
},
|
||||||
|
"entrypoint": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Override the default ENTRYPOINT of the image"
|
||||||
|
},
|
||||||
"custom_framework": {
|
"custom_framework": {
|
||||||
"type": ["string", "null"],
|
"type": ["string", "null"],
|
||||||
"description": "custom framework"
|
"description": "custom framework"
|
||||||
@@ -74,6 +78,10 @@
|
|||||||
"type": ["string", "null"],
|
"type": ["string", "null"],
|
||||||
"description": "start command"
|
"description": "start command"
|
||||||
},
|
},
|
||||||
|
"entrypoint": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Override the default ENTRYPOINT of the image"
|
||||||
|
},
|
||||||
"custom_framework": {
|
"custom_framework": {
|
||||||
"type": ["string", "null"],
|
"type": ["string", "null"],
|
||||||
"description": "custom framework"
|
"description": "custom framework"
|
||||||
|
|||||||
@@ -34,6 +34,10 @@
|
|||||||
"type": ["string", "null"],
|
"type": ["string", "null"],
|
||||||
"description": "start command"
|
"description": "start command"
|
||||||
},
|
},
|
||||||
|
"entrypoint": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Override the default ENTRYPOINT of the image"
|
||||||
|
},
|
||||||
"custom_framework": {
|
"custom_framework": {
|
||||||
"type": ["string", "null"],
|
"type": ["string", "null"],
|
||||||
"description": "custom framework"
|
"description": "custom framework"
|
||||||
|
|||||||
@@ -38,6 +38,10 @@
|
|||||||
"type": ["string", "null"],
|
"type": ["string", "null"],
|
||||||
"description": "start command"
|
"description": "start command"
|
||||||
},
|
},
|
||||||
|
"entrypoint": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Override the default ENTRYPOINT of the image"
|
||||||
|
},
|
||||||
"custom_framework": {
|
"custom_framework": {
|
||||||
"type": ["string", "null"],
|
"type": ["string", "null"],
|
||||||
"description": "custom framework"
|
"description": "custom framework"
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ export interface VersionConfigs {
|
|||||||
is_default: boolean;
|
is_default: boolean;
|
||||||
built_in_frameworks?: string[];
|
built_in_frameworks?: string[];
|
||||||
custom_framework?: string;
|
custom_framework?: string;
|
||||||
|
entrypoint?: string;
|
||||||
version_no?: string;
|
version_no?: string;
|
||||||
is_built_in?: boolean;
|
is_built_in?: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -112,6 +112,16 @@ export const VersionItem: React.FC<VersionItemProps> = ({ data }) => {
|
|||||||
</AutoTooltip>
|
</AutoTooltip>
|
||||||
</InfoItem>
|
</InfoItem>
|
||||||
)}
|
)}
|
||||||
|
{data.entrypoint && (
|
||||||
|
<InfoItem>
|
||||||
|
<span className="label">
|
||||||
|
{intl.formatMessage({ id: 'backend.replaceEntrypoint' })}:
|
||||||
|
</span>
|
||||||
|
<AutoTooltip ghost minWidth={20}>
|
||||||
|
{data.entrypoint}
|
||||||
|
</AutoTooltip>
|
||||||
|
</InfoItem>
|
||||||
|
)}
|
||||||
{data.run_command && (
|
{data.run_command && (
|
||||||
<InfoItem>
|
<InfoItem>
|
||||||
<span className="label">
|
<span className="label">
|
||||||
@@ -168,7 +178,7 @@ const VersionList: React.FC<{ versionConfigs: VersionListItem[] }> = ({
|
|||||||
<span>
|
<span>
|
||||||
{option.label}
|
{option.label}
|
||||||
{data.tips ? (
|
{data.tips ? (
|
||||||
data.locale ? (
|
data.tipLocale ? (
|
||||||
<span className="text-tertiary m-l-4">{` [${intl.formatMessage({ id: data.tips })}]`}</span>
|
<span className="text-tertiary m-l-4">{` [${intl.formatMessage({ id: data.tips })}]`}</span>
|
||||||
) : (
|
) : (
|
||||||
<span className="text-tertiary m-l-4">{` [${data.tips}]`}</span>
|
<span className="text-tertiary m-l-4">{` [${data.tips}]`}</span>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import CollapsibleContainer from '@/components/collapse-container';
|
|||||||
import BaseSelect from '@/components/seal-form/base/select';
|
import BaseSelect from '@/components/seal-form/base/select';
|
||||||
import SealInput from '@/components/seal-form/seal-input';
|
import SealInput from '@/components/seal-form/seal-input';
|
||||||
import SealSelect from '@/components/seal-form/seal-select';
|
import SealSelect from '@/components/seal-form/seal-select';
|
||||||
|
import SealTextArea from '@/components/seal-form/seal-textarea';
|
||||||
import { PageActionType } from '@/config/types';
|
import { PageActionType } from '@/config/types';
|
||||||
import useAppUtils from '@/hooks/use-app-utils';
|
import useAppUtils from '@/hooks/use-app-utils';
|
||||||
import { MinusOutlined, PlusOutlined } from '@ant-design/icons';
|
import { MinusOutlined, PlusOutlined } from '@ant-design/icons';
|
||||||
@@ -72,6 +73,7 @@ const VersionsForm: React.FC<AddModalProps> = ({
|
|||||||
version_no: '',
|
version_no: '',
|
||||||
image_name: '',
|
image_name: '',
|
||||||
run_command: '',
|
run_command: '',
|
||||||
|
entrypoint: '',
|
||||||
isBuiltin: false,
|
isBuiltin: false,
|
||||||
is_default: false
|
is_default: false
|
||||||
}
|
}
|
||||||
@@ -98,6 +100,7 @@ const VersionsForm: React.FC<AddModalProps> = ({
|
|||||||
version_no: '',
|
version_no: '',
|
||||||
image_name: '',
|
image_name: '',
|
||||||
run_command: '',
|
run_command: '',
|
||||||
|
entrypoint: '',
|
||||||
isBuiltin: false,
|
isBuiltin: false,
|
||||||
is_default: false
|
is_default: false
|
||||||
};
|
};
|
||||||
@@ -316,8 +319,12 @@ const VersionsForm: React.FC<AddModalProps> = ({
|
|||||||
>
|
>
|
||||||
<SealInput.Input
|
<SealInput.Input
|
||||||
trim
|
trim
|
||||||
label={intl.formatMessage({ id: 'backend.imageName' })}
|
|
||||||
required
|
required
|
||||||
|
placeholder={intl.formatMessage(
|
||||||
|
{ id: 'common.help.eg' },
|
||||||
|
{ content: 'vllm/vllm-openai:v0.12.0' }
|
||||||
|
)}
|
||||||
|
label={intl.formatMessage({ id: 'backend.imageName' })}
|
||||||
></SealInput.Input>
|
></SealInput.Input>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -338,14 +345,36 @@ const VersionsForm: React.FC<AddModalProps> = ({
|
|||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
<Form.Item name={[name, 'entrypoint']}>
|
||||||
|
<SealInput.TextArea
|
||||||
|
allowClear
|
||||||
|
description={intl.formatMessage({
|
||||||
|
id: 'backend.entrypoint.tips'
|
||||||
|
})}
|
||||||
|
label={intl.formatMessage({
|
||||||
|
id: 'backend.replaceEntrypoint'
|
||||||
|
})}
|
||||||
|
></SealInput.TextArea>
|
||||||
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name={[name, 'run_command']}
|
name={[name, 'run_command']}
|
||||||
style={{ marginBottom: 0 }}
|
style={{ marginBottom: 0 }}
|
||||||
>
|
>
|
||||||
<SealInput.TextArea
|
<SealTextArea
|
||||||
allowClear
|
allowClear
|
||||||
|
alwaysFocus={true}
|
||||||
|
description={intl.formatMessage({
|
||||||
|
id: 'backend.form.defaultExecuteCommand.tips'
|
||||||
|
})}
|
||||||
|
placeholder={intl.formatMessage(
|
||||||
|
{ id: 'common.help.eg' },
|
||||||
|
{
|
||||||
|
content:
|
||||||
|
'vllm serve {{model_path}} --port {{port}} --host {{worker_ip}} --served-model-name {{model_name}}'
|
||||||
|
}
|
||||||
|
)}
|
||||||
label={intl.formatMessage({ id: 'backend.runCommand' })}
|
label={intl.formatMessage({ id: 'backend.runCommand' })}
|
||||||
></SealInput.TextArea>
|
></SealTextArea>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</CollapsibleContainer>
|
</CollapsibleContainer>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user