Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
84304eadb7 | ||
|
|
ab3739219e | ||
|
|
7cf4932f3a | ||
|
|
032f1eefef | ||
|
|
e35be94f7c | ||
|
|
e76e2ad85a | ||
|
|
cb19cd2476 | ||
|
|
c532732ce9 | ||
|
|
2bc10dbb2e | ||
|
|
b634a03ef9 | ||
|
|
ac64bb98ad | ||
|
|
aa49539210 | ||
|
|
1f5678f781 | ||
|
|
7adc089e94 | ||
|
|
6e1e9c07ee | ||
|
|
1d55382f3b | ||
|
|
e68bb0bc4e | ||
|
|
c8e14dade5 | ||
|
|
e4c1a3f5c2 | ||
|
|
cdd6ef9cb3 | ||
|
|
737d43fd16 | ||
|
|
9f5163531c | ||
|
|
df5cf0f4e5 | ||
|
|
e160b64697 | ||
|
|
7d36c6ac4a | ||
|
|
0820edf55f | ||
|
|
12e324893f | ||
|
|
4017c4dd33 | ||
|
|
17b65d80e1 | ||
|
|
47cb3321ab | ||
|
|
547b9f617b | ||
|
|
9e7bd78693 | ||
|
|
c53d60b628 | ||
|
|
9566e0cdb2 | ||
|
|
ed3e779d22 | ||
|
|
3e2133753d | ||
|
|
736a853192 | ||
|
|
08ef2ffa9a | ||
|
|
6cf512cccd | ||
|
|
6f56e614f3 | ||
|
|
8c06862705 | ||
|
|
48159171c2 | ||
|
|
d5d0749b72 | ||
|
|
dc4dd81e11 | ||
|
|
c736e2186d | ||
|
|
7a76bda484 | ||
|
|
778ea36a9d | ||
|
|
04e1e1e566 | ||
|
|
01f66085ab | ||
|
|
ddf723525a | ||
|
|
7b18a92388 | ||
|
|
b985dcee63 | ||
|
|
77e92432ed | ||
|
|
a7b33a928b | ||
|
|
1218c6d096 | ||
|
|
6337431ed4 | ||
|
|
2ba5118bc2 | ||
|
|
12649c7ccf | ||
|
|
5d96932870 | ||
|
|
e6cfa6cf77 | ||
|
|
55d16c5681 | ||
|
|
ab75685955 | ||
|
|
bd0bf0bf63 | ||
|
|
fd050b33d9 | ||
|
|
698090b17a | ||
|
|
6f96260993 | ||
|
|
98a6b19298 | ||
|
|
ded270ff25 | ||
|
|
c8860207e8 | ||
|
|
8d045ffd8b | ||
|
|
fd7437e74a | ||
|
|
807ba866d3 | ||
|
|
5e2808d7c4 | ||
|
|
a26a3ff8f9 | ||
|
|
a2e9f69b96 | ||
|
|
ac800a303a | ||
|
|
dd542a9f29 | ||
|
|
26ce456bde | ||
|
|
7ce1faaf6c | ||
|
|
878bdaa5a7 | ||
|
|
68a7e3f66a | ||
|
|
feccc81c5f | ||
|
|
620045d7a3 | ||
|
|
6dc28d61b0 | ||
|
|
754cc29793 | ||
|
|
ed525509ea | ||
|
|
2b6f26e6c8 | ||
|
|
09821aab85 | ||
|
|
636190b8e6 | ||
|
|
e420c22a45 | ||
|
|
993f6edcc4 |
@@ -31,5 +31,6 @@ export default function createProxyTable(target?: string) {
|
|||||||
},
|
},
|
||||||
{}
|
{}
|
||||||
);
|
);
|
||||||
|
|
||||||
return proxyTable;
|
return proxyTable;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -338,6 +338,20 @@ const baseRoutes = [
|
|||||||
selectedIcon: 'icon-usage-filled',
|
selectedIcon: 'icon-usage-filled',
|
||||||
defaultIcon: 'icon-usage-outlined',
|
defaultIcon: 'icon-usage-outlined',
|
||||||
component: './usage/index'
|
component: './usage/index'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'billing',
|
||||||
|
path: '/usage/billing',
|
||||||
|
key: 'billing',
|
||||||
|
icon: 'icon-billing-outlined',
|
||||||
|
selectedIcon: 'icon-billing-filled',
|
||||||
|
defaultIcon: 'icon-billing-outlined',
|
||||||
|
hideInMenu: process.env.ENABLE_ENTERPRISE === 'true',
|
||||||
|
// OSS exposes the menu as a teaser for the enterprise billing
|
||||||
|
// module. The page itself just renders an upsell notice — the real
|
||||||
|
// billing UI lives in the enterprise plugin and shadows this route
|
||||||
|
// via `routes.extensions.ts`.
|
||||||
|
component: './billing'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
+1
-1
@@ -17,7 +17,7 @@
|
|||||||
"@ant-design/pro-components": "3.1.0-0",
|
"@ant-design/pro-components": "3.1.0-0",
|
||||||
"@antv/g6": "^5.0.51",
|
"@antv/g6": "^5.0.51",
|
||||||
"@braintree/sanitize-url": "^7.1.1",
|
"@braintree/sanitize-url": "^7.1.1",
|
||||||
"@gpustack/core-ui": "^1.0.22",
|
"@gpustack/core-ui": "^1.0.27",
|
||||||
"@huggingface/gguf": "^0.1.7",
|
"@huggingface/gguf": "^0.1.7",
|
||||||
"@huggingface/hub": "^0.15.1",
|
"@huggingface/hub": "^0.15.1",
|
||||||
"@huggingface/tasks": "^0.11.6",
|
"@huggingface/tasks": "^0.11.6",
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ export default (api: IApi) => {
|
|||||||
const info = JSON.parse(process.env.VERSION || '{}');
|
const info = JSON.parse(process.env.VERSION || '{}');
|
||||||
const env = process.env.NODE_ENV;
|
const env = process.env.NODE_ENV;
|
||||||
|
|
||||||
|
$('html').attr('lang', 'en');
|
||||||
|
|
||||||
$('html').attr('data-env', env);
|
$('html').attr('data-env', env);
|
||||||
|
|
||||||
$('html').attr(
|
$('html').attr(
|
||||||
|
|||||||
Generated
+5
-5
@@ -24,8 +24,8 @@ importers:
|
|||||||
specifier: ^7.1.1
|
specifier: ^7.1.1
|
||||||
version: 7.1.2
|
version: 7.1.2
|
||||||
'@gpustack/core-ui':
|
'@gpustack/core-ui':
|
||||||
specifier: ^1.0.22
|
specifier: ^1.0.27
|
||||||
version: 1.0.22(czdvzceysqw7iv6pct2ucnb23e)
|
version: 1.0.27(czdvzceysqw7iv6pct2ucnb23e)
|
||||||
'@huggingface/gguf':
|
'@huggingface/gguf':
|
||||||
specifier: ^0.1.7
|
specifier: ^0.1.7
|
||||||
version: 0.1.18
|
version: 0.1.18
|
||||||
@@ -1484,8 +1484,8 @@ packages:
|
|||||||
resolution: {integrity: sha512-KWk80UPIzPmUg+P0rKh6TqspRw0G6eux1PuJr+zz47ftMaZ9QDwbGzHZbtzWkl5hgayM/qrKRutllRC7D/vVXQ==, tarball: https://registry.npmjs.org/@formatjs/intl-utils/-/intl-utils-2.3.0.tgz}
|
resolution: {integrity: sha512-KWk80UPIzPmUg+P0rKh6TqspRw0G6eux1PuJr+zz47ftMaZ9QDwbGzHZbtzWkl5hgayM/qrKRutllRC7D/vVXQ==, tarball: https://registry.npmjs.org/@formatjs/intl-utils/-/intl-utils-2.3.0.tgz}
|
||||||
deprecated: the package is rather renamed to @formatjs/ecma-abstract with some changes in functionality (primarily selectUnit is removed and we don't plan to make any further changes to this package
|
deprecated: the package is rather renamed to @formatjs/ecma-abstract with some changes in functionality (primarily selectUnit is removed and we don't plan to make any further changes to this package
|
||||||
|
|
||||||
'@gpustack/core-ui@1.0.22':
|
'@gpustack/core-ui@1.0.27':
|
||||||
resolution: {integrity: sha512-uFLGalziwuojjMIeXMlCZn3tLRGA6PCa0JxLxa/+kKP+xAsy7Fcxgh4PerbTyk+P+hKnYv8cD+DPpkObDGqJgg==, tarball: https://registry.npmjs.org/@gpustack/core-ui/-/core-ui-1.0.22.tgz}
|
resolution: {integrity: sha512-m3ue0EHFKULla0mpnpxZwn9LVaGKS+HnuzQYSBECQa4vaP8MEEQsR7oFBtG8bhWUWYA9VzP6k7fjozDkP6TymA==, tarball: https://registry.npmjs.org/@gpustack/core-ui/-/core-ui-1.0.27.tgz}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@ant-design/icons': '>=6.0.0'
|
'@ant-design/icons': '>=6.0.0'
|
||||||
'@ant-design/pro-components': 3.1.0-0
|
'@ant-design/pro-components': 3.1.0-0
|
||||||
@@ -10808,7 +10808,7 @@ snapshots:
|
|||||||
|
|
||||||
'@formatjs/intl-utils@2.3.0': {}
|
'@formatjs/intl-utils@2.3.0': {}
|
||||||
|
|
||||||
'@gpustack/core-ui@1.0.22(czdvzceysqw7iv6pct2ucnb23e)':
|
'@gpustack/core-ui@1.0.27(czdvzceysqw7iv6pct2ucnb23e)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@ant-design/icons': 6.2.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
'@ant-design/icons': 6.2.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||||
'@ant-design/pro-components': 3.1.0-0(antd@6.3.7(date-fns@2.30.0)(moment@2.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
'@ant-design/pro-components': 3.1.0-0(antd@6.3.7(date-fns@2.30.0)(moment@2.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
<svg height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>AlibabaCloud</title><path d="M14.752 4.64h5.274C22.242 4.64 24 6.475 24 8.691V15.8a3.947 3.947 0 01-3.974 3.975h-5.274l1.299-1.835 3.822-1.222c.688-.23 1.146-.918 1.146-1.605v-5.81c0-.687-.458-1.375-1.146-1.605L16.05 6.475l-1.3-1.835zM2.98 15.111c0 .688.46 1.376 1.147 1.606l3.822 1.146 1.3 1.835H3.974A3.947 3.947 0 010 15.723V8.69c0-2.216 1.758-4.05 3.975-4.05h5.273L7.95 6.474 4.127 7.697c-.688.23-1.146.918-1.146 1.606v5.808z" fill="#FF6A00"></path><path d="M16.051 11.213H8.025v1.835h8.026v-1.835z" fill="#FF6A00"></path></svg>
|
||||||
|
After Width: | Height: | Size: 656 B |
@@ -0,0 +1 @@
|
|||||||
|
<svg height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>BaiLian</title><path d="M6.336 8.919v6.162l5.335-3.083L6.337 8.92z" fill="#1C54E3"></path><path d="M21.394 5.288s-.006-.006-.01-.006L17.01 2.754 6.336 8.92l5.335 3.082 9.701-5.6.016-.01a.635.635 0 00.006-1.1v-.003z" fill="#AA9AFF"></path><path d="M21.71 12.465a.62.62 0 00-.316.085s-.006 0-.009.003l-4.375 2.528 5.05 2.915h.006a2.06 2.06 0 00.28-1.04v-3.855a.637.637 0 00-.636-.636z" fill="#00EAD1"></path><path d="M22.06 17.996l-5.05-2.915L6.34 21.242l4.27 2.465s.016.006.022.012a2.102 2.102 0 002.093 0c.006-.003.016-.006.022-.012l8.538-4.93c.003 0 .006-.003.01-.006.321-.183.589-.45.775-.772h-.006l-.004-.003z" fill="#00CEC9"></path><path d="M11.672 11.998l-5.336 3.083-1.444.832-3.605 2.083H1.28c.173.303.416.555.709.738l.078.044.016.01.02.012 4.232 2.442 10.671-6.161-5.335-3.082z" fill="#00EAD1"></path><path d="M12.74.29c-.1-.06-.208-.107-.315-.148-.02-.006-.038-.016-.057-.022a2.121 2.121 0 00-.7-.12c-.233 0-.457.038-.668.11l-.031.01a2.196 2.196 0 00-.372.17L2.068 5.222s-.003 0-.006.003c-.324.183-.592.451-.781.773h.006l5.049 2.918L17.01 2.758 12.74.29z" fill="#7347FF"></path><path d="M1.287 6.001H1.28A2.06 2.06 0 001 7.041v9.915c0 .378.1.735.28 1.043h.007l5.049-2.918V8.919l-5.05-2.918z" fill="#0423DA"></path></svg>
|
||||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -63,6 +63,7 @@ export const fromClusterCreationAtom = atom(false);
|
|||||||
export const clusterSessionAtom = atom<{
|
export const clusterSessionAtom = atom<{
|
||||||
firstAddWorker: boolean;
|
firstAddWorker: boolean;
|
||||||
firstAddCluster: boolean;
|
firstAddCluster: boolean;
|
||||||
|
presetClusterType?: 'model' | 'gpu';
|
||||||
// Provider to preselect when the create flow opens — set by the
|
// Provider to preselect when the create flow opens — set by the
|
||||||
// empty-state CTA on feature pages that need a specific provider
|
// empty-state CTA on feature pages that need a specific provider
|
||||||
// (e.g. GPU Service can only schedule on Kubernetes, so its
|
// (e.g. GPU Service can only schedule on Kubernetes, so its
|
||||||
|
|||||||
@@ -55,3 +55,10 @@ export const userSettingsHelperAtom = atom(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
export const hideModalTemporarilyAtom = atom<boolean>(false);
|
export const hideModalTemporarilyAtom = atom<boolean>(false);
|
||||||
|
|
||||||
|
export const collapsedMenuGroupsAtom = atomWithStorage<string[]>(
|
||||||
|
'collapsedMenuGroups',
|
||||||
|
[],
|
||||||
|
undefined,
|
||||||
|
{ getOnInit: true }
|
||||||
|
);
|
||||||
|
|||||||
+1
-4
@@ -32,10 +32,7 @@ export const UpdateCheckAtom = atom<{
|
|||||||
latest_version: ''
|
latest_version: ''
|
||||||
});
|
});
|
||||||
|
|
||||||
export const initialPasswordAtom = atomWithStorage<string>(
|
export const initialPasswordAtom = atom<string>('');
|
||||||
'initialPassword',
|
|
||||||
''
|
|
||||||
);
|
|
||||||
|
|
||||||
// Namespace the server creates for an Org's resources on each Kubernetes
|
// Namespace the server creates for an Org's resources on each Kubernetes
|
||||||
// cluster. The format must match the backend's ``get_namespace_name``
|
// cluster. The format must match the backend's ``get_namespace_name``
|
||||||
|
|||||||
@@ -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=1780035199453') format('woff2'),
|
src: url('iconfont.woff2?t=1780907696624') format('woff2'),
|
||||||
url('iconfont.woff?t=1780035199453') format('woff'),
|
url('iconfont.woff?t=1780907696624') format('woff'),
|
||||||
url('iconfont.ttf?t=1780035199453') format('truetype');
|
url('iconfont.ttf?t=1780907696624') format('truetype');
|
||||||
}
|
}
|
||||||
|
|
||||||
.iconfont {
|
.iconfont {
|
||||||
@@ -13,6 +13,18 @@
|
|||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.icon-billing-outlined:before {
|
||||||
|
content: "\e70d";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-billing-filled:before {
|
||||||
|
content: "\e70e";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-fingerprint:before {
|
||||||
|
content: "\e70c";
|
||||||
|
}
|
||||||
|
|
||||||
.icon-database-outlined:before {
|
.icon-database-outlined:before {
|
||||||
content: "\e70a";
|
content: "\e70a";
|
||||||
}
|
}
|
||||||
|
|||||||
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": "47798379",
|
||||||
|
"name": "request_quote_24dp_1F1F1F_FILL0_wght300_GRAD0_opsz",
|
||||||
|
"font_class": "billing-outlined",
|
||||||
|
"unicode": "e70d",
|
||||||
|
"unicode_decimal": 59149
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"icon_id": "47798378",
|
||||||
|
"name": "request_quote_24dp_1F1F1F_FILL1_wght300_GRAD0_opsz",
|
||||||
|
"font_class": "billing-filled",
|
||||||
|
"unicode": "e70e",
|
||||||
|
"unicode_decimal": 59150
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"icon_id": "47761129",
|
||||||
|
"name": "fingerprint",
|
||||||
|
"font_class": "fingerprint",
|
||||||
|
"unicode": "e70c",
|
||||||
|
"unicode_decimal": 59148
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"icon_id": "47654402",
|
"icon_id": "47654402",
|
||||||
"name": "database_24dp_1F1F1F_FILL0_wght300_GRAD0_opsz24 (1",
|
"name": "database_24dp_1F1F1F_FILL0_wght300_GRAD0_opsz24 (1",
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -276,7 +276,6 @@ body {
|
|||||||
border-radius: var(--table-td-radius);
|
border-radius: var(--table-td-radius);
|
||||||
|
|
||||||
> td {
|
> td {
|
||||||
background-color: unset !important;
|
|
||||||
border-bottom: 1px solid var(--ant-color-split);
|
border-bottom: 1px solid var(--ant-color-split);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ export function useQueryDataList<
|
|||||||
Response = Array<ListItem>
|
Response = Array<ListItem>
|
||||||
>(option: {
|
>(option: {
|
||||||
key: string;
|
key: string;
|
||||||
|
manual?: boolean;
|
||||||
responseType?: 'array' | 'object';
|
responseType?: 'array' | 'object';
|
||||||
fetchList: (
|
fetchList: (
|
||||||
params: Params,
|
params: Params,
|
||||||
@@ -38,6 +39,7 @@ export function useQueryDataList<
|
|||||||
fetchList,
|
fetchList,
|
||||||
getLabel,
|
getLabel,
|
||||||
getValue,
|
getValue,
|
||||||
|
manual = true,
|
||||||
responseType = 'array',
|
responseType = 'array',
|
||||||
errorMsg
|
errorMsg
|
||||||
} = option;
|
} = option;
|
||||||
@@ -70,7 +72,7 @@ export function useQueryDataList<
|
|||||||
return responseType === 'array' ? res.items || [] : res;
|
return responseType === 'array' ? res.items || [] : res;
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
manual: true,
|
manual: manual,
|
||||||
debounceWait: option.debounceWait || 300,
|
debounceWait: option.debounceWait || 300,
|
||||||
onSuccess: () => {},
|
onSuccess: () => {},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
@@ -105,16 +107,18 @@ export function useQueryDataList<
|
|||||||
export function useQueryData<Detail, Params = any>(option: {
|
export function useQueryData<Detail, Params = any>(option: {
|
||||||
key: string;
|
key: string;
|
||||||
delay?: number;
|
delay?: number;
|
||||||
|
manual?: boolean;
|
||||||
fetchDetail: (params: Params, options?: any) => Promise<Detail>;
|
fetchDetail: (params: Params, options?: any) => Promise<Detail>;
|
||||||
getData?: (response: Detail, params?: any) => any;
|
getData?: (response: Detail, params?: any) => any;
|
||||||
errorMsg?: string;
|
errorMsg?: string;
|
||||||
}): {
|
}): {
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
detailData: Detail;
|
detailData: Detail;
|
||||||
|
manual?: boolean;
|
||||||
cancelRequest: () => void;
|
cancelRequest: () => void;
|
||||||
fetchData: (params: Params, extra?: any) => Promise<Detail>;
|
fetchData: (params: Params, extra?: any) => Promise<Detail>;
|
||||||
} {
|
} {
|
||||||
const { key, fetchDetail, getData, errorMsg, delay } = option;
|
const { key, fetchDetail, getData, errorMsg, delay, manual = true } = option;
|
||||||
const axiosTokenRef = useRef<CancelTokenSource | null>(null);
|
const axiosTokenRef = useRef<CancelTokenSource | null>(null);
|
||||||
const [detailData, setDetailData] = useState<Detail>({} as Detail);
|
const [detailData, setDetailData] = useState<Detail>({} as Detail);
|
||||||
|
|
||||||
@@ -142,7 +146,7 @@ export function useQueryData<Detail, Params = any>(option: {
|
|||||||
return res;
|
return res;
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
manual: true,
|
manual: manual,
|
||||||
onSuccess: () => {},
|
onSuccess: () => {},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
message.error(
|
message.error(
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import { useMemoizedFn } from 'ahooks';
|
||||||
|
import { useRef, useState } from 'react';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Centralizes the anti-double-submit + anti-deadlock logic shared by the
|
||||||
|
* "add/edit" modals.
|
||||||
|
*
|
||||||
|
* The lock has to straddle antd's async field validation, whose result comes
|
||||||
|
* back through two separate callbacks (`onFinish` / `onFinishFailed`). So the
|
||||||
|
* three wiring points below are intentional and map 1:1 to those anchors:
|
||||||
|
*
|
||||||
|
* - `guard` wrap the submit trigger (button / ModalFooter onOk). Blocks
|
||||||
|
* re-entry while a submit is already in flight, then fires submit.
|
||||||
|
* - `run` wrap the `onFinish` handler. Holds the lock + loading until the
|
||||||
|
* `onOk` request settles (success or error).
|
||||||
|
* - `release` pass as the form's `onFinishFailed`. Releases the lock when
|
||||||
|
* validation fails, otherwise the button would dead-lock.
|
||||||
|
*/
|
||||||
|
export default function useSubmitLock() {
|
||||||
|
const [loading, setLoading] = useState<boolean>(false);
|
||||||
|
const lockRef = useRef<boolean>(false);
|
||||||
|
|
||||||
|
const release = useMemoizedFn(() => {
|
||||||
|
setLoading(false);
|
||||||
|
lockRef.current = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
const guard = useMemoizedFn((submit: () => void) => {
|
||||||
|
if (lockRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
lockRef.current = true;
|
||||||
|
submit();
|
||||||
|
});
|
||||||
|
|
||||||
|
const run = useMemoizedFn(async (task: () => void | Promise<void>) => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
await task();
|
||||||
|
} finally {
|
||||||
|
release();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { loading, guard, run, release };
|
||||||
|
}
|
||||||
@@ -89,6 +89,10 @@ export function useUpdateChunkedList(options: {
|
|||||||
...cacheDataListRef.current
|
...cacheDataListRef.current
|
||||||
].slice(0, limit);
|
].slice(0, limit);
|
||||||
|
|
||||||
|
options.setDataList?.([...cacheDataListRef.current], {
|
||||||
|
createdIds: newDataList.map((item) => item.id)
|
||||||
|
});
|
||||||
|
|
||||||
options.onCreate?.(latestCreateList);
|
options.onCreate?.(latestCreateList);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -63,8 +63,7 @@ export default function useWatchList<T = Record<string, any>>(API: string) {
|
|||||||
listRequestTokenRef.current?.cancel?.();
|
listRequestTokenRef.current?.cancel?.();
|
||||||
listRequestTokenRef.current = createAxiosToken();
|
listRequestTokenRef.current = createAxiosToken();
|
||||||
const params = {
|
const params = {
|
||||||
page: 1,
|
page: -1
|
||||||
perPage: 100
|
|
||||||
};
|
};
|
||||||
const res: any = await queryAllDataList(params, {
|
const res: any = await queryAllDataList(params, {
|
||||||
token: listRequestTokenRef.current.token
|
token: listRequestTokenRef.current.token
|
||||||
@@ -86,6 +85,7 @@ export default function useWatchList<T = Record<string, any>>(API: string) {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
watchDataList,
|
watchDataList,
|
||||||
|
setWatchDataList,
|
||||||
deleteItemFromCache: handleDeleteItemFromCache
|
deleteItemFromCache: handleDeleteItemFromCache
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ 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';
|
||||||
|
import GithubStar from './github-star';
|
||||||
|
|
||||||
const NewLabel = styled.span`
|
const NewLabel = styled.span`
|
||||||
position: relative;
|
position: relative;
|
||||||
@@ -260,6 +261,7 @@ export const ExtraContent = (props: { isDarkTheme?: boolean }) => {
|
|||||||
<Wrapper>
|
<Wrapper>
|
||||||
{contextHolder}
|
{contextHolder}
|
||||||
<PluginExtraField name="OrgSwitcher" isDarkTheme={isDarkTheme} />
|
<PluginExtraField name="OrgSwitcher" isDarkTheme={isDarkTheme} />
|
||||||
|
{process.env.ENABLE_ENTERPRISE !== 'true' && <GithubStar />}
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
|
|||||||
@@ -0,0 +1,140 @@
|
|||||||
|
import externalLinks from '@/constants/external-links';
|
||||||
|
import { GithubFilled } from '@ant-design/icons';
|
||||||
|
import { useIntl } from '@umijs/max';
|
||||||
|
import { Tooltip } from 'antd';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
const REPO = 'gpustack/gpustack';
|
||||||
|
const CACHE_KEY = 'gpustack:github-stars';
|
||||||
|
const CACHE_TTL = 24 * 60 * 60 * 1000;
|
||||||
|
const FETCH_TIMEOUT = 4000;
|
||||||
|
|
||||||
|
const StarLink = styled.a`
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: stretch;
|
||||||
|
height: 24px;
|
||||||
|
border-radius: var(--ant-border-radius);
|
||||||
|
border: 1px solid var(--ant-color-border-secondary);
|
||||||
|
background-color: var(--ant-color-bg-container);
|
||||||
|
color: var(--ant-color-text-secondary);
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
transition:
|
||||||
|
border-color 0.2s,
|
||||||
|
color 0.2s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: var(--ant-color-border);
|
||||||
|
color: var(--ant-color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.seg {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 0 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.seg + .seg {
|
||||||
|
border-left: 1px solid var(--ant-color-border-secondary);
|
||||||
|
background-color: var(--ant-color-fill-quaternary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.anticon {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.count {
|
||||||
|
font-weight: 500;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
min-width: 1.5em;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const formatCount = (n: number): string => {
|
||||||
|
if (n >= 1000) {
|
||||||
|
const k = n / 1000;
|
||||||
|
return k >= 10 ? `${Math.round(k)}k` : `${k.toFixed(1)}k`;
|
||||||
|
}
|
||||||
|
return String(n);
|
||||||
|
};
|
||||||
|
|
||||||
|
type CacheEntry = { value: number; time: number };
|
||||||
|
|
||||||
|
const readCache = (): CacheEntry | null => {
|
||||||
|
try {
|
||||||
|
const raw = localStorage.getItem(CACHE_KEY);
|
||||||
|
if (!raw) return null;
|
||||||
|
const parsed = JSON.parse(raw);
|
||||||
|
if (typeof parsed?.value !== 'number' || typeof parsed?.time !== 'number') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return parsed;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const writeCache = (value: number) => {
|
||||||
|
try {
|
||||||
|
localStorage.setItem(
|
||||||
|
CACHE_KEY,
|
||||||
|
JSON.stringify({ value, time: Date.now() })
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
// ignore quota errors
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const GithubStar = () => {
|
||||||
|
const intl = useIntl();
|
||||||
|
const [count, setCount] = useState<number | null>(
|
||||||
|
() => readCache()?.value ?? null
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const cached = readCache();
|
||||||
|
const fresh = cached && Date.now() - cached.time < CACHE_TTL;
|
||||||
|
if (fresh) return;
|
||||||
|
|
||||||
|
const controller = new AbortController();
|
||||||
|
const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT);
|
||||||
|
|
||||||
|
fetch(`https://api.github.com/repos/${REPO}`, { signal: controller.signal })
|
||||||
|
.then((r) => (r.ok ? r.json() : null))
|
||||||
|
.then((data) => {
|
||||||
|
if (!data || typeof data.stargazers_count !== 'number') return;
|
||||||
|
setCount(data.stargazers_count);
|
||||||
|
writeCache(data.stargazers_count);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
// offline, blocked, rate-limited — stay hidden if no cache
|
||||||
|
})
|
||||||
|
.finally(() => clearTimeout(timer));
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearTimeout(timer);
|
||||||
|
controller.abort();
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip title={intl.formatMessage({ id: 'common.github.star.tooltip' })}>
|
||||||
|
<StarLink href={externalLinks.github} target="_blank" rel="noreferrer">
|
||||||
|
<span className="seg">
|
||||||
|
<GithubFilled />
|
||||||
|
</span>
|
||||||
|
<span className="seg">
|
||||||
|
<span className="count">
|
||||||
|
{count != null ? formatCount(count) : 'Star'}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</StarLink>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default GithubStar;
|
||||||
@@ -327,7 +327,7 @@ export default (props: any) => {
|
|||||||
config={{
|
config={{
|
||||||
apiBaseUrl: GPUSTACK_API_BASE_URL,
|
apiBaseUrl: GPUSTACK_API_BASE_URL,
|
||||||
theme: userSettings.theme,
|
theme: userSettings.theme,
|
||||||
iconUrl: '//at.alicdn.com/t/c/font_4613488_rwchdketjg8.js',
|
iconUrl: '',
|
||||||
isDarkTheme: userSettings.isDarkTheme,
|
isDarkTheme: userSettings.isDarkTheme,
|
||||||
defaultColorPrimary: COLOR_PRIMARY
|
defaultColorPrimary: COLOR_PRIMARY
|
||||||
}}
|
}}
|
||||||
|
|||||||
+21
-11
@@ -1,9 +1,11 @@
|
|||||||
|
import { collapsedMenuGroupsAtom } from '@/atoms/settings';
|
||||||
import { CaretDownOutlined } from '@ant-design/icons';
|
import { CaretDownOutlined } from '@ant-design/icons';
|
||||||
import { IconFont, OverlayScroller } from '@gpustack/core-ui';
|
import { IconFont, OverlayScroller } from '@gpustack/core-ui';
|
||||||
import { Link, useLocation } from '@umijs/max';
|
import { Link, useLocation } from '@umijs/max';
|
||||||
import { Tooltip } from 'antd';
|
import { Tooltip } from 'antd';
|
||||||
import { createStyles, type FullToken } from 'antd-style';
|
import { createStyles, type FullToken } from 'antd-style';
|
||||||
import React, { useMemo, useState } from 'react';
|
import { useAtom } from 'jotai';
|
||||||
|
import React, { useMemo } from 'react';
|
||||||
|
|
||||||
interface MenuItem {
|
interface MenuItem {
|
||||||
icon?: string;
|
icon?: string;
|
||||||
@@ -144,8 +146,19 @@ const SiderMenu: React.FC<SiderMenuProps> = (props) => {
|
|||||||
const { menuData, collapsed } = props;
|
const { menuData, collapsed } = props;
|
||||||
const { styles, cx } = useStyles();
|
const { styles, cx } = useStyles();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const [collapseKeys, setCollapseKeys] = useState<Set<string>>(new Set());
|
const [storedCollapsedGroups, setCollapsedGroups] = useAtom(
|
||||||
console.log('SiderMenu', location.pathname);
|
collapsedMenuGroupsAtom
|
||||||
|
);
|
||||||
|
// atomWithStorage falls back to the initial value on JSON parse
|
||||||
|
// errors, but not when the stored value is valid JSON of another
|
||||||
|
// shape — normalize so array methods below can't throw.
|
||||||
|
const collapsedGroups = Array.isArray(storedCollapsedGroups)
|
||||||
|
? storedCollapsedGroups
|
||||||
|
: [];
|
||||||
|
const collapseKeys = useMemo(
|
||||||
|
() => new Set(collapsedGroups),
|
||||||
|
[collapsedGroups]
|
||||||
|
);
|
||||||
|
|
||||||
const dividerStyles = useMemo(() => {
|
const dividerStyles = useMemo(() => {
|
||||||
if (collapsed) {
|
if (collapsed) {
|
||||||
@@ -164,14 +177,11 @@ const SiderMenu: React.FC<SiderMenuProps> = (props) => {
|
|||||||
const handleToggleGroup = (e: any, menuGroup: any) => {
|
const handleToggleGroup = (e: any, menuGroup: any) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
||||||
console.log('handleToggleGroup', menuGroup.key);
|
setCollapsedGroups(
|
||||||
|
collapsedGroups.includes(menuGroup.key)
|
||||||
if (collapseKeys.has(menuGroup.key)) {
|
? collapsedGroups.filter((key) => key !== menuGroup.key)
|
||||||
collapseKeys.delete(menuGroup.key);
|
: [...collapsedGroups, menuGroup.key]
|
||||||
} else {
|
);
|
||||||
collapseKeys.add(menuGroup.key);
|
|
||||||
}
|
|
||||||
setCollapseKeys(new Set(collapseKeys));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const menuItemRender = (menuItem: MenuItem, key: string) => {
|
const menuItemRender = (menuItem: MenuItem, key: string) => {
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export default {
|
|||||||
'ai.provider.ollama': 'Ollama',
|
'ai.provider.ollama': 'Ollama',
|
||||||
'ai.provider.openai': 'OpenAI',
|
'ai.provider.openai': 'OpenAI',
|
||||||
'ai.provider.openrouter': 'OpenRouter',
|
'ai.provider.openrouter': 'OpenRouter',
|
||||||
'ai.provider.qwen': 'Qwen',
|
'ai.provider.qwen': 'Alibaba Cloud Model Studio',
|
||||||
'ai.provider.spark': 'Spark',
|
'ai.provider.spark': 'Spark',
|
||||||
'ai.provider.stepfun': 'StepFun',
|
'ai.provider.stepfun': 'StepFun',
|
||||||
'ai.provider.together-ai': 'TogetherAI',
|
'ai.provider.together-ai': 'TogetherAI',
|
||||||
|
|||||||
@@ -23,11 +23,12 @@ export default {
|
|||||||
'backend.form.defaultExecuteCommand': 'Default Execution Command',
|
'backend.form.defaultExecuteCommand': 'Default Execution Command',
|
||||||
'backend.form.defaultExecuteCommand.tips': `'{{'model_path'}}', '{{'port'}}', '{{'worker_ip'}}' and '{{'model_name'}}' are placeholders that will be substituted with the actual values during deployment.`,
|
'backend.form.defaultExecuteCommand.tips': `'{{'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.parameterFormat': 'Parameter Format',
|
'backend.form.flagFormat': 'Flag Format',
|
||||||
'backend.form.parameterFormat.default': 'Backend Default',
|
'backend.form.flagFormat.tips':
|
||||||
'backend.form.parameterFormat.space': 'Space (--key value)',
|
'The format applied between an option and its value. Leave empty to keep each parameter as entered, without normalizing it.',
|
||||||
'backend.form.parameterFormat.equal': 'Equal (--key=value)',
|
'backend.form.flagFormat.space': 'Space Separated (--key value)',
|
||||||
'backend.form.commonParameters': 'Common Parameters',
|
'backend.form.flagFormat.equal': 'Equal Sign (--key=value)',
|
||||||
|
'backend.form.commonParameters': 'Common Backend Parameters',
|
||||||
'backend.form.commonParameters.tips':
|
'backend.form.commonParameters.tips':
|
||||||
'Shown as suggestions in the backend parameters input during deployment.',
|
'Shown as suggestions in the backend parameters input during deployment.',
|
||||||
'backend.form.versionConfig': 'Versions Config',
|
'backend.form.versionConfig': 'Versions Config',
|
||||||
|
|||||||
@@ -37,10 +37,10 @@ export default {
|
|||||||
'Stress test for long-context handling. Evaluates KV cache behavior, memory usage, and backend stability.',
|
'Stress test for long-context handling. Evaluates KV cache behavior, memory usage, and backend stability.',
|
||||||
'benchmark.form.profile.heavy.tips':
|
'benchmark.form.profile.heavy.tips':
|
||||||
'Decode-heavy generation benchmark. Measures sustained decoding speed and output token throughput.',
|
'Decode-heavy generation benchmark. Measures sustained decoding speed and output token throughput.',
|
||||||
'benchmark.table.filter.bygpu': 'Filter by GPU',
|
'benchmark.table.filter.bygpu': 'Search by GPU',
|
||||||
'benchmark.table.filter.bymodel': 'Filter by Model',
|
'benchmark.table.filter.bymodel': 'Search by model',
|
||||||
'benchmark.table.filter.bydataset': 'Filter by Dataset',
|
'benchmark.table.filter.bydataset': 'Filter by Dataset',
|
||||||
'benchmark.table.filter.byProfile': 'Filter by Profile',
|
'benchmark.table.filter.byProfile': 'Filter by profile',
|
||||||
'benchmark.table.avg': 'Avg',
|
'benchmark.table.avg': 'Avg',
|
||||||
'benchmark.table.columnSettings': 'Column Settings',
|
'benchmark.table.columnSettings': 'Column Settings',
|
||||||
'benchmark.detail.summary.results': 'Test Results',
|
'benchmark.detail.summary.results': 'Test Results',
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
export default {
|
||||||
|
'billing.upsell.title': 'Billing is an Enterprise feature',
|
||||||
|
'billing.upsell.subtitle':
|
||||||
|
'Track spend, generate invoices, and enforce budgets across teams. Upgrade to GPUStack Enterprise to manage billing.',
|
||||||
|
'billing.upsell.featuresTitle': 'What you get in Enterprise',
|
||||||
|
'billing.upsell.feature.usage':
|
||||||
|
'See cost breakdowns by organization, user, and model',
|
||||||
|
'billing.upsell.feature.invoices':
|
||||||
|
'Generate invoices and export billing reports',
|
||||||
|
'billing.upsell.feature.budgets':
|
||||||
|
'Set budgets and spending limits with alerts',
|
||||||
|
'billing.upsell.feature.chargeback':
|
||||||
|
'Attribute and charge back usage to teams and projects',
|
||||||
|
'billing.upsell.cta': 'Learn about Enterprise'
|
||||||
|
};
|
||||||
@@ -70,8 +70,13 @@ export default {
|
|||||||
'For <span class="bold-text">non-Docker</span> clusters, please register clusters or manage worker pools from the Clusters page.',
|
'For <span class="bold-text">non-Docker</span> clusters, please register clusters or manage worker pools from the Clusters page.',
|
||||||
'clusters.addworker.selectGPU': 'Select GPU Vendor',
|
'clusters.addworker.selectGPU': 'Select GPU Vendor',
|
||||||
'clusters.addworker.selectGPU.multiTag': 'Multi-select',
|
'clusters.addworker.selectGPU.multiTag': 'Multi-select',
|
||||||
|
'clusters.addworker.selectGPU.subtitle':
|
||||||
|
'You can select multiple GPU Vendors or none for CPU-only clusters',
|
||||||
'clusters.addworker.checkEnv': 'Check Environment',
|
'clusters.addworker.checkEnv': 'Check Environment',
|
||||||
|
'clusters.addworker.checkEnv.cpuOnlyTips':
|
||||||
|
'Use the following command to verify that the Kubernetes cluster has at least one ready node. You are registering a CPU-only cluster.',
|
||||||
'clusters.addworker.specifyArgs': 'Specify Arguments',
|
'clusters.addworker.specifyArgs': 'Specify Arguments',
|
||||||
|
'clusters.addworker.dtkVersion': 'DTK Version',
|
||||||
'clusters.addworker.runCommand': 'Run Command',
|
'clusters.addworker.runCommand': 'Run Command',
|
||||||
'clusters.addworker.specifyWorkerIP': 'Worker IP',
|
'clusters.addworker.specifyWorkerIP': 'Worker IP',
|
||||||
'clusters.addworker.detectWorkerIP': 'Auto-detect Worker IP',
|
'clusters.addworker.detectWorkerIP': 'Auto-detect Worker IP',
|
||||||
@@ -112,8 +117,6 @@ export default {
|
|||||||
'clusters.create.workerConfig': 'Worker Configuration',
|
'clusters.create.workerConfig': 'Worker Configuration',
|
||||||
'clusters.edit.k8sOptions.changed.tip':
|
'clusters.edit.k8sOptions.changed.tip':
|
||||||
'You have changed the Kubernetes options. Re-run the registration command on the target cluster for the changes to take effect.',
|
'You have changed the Kubernetes options. Re-run the registration command on the target cluster for the changes to take effect.',
|
||||||
'clusters.edit.workerConfig.tip':
|
|
||||||
'Changes to the worker configuration take effect only after restarting the affected workers.',
|
|
||||||
'clusters.addworker.containerName': 'Worker Container Name',
|
'clusters.addworker.containerName': 'Worker Container Name',
|
||||||
'clusters.addworker.containerName.tips':
|
'clusters.addworker.containerName.tips':
|
||||||
'Specify a name for the worker container.',
|
'Specify a name for the worker container.',
|
||||||
@@ -141,7 +144,7 @@ export default {
|
|||||||
'clusters.addworker.theadNotes-02':
|
'clusters.addworker.theadNotes-02':
|
||||||
'T-Head PPU uses the Container Device Interface (CDI) for device injection and requires the <span class="bold-text">/var/run/cdi</span> directory to be available for CDI generation.',
|
'T-Head PPU uses the Container Device Interface (CDI) for device injection and requires the <span class="bold-text">/var/run/cdi</span> directory to be available for CDI generation.',
|
||||||
'clusters.addworker.nvidiaNotes':
|
'clusters.addworker.nvidiaNotes':
|
||||||
'The built-in inference backends in GPUStack v2.1 require <span class="bold-text">CUDA 12.6+</span>. Please ensure your NVIDIA driver version is <span class="bold-text">560</span> or newer.',
|
'The built-in inference backends in GPUStack require <span class="bold-text">CUDA 12.8+</span>. Please ensure your NVIDIA driver version is <span class="bold-text">570</span> or newer.',
|
||||||
'clusters.volume.title': 'Volume Mounts',
|
'clusters.volume.title': 'Volume Mounts',
|
||||||
'clusters.volume.name': 'Volume Name',
|
'clusters.volume.name': 'Volume Name',
|
||||||
'clusters.volume.mountPath': 'Container Path',
|
'clusters.volume.mountPath': 'Container Path',
|
||||||
|
|||||||
@@ -169,6 +169,7 @@ export default {
|
|||||||
'common.time.hour': 'hour',
|
'common.time.hour': 'hour',
|
||||||
'common.time.minute': 'minutes',
|
'common.time.minute': 'minutes',
|
||||||
'common.issue.report': 'Report an issue',
|
'common.issue.report': 'Report an issue',
|
||||||
|
'common.github.star.tooltip': 'Star us on GitHub',
|
||||||
'common.social.discord': 'Join Our Discord',
|
'common.social.discord': 'Join Our Discord',
|
||||||
'common.table.mark': 'Comment',
|
'common.table.mark': 'Comment',
|
||||||
'common.table.rollback.mark': 'Rollback Comment',
|
'common.table.rollback.mark': 'Rollback Comment',
|
||||||
@@ -260,6 +261,10 @@ export default {
|
|||||||
'common.login.auth': 'Authenticating...',
|
'common.login.auth': 'Authenticating...',
|
||||||
'common.login.auth.failed': 'Authentication failed',
|
'common.login.auth.failed': 'Authentication failed',
|
||||||
'common.login.password': 'Log in with Password',
|
'common.login.password': 'Log in with Password',
|
||||||
|
'common.login.username.holder': 'Please enter username',
|
||||||
|
'common.login.password.holder': 'Please enter password',
|
||||||
|
'common.login.newpassword.holder': 'Please enter new password',
|
||||||
|
'common.login.confirm.holder': 'Please enter password again',
|
||||||
'common.external.login': 'Log in with {type}',
|
'common.external.login': 'Log in with {type}',
|
||||||
'common.sso.noConfig':
|
'common.sso.noConfig':
|
||||||
'Single sign-on is not enabled on this system. Please contact your administrator.',
|
'Single sign-on is not enabled on this system. Please contact your administrator.',
|
||||||
|
|||||||
@@ -120,6 +120,8 @@ export default {
|
|||||||
'gpuservice.instance.sliced': 'Sliced',
|
'gpuservice.instance.sliced': 'Sliced',
|
||||||
'gpuservice.instance.memory': 'VRAM',
|
'gpuservice.instance.memory': 'VRAM',
|
||||||
'gpuservice.instance.ram': 'RAM',
|
'gpuservice.instance.ram': 'RAM',
|
||||||
|
'gpuservice.instance.os': 'OS',
|
||||||
|
'gpuservice.instance.arch': 'Arch',
|
||||||
'gpuservice.instance.disk': 'Disk',
|
'gpuservice.instance.disk': 'Disk',
|
||||||
'gpuservice.table.count': 'Count',
|
'gpuservice.table.count': 'Count',
|
||||||
'gpuservice.instance.disk.system': 'System Disk',
|
'gpuservice.instance.disk.system': 'System Disk',
|
||||||
@@ -172,5 +174,9 @@ export default {
|
|||||||
'Please enter the temporary storage capacity',
|
'Please enter the temporary storage capacity',
|
||||||
'gpuservice.form.rule.name':
|
'gpuservice.form.rule.name':
|
||||||
"Lowercase letters, numbers, and '-'. Start and end with a letter or number, no consecutive '-', max 63 characters.",
|
"Lowercase letters, numbers, and '-'. Start and end with a letter or number, no consecutive '-', max 63 characters.",
|
||||||
'gpuservice.form.storage.select': 'Select Storage'
|
'gpuservice.form.storage.select': 'Select Storage',
|
||||||
|
'gpuservice.creator': 'Creator',
|
||||||
|
'gpuservice.owner.global': 'Global',
|
||||||
|
'gpuservice.template.group.yours': 'Your Templates',
|
||||||
|
'gpuservice.template.group.global': 'Global Templates'
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ export default {
|
|||||||
'menu.usage.usage': 'Usage',
|
'menu.usage.usage': 'Usage',
|
||||||
'menu.billingAndUsage': 'Usage & Billing',
|
'menu.billingAndUsage': 'Usage & Billing',
|
||||||
'menu.billingAndUsage.usage': 'Usage',
|
'menu.billingAndUsage.usage': 'Usage',
|
||||||
|
'menu.billingAndUsage.billing': 'Billing',
|
||||||
'menu.404': '404',
|
'menu.404': '404',
|
||||||
'menu.resources.clusters': 'Clusters',
|
'menu.resources.clusters': 'Clusters',
|
||||||
'menu.resources.credentials': 'Cloud Credentials',
|
'menu.resources.credentials': 'Cloud Credentials',
|
||||||
|
|||||||
@@ -90,6 +90,7 @@ export default {
|
|||||||
// --- Resource events ---
|
// --- Resource events ---
|
||||||
'usage.events.resourceType': 'Resource type',
|
'usage.events.resourceType': 'Resource type',
|
||||||
'usage.events.eventType': 'Event type',
|
'usage.events.eventType': 'Event type',
|
||||||
|
'usage.events.resourceName': 'Filter by name',
|
||||||
'usage.events.col.time': 'Time',
|
'usage.events.col.time': 'Time',
|
||||||
'usage.events.col.resource': 'Resource',
|
'usage.events.col.resource': 'Resource',
|
||||||
'usage.events.col.event': 'Event',
|
'usage.events.col.event': 'Event',
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export default {
|
|||||||
'ai.provider.ollama': 'Ollama',
|
'ai.provider.ollama': 'Ollama',
|
||||||
'ai.provider.openai': 'OpenAI',
|
'ai.provider.openai': 'OpenAI',
|
||||||
'ai.provider.openrouter': 'OpenRouter',
|
'ai.provider.openrouter': 'OpenRouter',
|
||||||
'ai.provider.qwen': 'Qwen',
|
'ai.provider.qwen': 'Alibaba Cloud Model Studio',
|
||||||
'ai.provider.spark': 'Spark',
|
'ai.provider.spark': 'Spark',
|
||||||
'ai.provider.stepfun': 'StepFun',
|
'ai.provider.stepfun': 'StepFun',
|
||||||
'ai.provider.together-ai': 'TogetherAI',
|
'ai.provider.together-ai': 'TogetherAI',
|
||||||
|
|||||||
@@ -23,11 +23,12 @@ export default {
|
|||||||
'backend.form.defaultExecuteCommand': 'Default Execution Command',
|
'backend.form.defaultExecuteCommand': 'Default Execution Command',
|
||||||
'backend.form.defaultExecuteCommand.tips': `'{{'model_path'}}', '{{'port'}}', '{{'worker_ip'}}' and '{{'model_name'}}' are placeholders that will be substituted with the actual values during deployment.`,
|
'backend.form.defaultExecuteCommand.tips': `'{{'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.parameterFormat': 'パラメータ形式',
|
'backend.form.flagFormat': 'フラグ形式',
|
||||||
'backend.form.parameterFormat.default': 'バックエンドのデフォルト',
|
'backend.form.flagFormat.tips':
|
||||||
'backend.form.parameterFormat.space': 'スペース区切り (--key value)',
|
'オプションとその値を連結する形式。空欄の場合は各パラメータを入力されたまま保持し、形式を統一しません。',
|
||||||
'backend.form.parameterFormat.equal': 'イコール連結 (--key=value)',
|
'backend.form.flagFormat.space': 'スペース区切り (--key value)',
|
||||||
'backend.form.commonParameters': 'よく使うパラメータ',
|
'backend.form.flagFormat.equal': 'イコール記号 (--key=value)',
|
||||||
|
'backend.form.commonParameters': 'よく使うバックエンドパラメータ',
|
||||||
'backend.form.commonParameters.tips':
|
'backend.form.commonParameters.tips':
|
||||||
'モデルのデプロイ時にバックエンドパラメータ入力欄の候補として表示されます。',
|
'モデルのデプロイ時にバックエンドパラメータ入力欄の候補として表示されます。',
|
||||||
'backend.form.versionConfig': 'Versions Config',
|
'backend.form.versionConfig': 'Versions Config',
|
||||||
|
|||||||
@@ -37,8 +37,8 @@ export default {
|
|||||||
'Stress test for long-context handling. Evaluates KV cache behavior, memory usage, and backend stability.',
|
'Stress test for long-context handling. Evaluates KV cache behavior, memory usage, and backend stability.',
|
||||||
'benchmark.form.profile.heavy.tips':
|
'benchmark.form.profile.heavy.tips':
|
||||||
'Decode-heavy generation benchmark. Measures sustained decoding speed and output token throughput.',
|
'Decode-heavy generation benchmark. Measures sustained decoding speed and output token throughput.',
|
||||||
'benchmark.table.filter.bygpu': 'Filter by GPU',
|
'benchmark.table.filter.bygpu': 'GPU 検索',
|
||||||
'benchmark.table.filter.bymodel': 'Filter by Model',
|
'benchmark.table.filter.bymodel': 'モデル検索',
|
||||||
'benchmark.table.filter.bydataset': 'Filter by Dataset',
|
'benchmark.table.filter.bydataset': 'Filter by Dataset',
|
||||||
'benchmark.table.filter.byProfile': 'Filter by Profile',
|
'benchmark.table.filter.byProfile': 'Filter by Profile',
|
||||||
'benchmark.table.avg': 'Avg',
|
'benchmark.table.avg': 'Avg',
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
export default {
|
||||||
|
'billing.upsell.title': 'Billing is an Enterprise feature',
|
||||||
|
'billing.upsell.subtitle':
|
||||||
|
'Track spend, generate invoices, and enforce budgets across teams. Upgrade to GPUStack Enterprise to manage billing.',
|
||||||
|
'billing.upsell.featuresTitle': 'What you get in Enterprise',
|
||||||
|
'billing.upsell.feature.usage':
|
||||||
|
'See cost breakdowns by organization, user, and model',
|
||||||
|
'billing.upsell.feature.invoices':
|
||||||
|
'Generate invoices and export billing reports',
|
||||||
|
'billing.upsell.feature.budgets':
|
||||||
|
'Set budgets and spending limits with alerts',
|
||||||
|
'billing.upsell.feature.chargeback':
|
||||||
|
'Attribute and charge back usage to teams and projects',
|
||||||
|
'billing.upsell.cta': 'Learn about Enterprise'
|
||||||
|
};
|
||||||
@@ -70,8 +70,13 @@ export default {
|
|||||||
'For <span class="bold-text">non-Docker</span> clusters, please register clusters or manage worker pools from the Clusters page.',
|
'For <span class="bold-text">non-Docker</span> clusters, please register clusters or manage worker pools from the Clusters page.',
|
||||||
'clusters.addworker.selectGPU': 'Select GPU Vendor',
|
'clusters.addworker.selectGPU': 'Select GPU Vendor',
|
||||||
'clusters.addworker.selectGPU.multiTag': 'Multi-select',
|
'clusters.addworker.selectGPU.multiTag': 'Multi-select',
|
||||||
|
'clusters.addworker.selectGPU.subtitle':
|
||||||
|
'複数の GPU ベンダーを選択するか、CPU クラスター専用の場合は選択不要です',
|
||||||
'clusters.addworker.checkEnv': 'Check Environment',
|
'clusters.addworker.checkEnv': 'Check Environment',
|
||||||
|
'clusters.addworker.checkEnv.cpuOnlyTips':
|
||||||
|
'以下のコマンドを使用して、Kubernetes クラスターに少なくとも 1 つのレディーノードがあることを確認してください。CPU クラスターを登録しています。',
|
||||||
'clusters.addworker.specifyArgs': 'Specify Arguments',
|
'clusters.addworker.specifyArgs': 'Specify Arguments',
|
||||||
|
'clusters.addworker.dtkVersion': 'DTK バージョン',
|
||||||
'clusters.addworker.runCommand': 'Run Command',
|
'clusters.addworker.runCommand': 'Run Command',
|
||||||
'clusters.addworker.specifyWorkerIP': 'Worker IP',
|
'clusters.addworker.specifyWorkerIP': 'Worker IP',
|
||||||
'clusters.addworker.detectWorkerIP': 'Worker IP を自動検出',
|
'clusters.addworker.detectWorkerIP': 'Worker IP を自動検出',
|
||||||
@@ -112,8 +117,6 @@ export default {
|
|||||||
'clusters.create.workerConfig': 'Worker Configuration',
|
'clusters.create.workerConfig': 'Worker Configuration',
|
||||||
'clusters.edit.k8sOptions.changed.tip':
|
'clusters.edit.k8sOptions.changed.tip':
|
||||||
'Kubernetes オプションを変更しました。変更を有効にするには、対象クラスターで登録コマンドを再実行してください。',
|
'Kubernetes オプションを変更しました。変更を有効にするには、対象クラスターで登録コマンドを再実行してください。',
|
||||||
'clusters.edit.workerConfig.tip':
|
|
||||||
'ワーカー設定の変更は、対象のワーカーを再起動した後に有効になります。',
|
|
||||||
'clusters.addworker.containerName': 'Worker Container Name',
|
'clusters.addworker.containerName': 'Worker Container Name',
|
||||||
'clusters.addworker.containerName.tips':
|
'clusters.addworker.containerName.tips':
|
||||||
'Specify a name for the worker container.',
|
'Specify a name for the worker container.',
|
||||||
@@ -141,7 +144,7 @@ export default {
|
|||||||
'clusters.addworker.theadNotes-02':
|
'clusters.addworker.theadNotes-02':
|
||||||
'T-Head PPU uses the Container Device Interface (CDI) for device injection and requires the <span class="bold-text">/var/run/cdi</span> directory to be available for CDI generation.',
|
'T-Head PPU uses the Container Device Interface (CDI) for device injection and requires the <span class="bold-text">/var/run/cdi</span> directory to be available for CDI generation.',
|
||||||
'clusters.addworker.nvidiaNotes':
|
'clusters.addworker.nvidiaNotes':
|
||||||
'The built-in inference backends in GPUStack v2.1 require <span class="bold-text">CUDA 12.6+</span>. Please ensure your NVIDIA driver version is <span class="bold-text">560</span> or newer.',
|
'The built-in inference backends in GPUStack require <span class="bold-text">CUDA 12.8+</span>. Please ensure your NVIDIA driver version is <span class="bold-text">570</span> or newer.',
|
||||||
'clusters.volume.title': 'Volume Mounts',
|
'clusters.volume.title': 'Volume Mounts',
|
||||||
'clusters.volume.name': 'Volume Name',
|
'clusters.volume.name': 'Volume Name',
|
||||||
'clusters.volume.mountPath': 'Container Path',
|
'clusters.volume.mountPath': 'Container Path',
|
||||||
|
|||||||
@@ -170,6 +170,7 @@ export default {
|
|||||||
'common.time.hour': '時間',
|
'common.time.hour': '時間',
|
||||||
'common.time.minute': '分',
|
'common.time.minute': '分',
|
||||||
'common.issue.report': '問題を報告',
|
'common.issue.report': '問題を報告',
|
||||||
|
'common.github.star.tooltip': 'GitHub でスターをつける',
|
||||||
'common.social.discord': 'Discordに参加',
|
'common.social.discord': 'Discordに参加',
|
||||||
'common.table.mark': 'コメント',
|
'common.table.mark': 'コメント',
|
||||||
'common.table.rollback.mark': 'ロールバックコメント',
|
'common.table.rollback.mark': 'ロールバックコメント',
|
||||||
@@ -260,6 +261,10 @@ export default {
|
|||||||
'common.login.auth': 'Authenticating...',
|
'common.login.auth': 'Authenticating...',
|
||||||
'common.login.auth.failed': 'Authentication failed',
|
'common.login.auth.failed': 'Authentication failed',
|
||||||
'common.login.password': 'Log in with Password',
|
'common.login.password': 'Log in with Password',
|
||||||
|
'common.login.username.holder': 'Please enter username',
|
||||||
|
'common.login.password.holder': 'Please enter password',
|
||||||
|
'common.login.newpassword.holder': 'Please enter new password',
|
||||||
|
'common.login.confirm.holder': 'Please enter password again',
|
||||||
'common.external.login': 'Log in with {type}',
|
'common.external.login': 'Log in with {type}',
|
||||||
'common.sso.noConfig':
|
'common.sso.noConfig':
|
||||||
'Single sign-on is not enabled on this system. Please contact your administrator.',
|
'Single sign-on is not enabled on this system. Please contact your administrator.',
|
||||||
|
|||||||
@@ -119,6 +119,8 @@ export default {
|
|||||||
'gpuservice.instance.sliced': '分割',
|
'gpuservice.instance.sliced': '分割',
|
||||||
'gpuservice.instance.memory': 'VRAM',
|
'gpuservice.instance.memory': 'VRAM',
|
||||||
'gpuservice.instance.ram': 'RAM',
|
'gpuservice.instance.ram': 'RAM',
|
||||||
|
'gpuservice.instance.os': 'OS',
|
||||||
|
'gpuservice.instance.arch': 'アーキテクチャ',
|
||||||
'gpuservice.instance.disk': 'ディスク',
|
'gpuservice.instance.disk': 'ディスク',
|
||||||
'gpuservice.table.count': '数量',
|
'gpuservice.table.count': '数量',
|
||||||
'gpuservice.instance.disk.system': 'システムディスク',
|
'gpuservice.instance.disk.system': 'システムディスク',
|
||||||
@@ -172,5 +174,9 @@ export default {
|
|||||||
'Data is cleared when the instance stops.',
|
'Data is cleared when the instance stops.',
|
||||||
'gpuservice.storage.persistentVolume.tips':
|
'gpuservice.storage.persistentVolume.tips':
|
||||||
'Data persists across instance restarts. Persistent volumes remain intact after instance termination and can be shared by multiple instances.',
|
'Data persists across instance restarts. Persistent volumes remain intact after instance termination and can be shared by multiple instances.',
|
||||||
'gpuservice.form.storage.select': 'ストレージを選択'
|
'gpuservice.form.storage.select': 'ストレージを選択',
|
||||||
|
'gpuservice.creator': '作成者',
|
||||||
|
'gpuservice.owner.global': 'グローバル',
|
||||||
|
'gpuservice.template.group.yours': '自分のテンプレート',
|
||||||
|
'gpuservice.template.group.global': 'グローバルテンプレート'
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ export default {
|
|||||||
'menu.usage.usage': '使用状況',
|
'menu.usage.usage': '使用状況',
|
||||||
'menu.billingAndUsage': '使用状況と請求',
|
'menu.billingAndUsage': '使用状況と請求',
|
||||||
'menu.billingAndUsage.usage': '使用状況',
|
'menu.billingAndUsage.usage': '使用状況',
|
||||||
|
'menu.billingAndUsage.billing': '請求',
|
||||||
'menu.404': '404',
|
'menu.404': '404',
|
||||||
'menu.settings': 'Settings',
|
'menu.settings': 'Settings',
|
||||||
'menu.resources.workers': 'Workers',
|
'menu.resources.workers': 'Workers',
|
||||||
|
|||||||
@@ -90,6 +90,7 @@ export default {
|
|||||||
// --- Resource events ---
|
// --- Resource events ---
|
||||||
'usage.events.resourceType': 'リソースタイプ',
|
'usage.events.resourceType': 'リソースタイプ',
|
||||||
'usage.events.eventType': 'イベントタイプ',
|
'usage.events.eventType': 'イベントタイプ',
|
||||||
|
'usage.events.resourceName': '名前で絞り込み',
|
||||||
'usage.events.col.time': '時刻',
|
'usage.events.col.time': '時刻',
|
||||||
'usage.events.col.resource': 'リソース',
|
'usage.events.col.resource': 'リソース',
|
||||||
'usage.events.col.event': 'イベント',
|
'usage.events.col.event': 'イベント',
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export default {
|
|||||||
'ai.provider.ollama': 'Ollama',
|
'ai.provider.ollama': 'Ollama',
|
||||||
'ai.provider.openai': 'OpenAI',
|
'ai.provider.openai': 'OpenAI',
|
||||||
'ai.provider.openrouter': 'OpenRouter',
|
'ai.provider.openrouter': 'OpenRouter',
|
||||||
'ai.provider.qwen': 'Qwen',
|
'ai.provider.qwen': 'Alibaba Cloud Model Studio',
|
||||||
'ai.provider.spark': 'Spark',
|
'ai.provider.spark': 'Spark',
|
||||||
'ai.provider.stepfun': 'StepFun',
|
'ai.provider.stepfun': 'StepFun',
|
||||||
'ai.provider.together-ai': 'TogetherAI',
|
'ai.provider.together-ai': 'TogetherAI',
|
||||||
|
|||||||
@@ -23,11 +23,12 @@ export default {
|
|||||||
'backend.form.defaultExecuteCommand': 'Команда выполнения по умолчанию',
|
'backend.form.defaultExecuteCommand': 'Команда выполнения по умолчанию',
|
||||||
'backend.form.defaultExecuteCommand.tips': `'{{'model_path'}}', '{{'port'}}', '{{'worker_ip'}}' и '{{'model_name'}}' заполняются реальными значениями во время запуска`,
|
'backend.form.defaultExecuteCommand.tips': `'{{'model_path'}}', '{{'port'}}', '{{'worker_ip'}}' и '{{'model_name'}}' заполняются реальными значениями во время запуска`,
|
||||||
'backend.form.defaultBackendParameters': 'Параметры бэкенда по умолчанию',
|
'backend.form.defaultBackendParameters': 'Параметры бэкенда по умолчанию',
|
||||||
'backend.form.parameterFormat': 'Формат параметров',
|
'backend.form.flagFormat': 'Формат флага',
|
||||||
'backend.form.parameterFormat.default': 'По умолчанию бэкенда',
|
'backend.form.flagFormat.tips':
|
||||||
'backend.form.parameterFormat.space': 'Пробел (--key value)',
|
'Формат соединения опции и её значения. Оставьте пустым, чтобы сохранить каждый параметр в исходном виде, без приведения к единому формату.',
|
||||||
'backend.form.parameterFormat.equal': 'Знак равенства (--key=value)',
|
'backend.form.flagFormat.space': 'Разделение пробелом (--key value)',
|
||||||
'backend.form.commonParameters': 'Часто используемые параметры',
|
'backend.form.flagFormat.equal': 'Знак равенства (--key=value)',
|
||||||
|
'backend.form.commonParameters': 'Общие параметры бэкенда',
|
||||||
'backend.form.commonParameters.tips':
|
'backend.form.commonParameters.tips':
|
||||||
'Отображаются как подсказки в поле параметров бэкенда при развёртывании.',
|
'Отображаются как подсказки в поле параметров бэкенда при развёртывании.',
|
||||||
'backend.form.versionConfig': 'Конфигурация версий',
|
'backend.form.versionConfig': 'Конфигурация версий',
|
||||||
|
|||||||
@@ -37,8 +37,8 @@ export default {
|
|||||||
'Stress test for long-context handling. Evaluates KV cache behavior, memory usage, and backend stability.',
|
'Stress test for long-context handling. Evaluates KV cache behavior, memory usage, and backend stability.',
|
||||||
'benchmark.form.profile.heavy.tips':
|
'benchmark.form.profile.heavy.tips':
|
||||||
'Decode-heavy generation benchmark. Measures sustained decoding speed and output token throughput.',
|
'Decode-heavy generation benchmark. Measures sustained decoding speed and output token throughput.',
|
||||||
'benchmark.table.filter.bygpu': 'Filter by GPU',
|
'benchmark.table.filter.bygpu': 'Поиск GPU',
|
||||||
'benchmark.table.filter.bymodel': 'Filter by Model',
|
'benchmark.table.filter.bymodel': 'Поиск модели',
|
||||||
'benchmark.table.filter.bydataset': 'Filter by Dataset',
|
'benchmark.table.filter.bydataset': 'Filter by Dataset',
|
||||||
'benchmark.table.filter.byProfile': 'Filter by Profile',
|
'benchmark.table.filter.byProfile': 'Filter by Profile',
|
||||||
'benchmark.table.avg': 'Avg',
|
'benchmark.table.avg': 'Avg',
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
export default {
|
||||||
|
'billing.upsell.title': 'Billing is an Enterprise feature',
|
||||||
|
'billing.upsell.subtitle':
|
||||||
|
'Track spend, generate invoices, and enforce budgets across teams. Upgrade to GPUStack Enterprise to manage billing.',
|
||||||
|
'billing.upsell.featuresTitle': 'What you get in Enterprise',
|
||||||
|
'billing.upsell.feature.usage':
|
||||||
|
'See cost breakdowns by organization, user, and model',
|
||||||
|
'billing.upsell.feature.invoices':
|
||||||
|
'Generate invoices and export billing reports',
|
||||||
|
'billing.upsell.feature.budgets':
|
||||||
|
'Set budgets and spending limits with alerts',
|
||||||
|
'billing.upsell.feature.chargeback':
|
||||||
|
'Attribute and charge back usage to teams and projects',
|
||||||
|
'billing.upsell.cta': 'Learn about Enterprise'
|
||||||
|
};
|
||||||
@@ -70,8 +70,13 @@ export default {
|
|||||||
'Для <span class="bold-text">не-Docker</span> кластеров, пожалуйста, регистрируйте кластеры или управляйте пулами воркеров на странице Кластеры.',
|
'Для <span class="bold-text">не-Docker</span> кластеров, пожалуйста, регистрируйте кластеры или управляйте пулами воркеров на странице Кластеры.',
|
||||||
'clusters.addworker.selectGPU': 'Выбрать производителя GPU',
|
'clusters.addworker.selectGPU': 'Выбрать производителя GPU',
|
||||||
'clusters.addworker.selectGPU.multiTag': 'Multi-select',
|
'clusters.addworker.selectGPU.multiTag': 'Multi-select',
|
||||||
|
'clusters.addworker.selectGPU.subtitle':
|
||||||
|
'Вы можете выбрать несколько производителей GPU или не выбирать для кластера только с CPU',
|
||||||
'clusters.addworker.checkEnv': 'Проверить окружение',
|
'clusters.addworker.checkEnv': 'Проверить окружение',
|
||||||
|
'clusters.addworker.checkEnv.cpuOnlyTips':
|
||||||
|
'Используйте следующую команду, чтобы убедиться, что в кластере Kubernetes есть хотя бы один готовый узел. Вы регистрируете кластер только с CPU.',
|
||||||
'clusters.addworker.specifyArgs': 'Указать аргументы',
|
'clusters.addworker.specifyArgs': 'Указать аргументы',
|
||||||
|
'clusters.addworker.dtkVersion': 'Версия DTK',
|
||||||
'clusters.addworker.runCommand': 'Выполнить команду',
|
'clusters.addworker.runCommand': 'Выполнить команду',
|
||||||
'clusters.addworker.specifyWorkerIP': 'Указать IP воркера',
|
'clusters.addworker.specifyWorkerIP': 'Указать IP воркера',
|
||||||
'clusters.addworker.detectWorkerIP': 'Автоматически определить IP воркера',
|
'clusters.addworker.detectWorkerIP': 'Автоматически определить IP воркера',
|
||||||
@@ -112,8 +117,6 @@ export default {
|
|||||||
'clusters.create.workerConfig': 'Конфигурация воркера',
|
'clusters.create.workerConfig': 'Конфигурация воркера',
|
||||||
'clusters.edit.k8sOptions.changed.tip':
|
'clusters.edit.k8sOptions.changed.tip':
|
||||||
'Вы изменили параметры Kubernetes. Чтобы изменения вступили в силу, повторно выполните команду регистрации в целевом кластере.',
|
'Вы изменили параметры Kubernetes. Чтобы изменения вступили в силу, повторно выполните команду регистрации в целевом кластере.',
|
||||||
'clusters.edit.workerConfig.tip':
|
|
||||||
'Изменения конфигурации воркера вступают в силу только после перезапуска соответствующих воркеров.',
|
|
||||||
'clusters.addworker.containerName': 'Имя контейнера воркера',
|
'clusters.addworker.containerName': 'Имя контейнера воркера',
|
||||||
'clusters.addworker.containerName.tips':
|
'clusters.addworker.containerName.tips':
|
||||||
'Укажите имя для контейнера воркера.',
|
'Укажите имя для контейнера воркера.',
|
||||||
@@ -142,7 +145,7 @@ export default {
|
|||||||
'clusters.addworker.theadNotes-02':
|
'clusters.addworker.theadNotes-02':
|
||||||
'T-Head PPU uses the Container Device Interface (CDI) for device injection and requires the <span class="bold-text">/var/run/cdi</span> directory to be available for CDI generation.',
|
'T-Head PPU uses the Container Device Interface (CDI) for device injection and requires the <span class="bold-text">/var/run/cdi</span> directory to be available for CDI generation.',
|
||||||
'clusters.addworker.nvidiaNotes':
|
'clusters.addworker.nvidiaNotes':
|
||||||
'The built-in inference backends in GPUStack v2.1 require <span class="bold-text">CUDA 12.6+</span>. Please ensure your NVIDIA driver version is <span class="bold-text">560</span> or newer.',
|
'The built-in inference backends in GPUStack require <span class="bold-text">CUDA 12.8+</span>. Please ensure your NVIDIA driver version is <span class="bold-text">570</span> or newer.',
|
||||||
'clusters.volume.title': 'Volume Mounts',
|
'clusters.volume.title': 'Volume Mounts',
|
||||||
'clusters.volume.name': 'Volume Name',
|
'clusters.volume.name': 'Volume Name',
|
||||||
'clusters.volume.mountPath': 'Container Path',
|
'clusters.volume.mountPath': 'Container Path',
|
||||||
|
|||||||
@@ -167,6 +167,7 @@ export default {
|
|||||||
'common.time.hour': 'Час',
|
'common.time.hour': 'Час',
|
||||||
'common.time.minute': 'Минута',
|
'common.time.minute': 'Минута',
|
||||||
'common.issue.report': 'Сообщить о проблеме',
|
'common.issue.report': 'Сообщить о проблеме',
|
||||||
|
'common.github.star.tooltip': 'Поставьте нам звезду на GitHub',
|
||||||
'common.social.discord': 'Присоединиться к Discord',
|
'common.social.discord': 'Присоединиться к Discord',
|
||||||
'common.table.mark': 'Комментарий',
|
'common.table.mark': 'Комментарий',
|
||||||
'common.table.rollback.mark': 'Комментарий к откату',
|
'common.table.rollback.mark': 'Комментарий к откату',
|
||||||
@@ -259,6 +260,10 @@ export default {
|
|||||||
'common.login.auth': 'Аутентификация...',
|
'common.login.auth': 'Аутентификация...',
|
||||||
'common.login.auth.failed': 'Ошибка аутентификации',
|
'common.login.auth.failed': 'Ошибка аутентификации',
|
||||||
'common.login.password': 'Войти с паролем',
|
'common.login.password': 'Войти с паролем',
|
||||||
|
'common.login.username.holder': 'Введите имя пользователя',
|
||||||
|
'common.login.password.holder': 'Введите пароль',
|
||||||
|
'common.login.newpassword.holder': 'Введите новый пароль',
|
||||||
|
'common.login.confirm.holder': 'Введите пароль ещё раз',
|
||||||
'common.external.login': 'Войти через {type}',
|
'common.external.login': 'Войти через {type}',
|
||||||
'common.sso.noConfig':
|
'common.sso.noConfig':
|
||||||
'Единый вход не настроен в этой системе. Пожалуйста, обратитесь к администратору.',
|
'Единый вход не настроен в этой системе. Пожалуйста, обратитесь к администратору.',
|
||||||
|
|||||||
@@ -118,6 +118,8 @@ export default {
|
|||||||
'gpuservice.instance.sliced': 'Разделено',
|
'gpuservice.instance.sliced': 'Разделено',
|
||||||
'gpuservice.instance.memory': 'VRAM',
|
'gpuservice.instance.memory': 'VRAM',
|
||||||
'gpuservice.instance.ram': 'RAM',
|
'gpuservice.instance.ram': 'RAM',
|
||||||
|
'gpuservice.instance.os': 'ОС',
|
||||||
|
'gpuservice.instance.arch': 'Архитектура',
|
||||||
'gpuservice.instance.disk': 'Диск',
|
'gpuservice.instance.disk': 'Диск',
|
||||||
'gpuservice.table.count': 'Количество',
|
'gpuservice.table.count': 'Количество',
|
||||||
'gpuservice.instance.disk.system': 'Системный диск',
|
'gpuservice.instance.disk.system': 'Системный диск',
|
||||||
@@ -169,5 +171,9 @@ export default {
|
|||||||
'Data is cleared when the instance stops.',
|
'Data is cleared when the instance stops.',
|
||||||
'gpuservice.storage.persistentVolume.tips':
|
'gpuservice.storage.persistentVolume.tips':
|
||||||
'Data persists across instance restarts. Persistent volumes remain intact after instance termination and can be shared by multiple instances.',
|
'Data persists across instance restarts. Persistent volumes remain intact after instance termination and can be shared by multiple instances.',
|
||||||
'gpuservice.form.storage.select': 'Выберите хранилище'
|
'gpuservice.form.storage.select': 'Выберите хранилище',
|
||||||
|
'gpuservice.creator': 'Создатель',
|
||||||
|
'gpuservice.owner.global': 'Глобальный',
|
||||||
|
'gpuservice.template.group.yours': 'Ваши шаблоны',
|
||||||
|
'gpuservice.template.group.global': 'Глобальные шаблоны'
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ export default {
|
|||||||
'menu.usage.usage': 'Использование',
|
'menu.usage.usage': 'Использование',
|
||||||
'menu.billingAndUsage': 'Использование и биллинг',
|
'menu.billingAndUsage': 'Использование и биллинг',
|
||||||
'menu.billingAndUsage.usage': 'Использование',
|
'menu.billingAndUsage.usage': 'Использование',
|
||||||
|
'menu.billingAndUsage.billing': 'Биллинг',
|
||||||
'menu.404': 'Ошибка 404',
|
'menu.404': 'Ошибка 404',
|
||||||
'menu.resources.workers': 'Воркеры',
|
'menu.resources.workers': 'Воркеры',
|
||||||
'menu.resources.gpus': 'GPUs',
|
'menu.resources.gpus': 'GPUs',
|
||||||
|
|||||||
@@ -90,6 +90,7 @@ export default {
|
|||||||
// --- Resource events ---
|
// --- Resource events ---
|
||||||
'usage.events.resourceType': 'Тип ресурса',
|
'usage.events.resourceType': 'Тип ресурса',
|
||||||
'usage.events.eventType': 'Тип события',
|
'usage.events.eventType': 'Тип события',
|
||||||
|
'usage.events.resourceName': 'Фильтр по имени',
|
||||||
'usage.events.col.time': 'Время',
|
'usage.events.col.time': 'Время',
|
||||||
'usage.events.col.resource': 'Ресурс',
|
'usage.events.col.resource': 'Ресурс',
|
||||||
'usage.events.col.event': 'Событие',
|
'usage.events.col.event': 'Событие',
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export default {
|
|||||||
'ai.provider.ollama': 'Ollama',
|
'ai.provider.ollama': 'Ollama',
|
||||||
'ai.provider.openai': 'OpenAI',
|
'ai.provider.openai': 'OpenAI',
|
||||||
'ai.provider.openrouter': 'OpenRouter',
|
'ai.provider.openrouter': 'OpenRouter',
|
||||||
'ai.provider.qwen': 'Qwen',
|
'ai.provider.qwen': 'Alibaba Cloud Model Studio',
|
||||||
'ai.provider.spark': 'Spark',
|
'ai.provider.spark': 'Spark',
|
||||||
'ai.provider.stepfun': 'StepFun',
|
'ai.provider.stepfun': 'StepFun',
|
||||||
'ai.provider.together-ai': 'TogetherAI',
|
'ai.provider.together-ai': 'TogetherAI',
|
||||||
|
|||||||
@@ -23,11 +23,12 @@ export default {
|
|||||||
'backend.form.defaultExecuteCommand': 'Varsayılan Çalıştırma Komutu',
|
'backend.form.defaultExecuteCommand': 'Varsayılan Çalıştırma Komutu',
|
||||||
'backend.form.defaultExecuteCommand.tips': `'{{'model_path'}}', '{{'port'}}', '{{'worker_ip'}}' ve '{{'model_name'}}' dağıtım sırasında gerçek değerlerle değiştirilecek yer tutuculardır.`,
|
'backend.form.defaultExecuteCommand.tips': `'{{'model_path'}}', '{{'port'}}', '{{'worker_ip'}}' ve '{{'model_name'}}' dağıtım sırasında gerçek değerlerle değiştirilecek yer tutuculardır.`,
|
||||||
'backend.form.defaultBackendParameters': 'Varsayılan Altyapı Parametreleri',
|
'backend.form.defaultBackendParameters': 'Varsayılan Altyapı Parametreleri',
|
||||||
'backend.form.parameterFormat': 'Parametre Biçimi',
|
'backend.form.flagFormat': 'Bayrak Biçimi',
|
||||||
'backend.form.parameterFormat.default': 'Varsayılan',
|
'backend.form.flagFormat.tips':
|
||||||
'backend.form.parameterFormat.space': 'Boşluk (--key value)',
|
'Bir seçenek ile değerinin birleştirilme biçimi. Boş bırakılırsa her parametre girildiği biçimde korunur, biçim birleştirilmez.',
|
||||||
'backend.form.parameterFormat.equal': 'Eşittir (--key=value)',
|
'backend.form.flagFormat.space': 'Boşlukla Ayır (--key value)',
|
||||||
'backend.form.commonParameters': 'Yaygın Parametreler',
|
'backend.form.flagFormat.equal': 'Eşittir İşareti (--key=value)',
|
||||||
|
'backend.form.commonParameters': 'Ortak Altyapı Parametreleri',
|
||||||
'backend.form.commonParameters.tips':
|
'backend.form.commonParameters.tips':
|
||||||
'Dağıtım sırasında altyapı parametreleri girişinde öneri olarak gösterilir.',
|
'Dağıtım sırasında altyapı parametreleri girişinde öneri olarak gösterilir.',
|
||||||
'backend.form.versionConfig': 'Sürüm Yapılandırması',
|
'backend.form.versionConfig': 'Sürüm Yapılandırması',
|
||||||
|
|||||||
@@ -37,8 +37,8 @@ export default {
|
|||||||
'Uzun bağlam işleme için stres testi. KV önbellek davranışını, bellek kullanımını ve altyapı kararlılığını değerlendirir.',
|
'Uzun bağlam işleme için stres testi. KV önbellek davranışını, bellek kullanımını ve altyapı kararlılığını değerlendirir.',
|
||||||
'benchmark.form.profile.heavy.tips':
|
'benchmark.form.profile.heavy.tips':
|
||||||
'Çözümleme ağırlıklı üretim kıyaslaması. Sürekli çözümleme hızını ve çıkış token verimini ölçer.',
|
'Çözümleme ağırlıklı üretim kıyaslaması. Sürekli çözümleme hızını ve çıkış token verimini ölçer.',
|
||||||
'benchmark.table.filter.bygpu': "GPU'ya göre filtrele",
|
'benchmark.table.filter.bygpu': 'GPU ara',
|
||||||
'benchmark.table.filter.bymodel': 'Modele göre filtrele',
|
'benchmark.table.filter.bymodel': 'Model ara',
|
||||||
'benchmark.table.filter.bydataset': 'Veri Kümesine göre filtrele',
|
'benchmark.table.filter.bydataset': 'Veri Kümesine göre filtrele',
|
||||||
'benchmark.table.filter.byProfile': 'Profile göre filtrele',
|
'benchmark.table.filter.byProfile': 'Profile göre filtrele',
|
||||||
'benchmark.table.avg': 'Ort.',
|
'benchmark.table.avg': 'Ort.',
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
export default {
|
||||||
|
'billing.upsell.title': 'Billing is an Enterprise feature',
|
||||||
|
'billing.upsell.subtitle':
|
||||||
|
'Track spend, generate invoices, and enforce budgets across teams. Upgrade to GPUStack Enterprise to manage billing.',
|
||||||
|
'billing.upsell.featuresTitle': 'What you get in Enterprise',
|
||||||
|
'billing.upsell.feature.usage':
|
||||||
|
'See cost breakdowns by organization, user, and model',
|
||||||
|
'billing.upsell.feature.invoices':
|
||||||
|
'Generate invoices and export billing reports',
|
||||||
|
'billing.upsell.feature.budgets':
|
||||||
|
'Set budgets and spending limits with alerts',
|
||||||
|
'billing.upsell.feature.chargeback':
|
||||||
|
'Attribute and charge back usage to teams and projects',
|
||||||
|
'billing.upsell.cta': 'Learn about Enterprise'
|
||||||
|
};
|
||||||
@@ -70,8 +70,13 @@ export default {
|
|||||||
'<span class="bold-text">Docker dışı</span> kümeler için lütfen Kümeler sayfasından küme kaydı oluşturun veya işçi havuzlarını yönetin.',
|
'<span class="bold-text">Docker dışı</span> kümeler için lütfen Kümeler sayfasından küme kaydı oluşturun veya işçi havuzlarını yönetin.',
|
||||||
'clusters.addworker.selectGPU': 'GPU Üreticisi Seç',
|
'clusters.addworker.selectGPU': 'GPU Üreticisi Seç',
|
||||||
'clusters.addworker.selectGPU.multiTag': 'Multi-select',
|
'clusters.addworker.selectGPU.multiTag': 'Multi-select',
|
||||||
|
'clusters.addworker.selectGPU.subtitle':
|
||||||
|
'Birden fazla GPU Üreticisi seçebilir veya yalnızca CPU kümeleri için hiçbirini seçmeyebilirsiniz',
|
||||||
'clusters.addworker.checkEnv': 'Ortamı Kontrol Et',
|
'clusters.addworker.checkEnv': 'Ortamı Kontrol Et',
|
||||||
|
'clusters.addworker.checkEnv.cpuOnlyTips':
|
||||||
|
'Kubernetes kümesinde en az bir hazır düğüm olduğunu doğrulamak için aşağıdaki komutu kullanın. Yalnızca CPU kümelerini kaydediyorsunuz.',
|
||||||
'clusters.addworker.specifyArgs': 'Argümanları Belirle',
|
'clusters.addworker.specifyArgs': 'Argümanları Belirle',
|
||||||
|
'clusters.addworker.dtkVersion': 'DTK Sürümü',
|
||||||
'clusters.addworker.runCommand': 'Komutu Çalıştır',
|
'clusters.addworker.runCommand': 'Komutu Çalıştır',
|
||||||
'clusters.addworker.specifyWorkerIP': "İşçi Düğüm IP'si",
|
'clusters.addworker.specifyWorkerIP': "İşçi Düğüm IP'si",
|
||||||
'clusters.addworker.detectWorkerIP': "İşçi Düğüm IP'sini Otomatik Algıla",
|
'clusters.addworker.detectWorkerIP': "İşçi Düğüm IP'sini Otomatik Algıla",
|
||||||
@@ -112,8 +117,6 @@ export default {
|
|||||||
'clusters.create.workerConfig': 'İşçi Düğüm Yapılandırması',
|
'clusters.create.workerConfig': 'İşçi Düğüm Yapılandırması',
|
||||||
'clusters.edit.k8sOptions.changed.tip':
|
'clusters.edit.k8sOptions.changed.tip':
|
||||||
'Kubernetes seçeneklerini değiştirdiniz. Değişikliklerin etkili olması için kayıt komutunu hedef kümede yeniden çalıştırın.',
|
'Kubernetes seçeneklerini değiştirdiniz. Değişikliklerin etkili olması için kayıt komutunu hedef kümede yeniden çalıştırın.',
|
||||||
'clusters.edit.workerConfig.tip':
|
|
||||||
'İşçi düğüm yapılandırmasındaki değişiklikler yalnızca ilgili işçi düğümleri yeniden başlatıldıktan sonra etkili olur.',
|
|
||||||
'clusters.addworker.containerName': 'İşçi Düğüm Konteyner Adı',
|
'clusters.addworker.containerName': 'İşçi Düğüm Konteyner Adı',
|
||||||
'clusters.addworker.containerName.tips':
|
'clusters.addworker.containerName.tips':
|
||||||
'İşçi düğüm konteyneri için bir ad belirtin.',
|
'İşçi düğüm konteyneri için bir ad belirtin.',
|
||||||
@@ -142,7 +145,7 @@ export default {
|
|||||||
'clusters.addworker.theadNotes-02':
|
'clusters.addworker.theadNotes-02':
|
||||||
'T-Head PPU, cihaz enjeksiyonu için Container Device Interface (CDI) kullanır ve CDI oluşturma için <span class="bold-text">/var/run/cdi</span> dizininin kullanılabilir olmasını gerektirir.',
|
'T-Head PPU, cihaz enjeksiyonu için Container Device Interface (CDI) kullanır ve CDI oluşturma için <span class="bold-text">/var/run/cdi</span> dizininin kullanılabilir olmasını gerektirir.',
|
||||||
'clusters.addworker.nvidiaNotes':
|
'clusters.addworker.nvidiaNotes':
|
||||||
'GPUStack v2.1\'deki yerleşik çıkarım altyapıları <span class="bold-text">CUDA 12.6+</span> gerektirir. Lütfen NVIDIA sürücü sürümünüzün <span class="bold-text">560</span> veya daha yeni olduğundan emin olun.',
|
'GPUStack\'teki yerleşik çıkarım altyapıları <span class="bold-text">CUDA 12.8+</span> gerektirir. Lütfen NVIDIA sürücü sürümünüzün <span class="bold-text">570</span> veya daha yeni olduğundan emin olun.',
|
||||||
'clusters.volume.title': 'Volume Mounts',
|
'clusters.volume.title': 'Volume Mounts',
|
||||||
'clusters.volume.name': 'Volume Name',
|
'clusters.volume.name': 'Volume Name',
|
||||||
'clusters.volume.mountPath': 'Container Path',
|
'clusters.volume.mountPath': 'Container Path',
|
||||||
|
|||||||
@@ -167,6 +167,7 @@ export default {
|
|||||||
'common.time.hour': 'saat',
|
'common.time.hour': 'saat',
|
||||||
'common.time.minute': 'dakika',
|
'common.time.minute': 'dakika',
|
||||||
'common.issue.report': 'Sorun bildir',
|
'common.issue.report': 'Sorun bildir',
|
||||||
|
'common.github.star.tooltip': "GitHub'da bize yıldız verin",
|
||||||
'common.social.discord': "Discord'umuza Katılın",
|
'common.social.discord': "Discord'umuza Katılın",
|
||||||
'common.table.mark': 'Yorum',
|
'common.table.mark': 'Yorum',
|
||||||
'common.table.rollback.mark': 'Geri Alma Yorumu',
|
'common.table.rollback.mark': 'Geri Alma Yorumu',
|
||||||
@@ -263,6 +264,10 @@ export default {
|
|||||||
'common.login.auth': 'Kimlik doğrulanıyor...',
|
'common.login.auth': 'Kimlik doğrulanıyor...',
|
||||||
'common.login.auth.failed': 'Kimlik doğrulama başarısız',
|
'common.login.auth.failed': 'Kimlik doğrulama başarısız',
|
||||||
'common.login.password': 'Şifre ile giriş yap',
|
'common.login.password': 'Şifre ile giriş yap',
|
||||||
|
'common.login.username.holder': 'Lütfen kullanıcı adını girin',
|
||||||
|
'common.login.password.holder': 'Lütfen şifreyi girin',
|
||||||
|
'common.login.newpassword.holder': 'Lütfen yeni şifreyi girin',
|
||||||
|
'common.login.confirm.holder': 'Lütfen şifreyi tekrar girin',
|
||||||
'common.external.login': '{type} ile giriş yap',
|
'common.external.login': '{type} ile giriş yap',
|
||||||
'common.sso.noConfig':
|
'common.sso.noConfig':
|
||||||
'Bu sistemde çoklu oturum açma etkinleştirilmemiş. Lütfen yöneticinize başvurun.',
|
'Bu sistemde çoklu oturum açma etkinleştirilmemiş. Lütfen yöneticinize başvurun.',
|
||||||
|
|||||||
@@ -114,6 +114,8 @@ export default {
|
|||||||
'gpuservice.instance.sliced': 'Bölünmüş',
|
'gpuservice.instance.sliced': 'Bölünmüş',
|
||||||
'gpuservice.instance.memory': 'VRAM',
|
'gpuservice.instance.memory': 'VRAM',
|
||||||
'gpuservice.instance.ram': 'RAM',
|
'gpuservice.instance.ram': 'RAM',
|
||||||
|
'gpuservice.instance.os': 'OS',
|
||||||
|
'gpuservice.instance.arch': 'Mimari',
|
||||||
'gpuservice.instance.disk': 'Disk',
|
'gpuservice.instance.disk': 'Disk',
|
||||||
'gpuservice.table.count': 'Sayı',
|
'gpuservice.table.count': 'Sayı',
|
||||||
'gpuservice.instance.disk.system': 'Sistem Diski',
|
'gpuservice.instance.disk.system': 'Sistem Diski',
|
||||||
@@ -167,5 +169,9 @@ export default {
|
|||||||
'Data is cleared when the instance stops.',
|
'Data is cleared when the instance stops.',
|
||||||
'gpuservice.storage.persistentVolume.tips':
|
'gpuservice.storage.persistentVolume.tips':
|
||||||
'Data persists across instance restarts. Persistent volumes remain intact after instance termination and can be shared by multiple instances.',
|
'Data persists across instance restarts. Persistent volumes remain intact after instance termination and can be shared by multiple instances.',
|
||||||
'gpuservice.form.storage.select': 'Depolamayı Seç'
|
'gpuservice.form.storage.select': 'Depolamayı Seç',
|
||||||
|
'gpuservice.creator': 'Oluşturan',
|
||||||
|
'gpuservice.owner.global': 'Genel',
|
||||||
|
'gpuservice.template.group.yours': 'Şablonlarınız',
|
||||||
|
'gpuservice.template.group.global': 'Genel Şablonlar'
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ export default {
|
|||||||
'menu.usage.usage': 'Kullanım',
|
'menu.usage.usage': 'Kullanım',
|
||||||
'menu.billingAndUsage': 'Kullanım ve Faturalandırma',
|
'menu.billingAndUsage': 'Kullanım ve Faturalandırma',
|
||||||
'menu.billingAndUsage.usage': 'Kullanım',
|
'menu.billingAndUsage.usage': 'Kullanım',
|
||||||
|
'menu.billingAndUsage.billing': 'Faturalandırma',
|
||||||
'menu.404': '404',
|
'menu.404': '404',
|
||||||
'menu.resources.clusters': 'Kümeler',
|
'menu.resources.clusters': 'Kümeler',
|
||||||
'menu.resources.credentials': 'Bulut Kimlik Bilgileri',
|
'menu.resources.credentials': 'Bulut Kimlik Bilgileri',
|
||||||
|
|||||||
@@ -90,6 +90,7 @@ export default {
|
|||||||
// --- Resource events ---
|
// --- Resource events ---
|
||||||
'usage.events.resourceType': 'Kaynak türü',
|
'usage.events.resourceType': 'Kaynak türü',
|
||||||
'usage.events.eventType': 'Olay türü',
|
'usage.events.eventType': 'Olay türü',
|
||||||
|
'usage.events.resourceName': 'Ada göre filtrele',
|
||||||
'usage.events.col.time': 'Zaman',
|
'usage.events.col.time': 'Zaman',
|
||||||
'usage.events.col.resource': 'Kaynak',
|
'usage.events.col.resource': 'Kaynak',
|
||||||
'usage.events.col.event': 'Olay',
|
'usage.events.col.event': 'Olay',
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export default {
|
|||||||
'ai.provider.ollama': 'Ollama',
|
'ai.provider.ollama': 'Ollama',
|
||||||
'ai.provider.openai': 'OpenAI',
|
'ai.provider.openai': 'OpenAI',
|
||||||
'ai.provider.openrouter': 'OpenRouter',
|
'ai.provider.openrouter': 'OpenRouter',
|
||||||
'ai.provider.qwen': '通义千问',
|
'ai.provider.qwen': '阿里云百炼',
|
||||||
'ai.provider.spark': '讯飞星火',
|
'ai.provider.spark': '讯飞星火',
|
||||||
'ai.provider.stepfun': '阶跃星辰',
|
'ai.provider.stepfun': '阶跃星辰',
|
||||||
'ai.provider.together-ai': 'Together AI',
|
'ai.provider.together-ai': 'Together AI',
|
||||||
|
|||||||
@@ -23,11 +23,12 @@ export default {
|
|||||||
'backend.form.defaultExecuteCommand': '默认执行命令',
|
'backend.form.defaultExecuteCommand': '默认执行命令',
|
||||||
'backend.form.defaultExecuteCommand.tips': `'{{'model_path'}}'、'{{'port'}}'、'{{'worker_ip'}}' 和 '{{'model_name'}}' 都是占位符,在部署过程中会被替换为实际的值。`,
|
'backend.form.defaultExecuteCommand.tips': `'{{'model_path'}}'、'{{'port'}}'、'{{'worker_ip'}}' 和 '{{'model_name'}}' 都是占位符,在部署过程中会被替换为实际的值。`,
|
||||||
'backend.form.defaultBackendParameters': '默认后端参数',
|
'backend.form.defaultBackendParameters': '默认后端参数',
|
||||||
'backend.form.parameterFormat': '参数输出格式',
|
'backend.form.flagFormat': '参数格式',
|
||||||
'backend.form.parameterFormat.default': '后端默认',
|
'backend.form.flagFormat.tips':
|
||||||
'backend.form.parameterFormat.space': '空格分隔 (--key value)',
|
'选项名与取值之间使用的连接格式。留空则保留每个参数原本的写法,不做统一转换。',
|
||||||
'backend.form.parameterFormat.equal': '等号连接 (--key=value)',
|
'backend.form.flagFormat.space': '空格分隔 (--key value)',
|
||||||
'backend.form.commonParameters': '常用参数',
|
'backend.form.flagFormat.equal': '等号连接 (--key=value)',
|
||||||
|
'backend.form.commonParameters': '常用后端参数',
|
||||||
'backend.form.commonParameters.tips': '部署模型时作为后端参数候选项展示。',
|
'backend.form.commonParameters.tips': '部署模型时作为后端参数候选项展示。',
|
||||||
'backend.form.versionConfig': '版本配置',
|
'backend.form.versionConfig': '版本配置',
|
||||||
'backend.form.addParameter': '添加参数',
|
'backend.form.addParameter': '添加参数',
|
||||||
|
|||||||
@@ -37,8 +37,8 @@ export default {
|
|||||||
'长上下文压力测试。评估 KV Cache 行为、内存占用及后端稳定性。',
|
'长上下文压力测试。评估 KV Cache 行为、内存占用及后端稳定性。',
|
||||||
'benchmark.form.profile.heavy.tips':
|
'benchmark.form.profile.heavy.tips':
|
||||||
'以解码为主的生成测试。衡量持续解码速度与输出 Token 吞吐能力。',
|
'以解码为主的生成测试。衡量持续解码速度与输出 Token 吞吐能力。',
|
||||||
'benchmark.table.filter.bygpu': '按 GPU 过滤',
|
'benchmark.table.filter.bygpu': 'GPU 查询',
|
||||||
'benchmark.table.filter.bymodel': '按模型过滤',
|
'benchmark.table.filter.bymodel': '模型查询',
|
||||||
'benchmark.table.filter.bydataset': '按数据集过滤',
|
'benchmark.table.filter.bydataset': '按数据集过滤',
|
||||||
'benchmark.table.filter.byProfile': '按模式过滤',
|
'benchmark.table.filter.byProfile': '按模式过滤',
|
||||||
'benchmark.table.avg': '均值',
|
'benchmark.table.avg': '均值',
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
export default {
|
||||||
|
'billing.upsell.title': '计费是企业版功能',
|
||||||
|
'billing.upsell.subtitle':
|
||||||
|
'在团队间跟踪花费、生成账单并执行预算。升级到 GPUStack 企业版即可管理计费。',
|
||||||
|
'billing.upsell.featuresTitle': '企业版包含的能力',
|
||||||
|
'billing.upsell.feature.usage': '按组织、用户与模型查看成本明细',
|
||||||
|
'billing.upsell.feature.invoices': '生成账单并导出计费报表',
|
||||||
|
'billing.upsell.feature.budgets': '设置预算与花费上限并触发告警',
|
||||||
|
'billing.upsell.feature.chargeback': '将用量归属并分摊到团队与项目',
|
||||||
|
'billing.upsell.cta': '了解企业版'
|
||||||
|
};
|
||||||
@@ -68,8 +68,13 @@ export default {
|
|||||||
'<span class="bold-text">非 Docker</span> 集群请前往集群页面注册集群或管理节点池。',
|
'<span class="bold-text">非 Docker</span> 集群请前往集群页面注册集群或管理节点池。',
|
||||||
'clusters.addworker.selectGPU': '选择 GPU 厂商',
|
'clusters.addworker.selectGPU': '选择 GPU 厂商',
|
||||||
'clusters.addworker.selectGPU.multiTag': '可多选',
|
'clusters.addworker.selectGPU.multiTag': '可多选',
|
||||||
|
'clusters.addworker.selectGPU.subtitle':
|
||||||
|
'可选择多个 GPU 厂商,或不选择以用于仅 CPU 的集群',
|
||||||
'clusters.addworker.checkEnv': '检查环境',
|
'clusters.addworker.checkEnv': '检查环境',
|
||||||
|
'clusters.addworker.checkEnv.cpuOnlyTips':
|
||||||
|
'使用以下命令验证 Kubernetes 集群中至少有一个就绪节点。你正在注册仅 CPU 的集群。',
|
||||||
'clusters.addworker.specifyArgs': '指定参数',
|
'clusters.addworker.specifyArgs': '指定参数',
|
||||||
|
'clusters.addworker.dtkVersion': 'DTK 版本',
|
||||||
'clusters.addworker.runCommand': '运行指令',
|
'clusters.addworker.runCommand': '运行指令',
|
||||||
'clusters.addworker.specifyWorkerIP': '节点 IP',
|
'clusters.addworker.specifyWorkerIP': '节点 IP',
|
||||||
'clusters.addworker.detectWorkerIP': '自动检测节点 IP',
|
'clusters.addworker.detectWorkerIP': '自动检测节点 IP',
|
||||||
@@ -110,8 +115,6 @@ export default {
|
|||||||
'clusters.create.workerConfig': '节点配置',
|
'clusters.create.workerConfig': '节点配置',
|
||||||
'clusters.edit.k8sOptions.changed.tip':
|
'clusters.edit.k8sOptions.changed.tip':
|
||||||
'您已修改 Kubernetes 选项,需要在目标集群上重新运行注册命令才会生效。',
|
'您已修改 Kubernetes 选项,需要在目标集群上重新运行注册命令才会生效。',
|
||||||
'clusters.edit.workerConfig.tip':
|
|
||||||
'修改节点配置后,需要重启对应节点才会生效。',
|
|
||||||
'clusters.addworker.containerName': '节点容器名称',
|
'clusters.addworker.containerName': '节点容器名称',
|
||||||
'clusters.addworker.containerName.tips': '为节点容器指定一个名称。',
|
'clusters.addworker.containerName.tips': '为节点容器指定一个名称。',
|
||||||
'clusters.addworker.dataVolume': 'GPUStack 数据卷',
|
'clusters.addworker.dataVolume': 'GPUStack 数据卷',
|
||||||
@@ -135,7 +138,7 @@ export default {
|
|||||||
'clusters.addworker.theadNotes-02':
|
'clusters.addworker.theadNotes-02':
|
||||||
'平头哥(T-Head)PPU 使用容器设备接口(CDI)进行设备注入,因此需要确保 <span class="bold-text">/var/run/cdi</span> 目录可用以生成 CDI。',
|
'平头哥(T-Head)PPU 使用容器设备接口(CDI)进行设备注入,因此需要确保 <span class="bold-text">/var/run/cdi</span> 目录可用以生成 CDI。',
|
||||||
'clusters.addworker.nvidiaNotes':
|
'clusters.addworker.nvidiaNotes':
|
||||||
'GPUStack v2.1 内置推理后端依赖 <span class="bold-text">CUDA 12.6</span> 及以上版本,请确保 NVIDIA 驱动版本为 <span class="bold-text">560</span> 或以上。',
|
'GPUStack 内置推理后端依赖 <span class="bold-text">CUDA 12.8</span> 及以上版本,请确保 NVIDIA 驱动版本为 <span class="bold-text">570</span> 或以上。',
|
||||||
'clusters.volume.title': '卷挂载',
|
'clusters.volume.title': '卷挂载',
|
||||||
'clusters.volume.name': '卷名称',
|
'clusters.volume.name': '卷名称',
|
||||||
'clusters.volume.mountPath': '容器内路径',
|
'clusters.volume.mountPath': '容器内路径',
|
||||||
|
|||||||
@@ -161,6 +161,7 @@ export default {
|
|||||||
'common.time.hour': '小时',
|
'common.time.hour': '小时',
|
||||||
'common.time.minute': '分钟',
|
'common.time.minute': '分钟',
|
||||||
'common.issue.report': '问题反馈',
|
'common.issue.report': '问题反馈',
|
||||||
|
'common.github.star.tooltip': '在 GitHub 上 Star 我们',
|
||||||
'common.social.discord': '加入我们的Discord',
|
'common.social.discord': '加入我们的Discord',
|
||||||
'common.table.mark': '备注',
|
'common.table.mark': '备注',
|
||||||
'common.table.rollback.mark': '回滚备注',
|
'common.table.rollback.mark': '回滚备注',
|
||||||
@@ -253,6 +254,10 @@ export default {
|
|||||||
'common.login.auth': '认证中...',
|
'common.login.auth': '认证中...',
|
||||||
'common.login.auth.failed': '认证失败',
|
'common.login.auth.failed': '认证失败',
|
||||||
'common.login.password': '使用密码登录',
|
'common.login.password': '使用密码登录',
|
||||||
|
'common.login.username.holder': '请输入用户名',
|
||||||
|
'common.login.password.holder': '请输入密码',
|
||||||
|
'common.login.newpassword.holder': '请输入新密码',
|
||||||
|
'common.login.confirm.holder': '请再次输入密码',
|
||||||
'common.external.login': '使用 {type} 登录',
|
'common.external.login': '使用 {type} 登录',
|
||||||
'common.sso.noConfig': '该系统未启用单点登录,请联系管理员。',
|
'common.sso.noConfig': '该系统未启用单点登录,请联系管理员。',
|
||||||
'common.button.edit.item': '编辑 {name}',
|
'common.button.edit.item': '编辑 {name}',
|
||||||
@@ -260,7 +265,7 @@ export default {
|
|||||||
'common.button.terminal': '终端',
|
'common.button.terminal': '终端',
|
||||||
'common.button.addItem': '添加项',
|
'common.button.addItem': '添加项',
|
||||||
'common.help.default': '默认: {content}',
|
'common.help.default': '默认: {content}',
|
||||||
'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': '示例',
|
||||||
|
|||||||
@@ -109,6 +109,8 @@ export default {
|
|||||||
'gpuservice.instance.sliced': '切分',
|
'gpuservice.instance.sliced': '切分',
|
||||||
'gpuservice.instance.memory': '显存',
|
'gpuservice.instance.memory': '显存',
|
||||||
'gpuservice.instance.ram': '内存',
|
'gpuservice.instance.ram': '内存',
|
||||||
|
'gpuservice.instance.os': '系统',
|
||||||
|
'gpuservice.instance.arch': '架构',
|
||||||
'gpuservice.instance.disk': '磁盘',
|
'gpuservice.instance.disk': '磁盘',
|
||||||
'gpuservice.table.count': '数量',
|
'gpuservice.table.count': '数量',
|
||||||
'gpuservice.instance.disk.system': '系统盘',
|
'gpuservice.instance.disk.system': '系统盘',
|
||||||
@@ -156,5 +158,9 @@ export default {
|
|||||||
'gpuservice.storage.tempCapacity.required': '请输入临时存储容量',
|
'gpuservice.storage.tempCapacity.required': '请输入临时存储容量',
|
||||||
'gpuservice.form.rule.name':
|
'gpuservice.form.rule.name':
|
||||||
'由小写字母、数字和 "-" 组成,以字母或数字开头和结尾,不能包含连续的 "-",最多 63 个字符。',
|
'由小写字母、数字和 "-" 组成,以字母或数字开头和结尾,不能包含连续的 "-",最多 63 个字符。',
|
||||||
'gpuservice.form.storage.select': '选择存储'
|
'gpuservice.form.storage.select': '选择存储',
|
||||||
|
'gpuservice.creator': '创建者',
|
||||||
|
'gpuservice.owner.global': '全局',
|
||||||
|
'gpuservice.template.group.yours': '我的模板',
|
||||||
|
'gpuservice.template.group.global': '全局模板'
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ export default {
|
|||||||
'menu.usage.usage': '使用量',
|
'menu.usage.usage': '使用量',
|
||||||
'menu.billingAndUsage': '用量与计费',
|
'menu.billingAndUsage': '用量与计费',
|
||||||
'menu.billingAndUsage.usage': '用量统计',
|
'menu.billingAndUsage.usage': '用量统计',
|
||||||
|
'menu.billingAndUsage.billing': '计费',
|
||||||
'menu.404': '404',
|
'menu.404': '404',
|
||||||
'menu.resources.workers': '节点',
|
'menu.resources.workers': '节点',
|
||||||
'menu.resources.gpus': 'GPU',
|
'menu.resources.gpus': 'GPU',
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ export default {
|
|||||||
'models.form.search.gguftips':
|
'models.form.search.gguftips':
|
||||||
'当 macOS 或 Windows 作节点时勾选 GGUF(搜索语音模型时取消勾选)',
|
'当 macOS 或 Windows 作节点时勾选 GGUF(搜索语音模型时取消勾选)',
|
||||||
'models.form.button.addlabel': '添加标签',
|
'models.form.button.addlabel': '添加标签',
|
||||||
'models.filter.category': '按类别筛选',
|
'models.filter.category': '按类别过滤',
|
||||||
'models.list.more.logs': '查看更多',
|
'models.list.more.logs': '查看更多',
|
||||||
'models.catalog.release.date': '发布日期',
|
'models.catalog.release.date': '发布日期',
|
||||||
'models.localpath.gguf.tips.title': 'GGUF 格式模型',
|
'models.localpath.gguf.tips.title': 'GGUF 格式模型',
|
||||||
|
|||||||
@@ -90,6 +90,7 @@ export default {
|
|||||||
// --- Resource events ---
|
// --- Resource events ---
|
||||||
'usage.events.resourceType': '资源类型',
|
'usage.events.resourceType': '资源类型',
|
||||||
'usage.events.eventType': '事件类型',
|
'usage.events.eventType': '事件类型',
|
||||||
|
'usage.events.resourceName': '按名称筛选',
|
||||||
'usage.events.col.time': '时间',
|
'usage.events.col.time': '时间',
|
||||||
'usage.events.col.resource': '资源',
|
'usage.events.col.resource': '资源',
|
||||||
'usage.events.col.event': '事件',
|
'usage.events.col.event': '事件',
|
||||||
|
|||||||
@@ -1,9 +1,15 @@
|
|||||||
import useCoolColors from '@/hooks/use-cool-colors';
|
import useCoolColors from '@/hooks/use-cool-colors';
|
||||||
import { Chart } from '@gpustack/core-ui';
|
import { Chart } from '@gpustack/core-ui';
|
||||||
import { formatLargeNumber } from '@gpustack/core-ui/utils';
|
import { formatLargeNumber } from '@gpustack/core-ui/utils';
|
||||||
import { Empty, theme } from 'antd';
|
import { Empty, Spin, theme } from 'antd';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import React, { useEffect, useMemo, useRef } from 'react';
|
import React, {
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useRef,
|
||||||
|
useState
|
||||||
|
} from 'react';
|
||||||
|
|
||||||
export interface BarSeriesItem {
|
export interface BarSeriesItem {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -17,10 +23,18 @@ export interface BarChartProps {
|
|||||||
xAxisData: string[];
|
xAxisData: string[];
|
||||||
height: number | string;
|
height: number | string;
|
||||||
width?: number | string;
|
width?: number | string;
|
||||||
|
loading?: boolean;
|
||||||
legendData?: { name: string; icon?: string }[];
|
legendData?: { name: string; icon?: string }[];
|
||||||
labelFormatter?: (val: any, index?: number) => string;
|
labelFormatter?: (val: any, index?: number) => string;
|
||||||
tooltipValueFormatter?: (val: any) => string;
|
tooltipValueFormatter?: (val: any) => string;
|
||||||
title?: string;
|
title?: string;
|
||||||
|
grid?: {
|
||||||
|
left?: number | string;
|
||||||
|
right?: number | string;
|
||||||
|
top?: number | string;
|
||||||
|
bottom?: number | string;
|
||||||
|
containLabel?: boolean;
|
||||||
|
};
|
||||||
legendIsolate?: boolean;
|
legendIsolate?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,15 +45,38 @@ const BarChart: React.FC<BarChartProps> = (props) => {
|
|||||||
height,
|
height,
|
||||||
width,
|
width,
|
||||||
legendData,
|
legendData,
|
||||||
|
loading,
|
||||||
labelFormatter,
|
labelFormatter,
|
||||||
tooltipValueFormatter,
|
tooltipValueFormatter,
|
||||||
title,
|
title,
|
||||||
|
grid,
|
||||||
legendIsolate
|
legendIsolate
|
||||||
} = props;
|
} = props;
|
||||||
const { token } = theme.useToken();
|
const { token } = theme.useToken();
|
||||||
const chartRef = useRef<{ chart: any } | null>(null);
|
const chartRef = useRef<{ chart: any } | null>(null);
|
||||||
const generateCoolColors = useCoolColors();
|
const generateCoolColors = useCoolColors();
|
||||||
|
|
||||||
|
// ECharts reads the DOM width at init time; with a "100%" width it can pick
|
||||||
|
// up a stale/tiny value while the flex child's layout is still resolving,
|
||||||
|
// rendering every bar squeezed at the left edge until its internal (throttled)
|
||||||
|
// ResizeObserver corrects it ~100ms later — a visible blue-sliver flash. We
|
||||||
|
// measure the container ourselves via a callback ref (which fires during
|
||||||
|
// commit, before paint) and feed ECharts an explicit pixel width, so the very
|
||||||
|
// first render is already correct. No gating, so the chart mounts with no
|
||||||
|
// extra delay; the ResizeObserver keeps the width in sync afterwards.
|
||||||
|
const resizeObserverRef = useRef<ResizeObserver | null>(null);
|
||||||
|
const [measuredWidth, setMeasuredWidth] = useState(0);
|
||||||
|
const measureRef = useCallback((node: HTMLDivElement | null) => {
|
||||||
|
resizeObserverRef.current?.disconnect();
|
||||||
|
if (!node) return;
|
||||||
|
setMeasuredWidth(node.clientWidth);
|
||||||
|
resizeObserverRef.current = new ResizeObserver(() => {
|
||||||
|
setMeasuredWidth(node.clientWidth);
|
||||||
|
});
|
||||||
|
resizeObserverRef.current.observe(node);
|
||||||
|
}, []);
|
||||||
|
useEffect(() => () => resizeObserverRef.current?.disconnect(), []);
|
||||||
|
|
||||||
const dynamicColors = useMemo(
|
const dynamicColors = useMemo(
|
||||||
() => generateCoolColors(seriesData.length),
|
() => generateCoolColors(seriesData.length),
|
||||||
[seriesData.length, generateCoolColors]
|
[seriesData.length, generateCoolColors]
|
||||||
@@ -126,8 +163,9 @@ const BarChart: React.FC<BarChartProps> = (props) => {
|
|||||||
left: 0,
|
left: 0,
|
||||||
right: 0,
|
right: 0,
|
||||||
top: title ? 30 : 10,
|
top: title ? 30 : 10,
|
||||||
bottom: 0,
|
bottom: 8,
|
||||||
containLabel: true
|
containLabel: true,
|
||||||
|
...grid
|
||||||
},
|
},
|
||||||
tooltip: {
|
tooltip: {
|
||||||
trigger: 'axis',
|
trigger: 'axis',
|
||||||
@@ -268,18 +306,43 @@ const BarChart: React.FC<BarChartProps> = (props) => {
|
|||||||
justifyContent: 'center'
|
justifyContent: 'center'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />
|
{loading ? (
|
||||||
|
<Spin size="middle" />
|
||||||
|
) : (
|
||||||
|
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Chart
|
<div
|
||||||
ref={chartRef as any}
|
ref={measureRef}
|
||||||
options={options as any}
|
style={{ width: width || '100%', height, position: 'relative' }}
|
||||||
height={height}
|
>
|
||||||
width={width || '100%'}
|
<Chart
|
||||||
/>
|
ref={chartRef as any}
|
||||||
|
options={options as any}
|
||||||
|
height={height}
|
||||||
|
width={measuredWidth || width || '100%'}
|
||||||
|
/>
|
||||||
|
{loading && (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
inset: 20,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
background: 'var(--ant-color-bg-container)',
|
||||||
|
opacity: 0.6,
|
||||||
|
pointerEvents: 'none'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Spin size="middle" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,13 @@ import useCoolColors from '@/hooks/use-cool-colors';
|
|||||||
import { Chart } from '@gpustack/core-ui';
|
import { Chart } from '@gpustack/core-ui';
|
||||||
import { formatLargeNumber } from '@gpustack/core-ui/utils';
|
import { formatLargeNumber } from '@gpustack/core-ui/utils';
|
||||||
import { Empty, Spin, theme } from 'antd';
|
import { Empty, Spin, theme } from 'antd';
|
||||||
import React, { useMemo, useRef } from 'react';
|
import React, {
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useRef,
|
||||||
|
useState
|
||||||
|
} from 'react';
|
||||||
|
|
||||||
export interface PieChartItem {
|
export interface PieChartItem {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -37,6 +43,26 @@ const PieChart: React.FC<PieChartProps> = ({
|
|||||||
const chartRef = useRef<{ chart: any } | null>(null);
|
const chartRef = useRef<{ chart: any } | null>(null);
|
||||||
const generateCoolColors = useCoolColors();
|
const generateCoolColors = useCoolColors();
|
||||||
|
|
||||||
|
// ECharts reads the DOM width at init time; with a "100%" width it can pick
|
||||||
|
// up a stale/tiny value while layout is still resolving, briefly rendering
|
||||||
|
// the donut undersized before its internal (throttled) ResizeObserver
|
||||||
|
// corrects it — a visible flash. We measure the container via a callback ref
|
||||||
|
// (fires during commit, before paint) and feed ECharts an explicit pixel
|
||||||
|
// width so the first render is already correct. The ResizeObserver keeps it
|
||||||
|
// in sync afterwards.
|
||||||
|
const resizeObserverRef = useRef<ResizeObserver | null>(null);
|
||||||
|
const [measuredWidth, setMeasuredWidth] = useState(0);
|
||||||
|
const measureRef = useCallback((node: HTMLDivElement | null) => {
|
||||||
|
resizeObserverRef.current?.disconnect();
|
||||||
|
if (!node) return;
|
||||||
|
setMeasuredWidth(node.clientWidth);
|
||||||
|
resizeObserverRef.current = new ResizeObserver(() => {
|
||||||
|
setMeasuredWidth(node.clientWidth);
|
||||||
|
});
|
||||||
|
resizeObserverRef.current.observe(node);
|
||||||
|
}, []);
|
||||||
|
useEffect(() => () => resizeObserverRef.current?.disconnect(), []);
|
||||||
|
|
||||||
const colors = useMemo(() => {
|
const colors = useMemo(() => {
|
||||||
const generatedColors = generateCoolColors(data.length + colorOffset);
|
const generatedColors = generateCoolColors(data.length + colorOffset);
|
||||||
return generatedColors.slice(colorOffset);
|
return generatedColors.slice(colorOffset);
|
||||||
@@ -142,12 +168,12 @@ const PieChart: React.FC<PieChartProps> = ({
|
|||||||
const displayTotal = total ?? data.reduce((sum, item) => sum + item.value, 0);
|
const displayTotal = total ?? data.reduce((sum, item) => sum + item.value, 0);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ width, height, position: 'relative' }}>
|
<div ref={measureRef} style={{ width, height, position: 'relative' }}>
|
||||||
<Chart
|
<Chart
|
||||||
ref={chartRef as any}
|
ref={chartRef as any}
|
||||||
options={options as any}
|
options={options as any}
|
||||||
height={height}
|
height={height}
|
||||||
width={width}
|
width={measuredWidth || width}
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { PageAction } from '@/config';
|
import { PageAction } from '@/config';
|
||||||
import { PageActionType } from '@/config/types';
|
import { PageActionType } from '@/config/types';
|
||||||
|
import useSubmitLock from '@/hooks/use-submit-lock';
|
||||||
import {
|
import {
|
||||||
AlertBlockInfo,
|
AlertBlockInfo,
|
||||||
Input as CInput,
|
Input as CInput,
|
||||||
@@ -45,7 +46,7 @@ const AddModal: React.FC<AddModalProps> = ({
|
|||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const [showKey, setShowKey] = useState(false);
|
const [showKey, setShowKey] = useState(false);
|
||||||
const [apikeyValue, setAPIKeyValue] = useState('');
|
const [apikeyValue, setAPIKeyValue] = useState('');
|
||||||
const [loading, setLoading] = useState(false);
|
const { loading, guard, run, release } = useSubmitLock();
|
||||||
const [isChanged, setIsChanged] = useState(false);
|
const [isChanged, setIsChanged] = useState(false);
|
||||||
const cacheFormRef = useRef<{
|
const cacheFormRef = useRef<{
|
||||||
allowed_type: string;
|
allowed_type: string;
|
||||||
@@ -116,31 +117,31 @@ const AddModal: React.FC<AddModalProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleOnOk = async (formdata: FormData) => {
|
const handleOnOk = async (formdata: FormData) => {
|
||||||
try {
|
await run(async () => {
|
||||||
setLoading(true);
|
try {
|
||||||
const data = {
|
const data = {
|
||||||
..._.omit(formdata, ['allowed_type']),
|
..._.omit(formdata, ['allowed_type']),
|
||||||
allowed_model_names:
|
allowed_model_names:
|
||||||
formdata.allowed_type === 'all' ||
|
formdata.allowed_type === 'all' ||
|
||||||
!formdata.scope?.includes('inference')
|
!formdata.scope?.includes('inference')
|
||||||
? []
|
? []
|
||||||
: formdata.allowed_model_names || []
|
: formdata.allowed_model_names || []
|
||||||
};
|
};
|
||||||
if (action === PageAction.CREATE) {
|
if (action === PageAction.CREATE) {
|
||||||
await createAPIKey(data);
|
await createAPIKey(data);
|
||||||
} else if (action === PageAction.EDIT && currentData?.id) {
|
} else if (action === PageAction.EDIT && currentData?.id) {
|
||||||
await updateAPIKey({
|
await updateAPIKey({
|
||||||
..._.omit(data, ['expires_in'])
|
..._.omit(data, ['expires_in'])
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// handled in interceptor
|
||||||
}
|
}
|
||||||
setLoading(false);
|
});
|
||||||
} catch (error) {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSumit = () => {
|
const handleSumit = () => {
|
||||||
form.submit();
|
guard(() => form.submit());
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDone = () => {
|
const handleDone = () => {
|
||||||
@@ -278,6 +279,7 @@ const AddModal: React.FC<AddModalProps> = ({
|
|||||||
name="addAPIKey"
|
name="addAPIKey"
|
||||||
form={form}
|
form={form}
|
||||||
onFinish={handleOnOk}
|
onFinish={handleOnOk}
|
||||||
|
onFinishFailed={release}
|
||||||
preserve={false}
|
preserve={false}
|
||||||
initialValues={{
|
initialValues={{
|
||||||
allowed_type: 'all',
|
allowed_type: 'all',
|
||||||
|
|||||||
@@ -3,6 +3,13 @@ export interface ListItem {
|
|||||||
description: string;
|
description: string;
|
||||||
id: number;
|
id: number;
|
||||||
value: string;
|
value: string;
|
||||||
|
masked_value?: string;
|
||||||
|
user_id?: number;
|
||||||
|
user_name?: string;
|
||||||
|
// The owning principal — an Org, or a USER principal when the key
|
||||||
|
// was created in someone's Personal Org. Read by the enterprise
|
||||||
|
// plugin's Organization column in the admin All-org view.
|
||||||
|
owner_principal_id?: number;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
expires_at: string;
|
expires_at: string;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
// columns.ts
|
// columns.ts
|
||||||
import { tableSorter } from '@/config/settings';
|
import { tableSorter } from '@/config/settings';
|
||||||
|
import { usePluginListColumns } from '@/plugins/list-extra-columns';
|
||||||
import { DashboardOutlined } from '@ant-design/icons';
|
import { DashboardOutlined } from '@ant-design/icons';
|
||||||
import {
|
import {
|
||||||
AutoTooltip,
|
AutoTooltip,
|
||||||
@@ -48,6 +49,7 @@ const useModelsColumns = ({
|
|||||||
onConfigAction
|
onConfigAction
|
||||||
}: ColumnsHookProps): ColumnsType<ListItem> => {
|
}: ColumnsHookProps): ColumnsType<ListItem> => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
const pluginCols = usePluginListColumns('apiKeys');
|
||||||
|
|
||||||
const actionList = useMemo<APIKeyAction[]>(() => {
|
const actionList = useMemo<APIKeyAction[]>(() => {
|
||||||
// Built-ins use a step-of-10 priority scale so plugins have room
|
// Built-ins use a step-of-10 priority scale so plugins have room
|
||||||
@@ -124,6 +126,12 @@ const useModelsColumns = ({
|
|||||||
}, [intl, configActions, onConfigAction]);
|
}, [intl, configActions, onConfigAction]);
|
||||||
|
|
||||||
return useMemo(() => {
|
return useMemo(() => {
|
||||||
|
const pluginRendered = pluginCols.map((c) => ({
|
||||||
|
title: intl.formatMessage({ id: c.titleId }),
|
||||||
|
key: c.key,
|
||||||
|
ellipsis: { showTitle: false },
|
||||||
|
render: (_text: any, record: ListItem) => c.render(record)
|
||||||
|
}));
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
title: intl.formatMessage({ id: 'common.table.name' }),
|
title: intl.formatMessage({ id: 'common.table.name' }),
|
||||||
@@ -152,6 +160,7 @@ const useModelsColumns = ({
|
|||||||
</span>
|
</span>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
...pluginRendered,
|
||||||
{
|
{
|
||||||
title: intl.formatMessage({ id: 'apikeys.table.key' }),
|
title: intl.formatMessage({ id: 'apikeys.table.key' }),
|
||||||
dataIndex: 'masked_value',
|
dataIndex: 'masked_value',
|
||||||
@@ -269,7 +278,7 @@ const useModelsColumns = ({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
}, [intl, showCreator, handleSelect, actionList]);
|
}, [intl, showCreator, handleSelect, actionList, pluginCols]);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default useModelsColumns;
|
export default useModelsColumns;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { PageAction } from '@/config';
|
import { PageAction } from '@/config';
|
||||||
import { PageActionType } from '@/config/types';
|
import { PageActionType } from '@/config/types';
|
||||||
|
import useSubmitLock from '@/hooks/use-submit-lock';
|
||||||
import {
|
import {
|
||||||
AlertBlockInfo,
|
AlertBlockInfo,
|
||||||
GSDrawer,
|
GSDrawer,
|
||||||
@@ -63,6 +64,7 @@ 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 alertRef = useRef<HTMLDivElement>(null);
|
const alertRef = useRef<HTMLDivElement>(null);
|
||||||
|
const { loading, guard, run, release } = useSubmitLock();
|
||||||
|
|
||||||
const showVersionCustomSuffix =
|
const showVersionCustomSuffix =
|
||||||
currentData?.backend_source === BackendSourceValueMap.BUILTIN ||
|
currentData?.backend_source === BackendSourceValueMap.BUILTIN ||
|
||||||
@@ -92,17 +94,22 @@ const AddModal: React.FC<AddModalProps> = (props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const onOk = () => {
|
const onOk = () => {
|
||||||
if (activeKey === 'yaml') {
|
guard(() => {
|
||||||
const content = editorRef.current?.getContent();
|
if (activeKey === 'yaml') {
|
||||||
if (content) {
|
const content = editorRef.current?.getContent();
|
||||||
onSubmitYaml({ content: content });
|
if (!content) {
|
||||||
|
// nothing to submit — drop the lock so the button stays usable
|
||||||
|
release();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
run(() => onSubmitYaml({ content: content }));
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
formRef.current?.submit();
|
formRef.current?.submit();
|
||||||
}
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const onFinish = (values: FormData) => {
|
const onFinish = async (values: FormData) => {
|
||||||
const versionConfigs = values.version_configs?.reduce(
|
const versionConfigs = values.version_configs?.reduce(
|
||||||
(acc: Record<string, any>, curr) => {
|
(acc: Record<string, any>, curr) => {
|
||||||
if (curr.version_no) {
|
if (curr.version_no) {
|
||||||
@@ -117,16 +124,15 @@ const AddModal: React.FC<AddModalProps> = (props) => {
|
|||||||
|
|
||||||
const defaultVersion = values.version_configs?.find((v) => v.is_default);
|
const defaultVersion = values.version_configs?.find((v) => v.is_default);
|
||||||
|
|
||||||
onSubmit({
|
await run(() =>
|
||||||
...values,
|
onSubmit({
|
||||||
parameter_format:
|
...values,
|
||||||
values.parameter_format === 'auto' || !values.parameter_format
|
parameter_format: values.parameter_format || null,
|
||||||
? null
|
default_version: defaultVersion?.version_no || '',
|
||||||
: values.parameter_format,
|
// @ts-ignore
|
||||||
default_version: defaultVersion?.version_no || '',
|
version_configs: versionConfigs
|
||||||
// @ts-ignore
|
})
|
||||||
version_configs: versionConfigs
|
);
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -159,7 +165,7 @@ const AddModal: React.FC<AddModalProps> = (props) => {
|
|||||||
return {
|
return {
|
||||||
...values,
|
...values,
|
||||||
backend_name: values.backend_name.replace(/-custom$/, ''),
|
backend_name: values.backend_name.replace(/-custom$/, ''),
|
||||||
parameter_format: values.parameter_format ?? 'auto',
|
parameter_format: values.parameter_format,
|
||||||
version_configs: versionConfigs,
|
version_configs: versionConfigs,
|
||||||
built_in_version_configs: builtInVersions
|
built_in_version_configs: builtInVersions
|
||||||
};
|
};
|
||||||
@@ -279,6 +285,7 @@ const AddModal: React.FC<AddModalProps> = (props) => {
|
|||||||
<ModalFooter
|
<ModalFooter
|
||||||
onCancel={onClose}
|
onCancel={onClose}
|
||||||
onOk={onOk}
|
onOk={onOk}
|
||||||
|
loading={loading}
|
||||||
style={ModalFooterStyle}
|
style={ModalFooterStyle}
|
||||||
></ModalFooter>
|
></ModalFooter>
|
||||||
</>
|
</>
|
||||||
@@ -295,6 +302,7 @@ const AddModal: React.FC<AddModalProps> = (props) => {
|
|||||||
children: (
|
children: (
|
||||||
<BackendForm
|
<BackendForm
|
||||||
onFinish={onFinish}
|
onFinish={onFinish}
|
||||||
|
onFinishFailed={release}
|
||||||
action={action}
|
action={action}
|
||||||
currentData={formContent as ListItem}
|
currentData={formContent as ListItem}
|
||||||
ref={formRef}
|
ref={formRef}
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ export interface FormData {
|
|||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
backend_source?: string;
|
backend_source?: string;
|
||||||
default_env?: Record<string, any>;
|
default_env?: Record<string, any>;
|
||||||
parameter_format?: 'space' | 'equal' | 'auto' | null;
|
parameter_format?: 'space' | 'equal' | null;
|
||||||
common_parameters?: string[];
|
common_parameters?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -151,27 +151,24 @@ const BasicForm = () => {
|
|||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item<FormData>
|
<Form.Item<FormData>
|
||||||
name="parameter_format"
|
name="parameter_format"
|
||||||
initialValue="auto"
|
|
||||||
rules={[{ required: false }]}
|
rules={[{ required: false }]}
|
||||||
>
|
>
|
||||||
<SealSelect
|
<SealSelect
|
||||||
label={intl.formatMessage({ id: 'backend.form.parameterFormat' })}
|
allowClear
|
||||||
|
label={intl.formatMessage({ id: 'backend.form.flagFormat' })}
|
||||||
|
description={intl.formatMessage({
|
||||||
|
id: 'backend.form.flagFormat.tips'
|
||||||
|
})}
|
||||||
options={[
|
options={[
|
||||||
{
|
{
|
||||||
label: intl.formatMessage({
|
label: intl.formatMessage({
|
||||||
id: 'backend.form.parameterFormat.default'
|
id: 'backend.form.flagFormat.space'
|
||||||
}),
|
|
||||||
value: 'auto'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: intl.formatMessage({
|
|
||||||
id: 'backend.form.parameterFormat.space'
|
|
||||||
}),
|
}),
|
||||||
value: 'space'
|
value: 'space'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: intl.formatMessage({
|
label: intl.formatMessage({
|
||||||
id: 'backend.form.parameterFormat.equal'
|
id: 'backend.form.flagFormat.equal'
|
||||||
}),
|
}),
|
||||||
value: 'equal'
|
value: 'equal'
|
||||||
}
|
}
|
||||||
@@ -183,6 +180,9 @@ const BasicForm = () => {
|
|||||||
rules={[{ required: false }]}
|
rules={[{ required: false }]}
|
||||||
>
|
>
|
||||||
<ListInput
|
<ListInput
|
||||||
|
description={intl.formatMessage({
|
||||||
|
id: 'backend.form.commonParameters.tips'
|
||||||
|
})}
|
||||||
btnText={intl.formatMessage({ id: 'backend.form.addParameter' })}
|
btnText={intl.formatMessage({ id: 'backend.form.addParameter' })}
|
||||||
label={intl.formatMessage({
|
label={intl.formatMessage({
|
||||||
id: 'backend.form.commonParameters'
|
id: 'backend.form.commonParameters'
|
||||||
|
|||||||
@@ -13,10 +13,11 @@ type AddModalProps = {
|
|||||||
action: PageActionType;
|
action: PageActionType;
|
||||||
currentData?: ListItem;
|
currentData?: ListItem;
|
||||||
onFinish: (values: FormData) => void;
|
onFinish: (values: FormData) => void;
|
||||||
|
onFinishFailed?: (errorInfo: any) => void;
|
||||||
ref?: any;
|
ref?: any;
|
||||||
};
|
};
|
||||||
const BackendForm: React.FC<AddModalProps> = forwardRef(
|
const BackendForm: React.FC<AddModalProps> = forwardRef(
|
||||||
({ action, currentData, onFinish }, ref) => {
|
({ action, currentData, onFinish, onFinishFailed }, ref) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
const [activeKey, setActiveKey] = React.useState<string[]>([]);
|
const [activeKey, setActiveKey] = React.useState<string[]>([]);
|
||||||
@@ -26,7 +27,7 @@ const BackendForm: React.FC<AddModalProps> = forwardRef(
|
|||||||
currentData?.backend_source === BackendSourceValueMap.BUILTIN ||
|
currentData?.backend_source === BackendSourceValueMap.BUILTIN ||
|
||||||
currentData?.backend_source === BackendSourceValueMap.COMMUNITY;
|
currentData?.backend_source === BackendSourceValueMap.COMMUNITY;
|
||||||
|
|
||||||
const onFinishFailed = (errorInfo: any) => {
|
const handleOnFinishFailed = (errorInfo: any) => {
|
||||||
const errorFields = errorInfo.errorFields || [];
|
const errorFields = errorInfo.errorFields || [];
|
||||||
if (errorFields.length > 0) {
|
if (errorFields.length > 0) {
|
||||||
const versionError = errorFields.find((field: any) =>
|
const versionError = errorFields.find((field: any) =>
|
||||||
@@ -38,6 +39,7 @@ const BackendForm: React.FC<AddModalProps> = forwardRef(
|
|||||||
setActiveKey([]);
|
setActiveKey([]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
onFinishFailed?.(errorInfo);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleOnFinish = (values: FormData) => {
|
const handleOnFinish = (values: FormData) => {
|
||||||
@@ -106,7 +108,7 @@ const BackendForm: React.FC<AddModalProps> = forwardRef(
|
|||||||
preserve={false}
|
preserve={false}
|
||||||
scrollToFirstError={true}
|
scrollToFirstError={true}
|
||||||
initialValues={_.omit(currentData, ['version_configs'])}
|
initialValues={_.omit(currentData, ['version_configs'])}
|
||||||
onFinishFailed={onFinishFailed}
|
onFinishFailed={handleOnFinishFailed}
|
||||||
>
|
>
|
||||||
<BasicForm></BasicForm>
|
<BasicForm></BasicForm>
|
||||||
<VersionsForm
|
<VersionsForm
|
||||||
|
|||||||
@@ -251,7 +251,7 @@ const BackendList = () => {
|
|||||||
selectHolder={intl.formatMessage({ id: 'backend.filter.source' })}
|
selectHolder={intl.formatMessage({ id: 'backend.filter.source' })}
|
||||||
buttonText={intl.formatMessage({ id: 'backend.button.add' })}
|
buttonText={intl.formatMessage({ id: 'backend.button.add' })}
|
||||||
handleClickPrimary={handleAddBackend}
|
handleClickPrimary={handleAddBackend}
|
||||||
handleSearch={handleSearch}
|
handleSearch={() => loadMore(1)}
|
||||||
handleSelectChange={handleFilterBySource}
|
handleSelectChange={handleFilterBySource}
|
||||||
handleInputChange={handleNameChange}
|
handleInputChange={handleNameChange}
|
||||||
rowSelection={rowSelection}
|
rowSelection={rowSelection}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { PageActionType } from '@/config/types';
|
import { PageActionType } from '@/config/types';
|
||||||
|
import useSubmitLock from '@/hooks/use-submit-lock';
|
||||||
import { FormDrawer } from '@gpustack/core-ui';
|
import { FormDrawer } from '@gpustack/core-ui';
|
||||||
import React, { useRef } from 'react';
|
import React, { useRef } from 'react';
|
||||||
import { FormData, BenchmarkListItem as ListItem } from '../config/types';
|
import { FormData, BenchmarkListItem as ListItem } from '../config/types';
|
||||||
@@ -22,21 +23,24 @@ const AddBenchmark: React.FC<AddModalProps> = ({
|
|||||||
open,
|
open,
|
||||||
currentData,
|
currentData,
|
||||||
clusterList,
|
clusterList,
|
||||||
profilesOptions,
|
profilesOptions = [],
|
||||||
datasetList,
|
datasetList = [],
|
||||||
onOk,
|
onOk,
|
||||||
onCancel
|
onCancel
|
||||||
}) => {
|
}) => {
|
||||||
const form = useRef<any>(null);
|
const form = useRef<any>(null);
|
||||||
|
const { loading, guard, run, release } = useSubmitLock();
|
||||||
|
|
||||||
const handleSubmit = () => {
|
const handleSubmit = () => {
|
||||||
form.current?.submit();
|
guard(() => form.current?.submit());
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleOk = async (data: FormData) => {
|
const handleOk = async (data: FormData) => {
|
||||||
onOk({
|
await run(() =>
|
||||||
...data
|
onOk({
|
||||||
});
|
...data
|
||||||
|
})
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCancel = () => {
|
const handleCancel = () => {
|
||||||
@@ -51,6 +55,7 @@ const AddBenchmark: React.FC<AddModalProps> = ({
|
|||||||
onCancel={handleCancel}
|
onCancel={handleCancel}
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
width={600}
|
width={600}
|
||||||
|
loading={loading}
|
||||||
>
|
>
|
||||||
<BenchmarkForm
|
<BenchmarkForm
|
||||||
ref={form}
|
ref={form}
|
||||||
@@ -61,6 +66,7 @@ const AddBenchmark: React.FC<AddModalProps> = ({
|
|||||||
profilesOptions={profilesOptions}
|
profilesOptions={profilesOptions}
|
||||||
datasetList={datasetList}
|
datasetList={datasetList}
|
||||||
onFinish={handleOk}
|
onFinish={handleOk}
|
||||||
|
onFinishFailed={release}
|
||||||
/>
|
/>
|
||||||
</FormDrawer>
|
</FormDrawer>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { modelCategoriesMap } from '@/pages/llmodels/config';
|
|
||||||
import { SearchOutlined, SyncOutlined } from '@ant-design/icons';
|
import { SearchOutlined, SyncOutlined } from '@ant-design/icons';
|
||||||
import { FiltersButton } from '@gpustack/core-ui';
|
import { BaseSelect } from '@gpustack/core-ui';
|
||||||
import { useIntl } from '@umijs/max';
|
import { useIntl } from '@umijs/max';
|
||||||
import { Button, Input, Space } from 'antd';
|
import { Button, Input, Space } from 'antd';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { profileOptions } from '../config';
|
||||||
|
|
||||||
export interface RightActionsProps {
|
export interface RightActionsProps {
|
||||||
handleInputChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
handleInputChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
||||||
@@ -38,22 +38,15 @@ const RightActions: React.FC<RightActionsProps> = ({
|
|||||||
|
|
||||||
const handleGPUChange = debounceUpdateFilter;
|
const handleGPUChange = debounceUpdateFilter;
|
||||||
|
|
||||||
const modelOptions = modelList
|
const handleSearchByModelDebounce = _.debounce((value: any) => {
|
||||||
?.filter((item) => {
|
handleQueryChange({
|
||||||
return item.categories?.includes(modelCategoriesMap.llm);
|
page: 1,
|
||||||
})
|
model_name: value
|
||||||
.map((item) => ({
|
});
|
||||||
label: item.label,
|
}, 350);
|
||||||
value: item.label
|
|
||||||
}));
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Space>
|
<Space>
|
||||||
<FiltersButton
|
|
||||||
onClick={toggleFilters}
|
|
||||||
count={count}
|
|
||||||
onClear={onClear}
|
|
||||||
></FiltersButton>
|
|
||||||
<Input
|
<Input
|
||||||
prefix={
|
prefix={
|
||||||
<SearchOutlined
|
<SearchOutlined
|
||||||
@@ -63,11 +56,24 @@ const RightActions: React.FC<RightActionsProps> = ({
|
|||||||
placeholder={intl.formatMessage({
|
placeholder={intl.formatMessage({
|
||||||
id: 'common.filter.name'
|
id: 'common.filter.name'
|
||||||
})}
|
})}
|
||||||
style={{ width: 300 }}
|
style={{ width: 200 }}
|
||||||
allowClear
|
allowClear
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
></Input>
|
></Input>
|
||||||
{/* <Input
|
<Input
|
||||||
|
prefix={
|
||||||
|
<SearchOutlined
|
||||||
|
style={{ color: 'var(--ant-color-text-placeholder)' }}
|
||||||
|
></SearchOutlined>
|
||||||
|
}
|
||||||
|
placeholder={intl.formatMessage({
|
||||||
|
id: 'benchmark.table.filter.bymodel'
|
||||||
|
})}
|
||||||
|
style={{ width: 180 }}
|
||||||
|
allowClear
|
||||||
|
onChange={handleSearchByModelDebounce}
|
||||||
|
></Input>
|
||||||
|
<Input
|
||||||
prefix={
|
prefix={
|
||||||
<SearchOutlined
|
<SearchOutlined
|
||||||
style={{ color: 'var(--ant-color-text-placeholder)' }}
|
style={{ color: 'var(--ant-color-text-placeholder)' }}
|
||||||
@@ -104,20 +110,6 @@ const RightActions: React.FC<RightActionsProps> = ({
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
></BaseSelect>
|
></BaseSelect>
|
||||||
<BaseSelect
|
|
||||||
allowClear
|
|
||||||
placeholder={intl.formatMessage({
|
|
||||||
id: 'benchmark.table.filter.bymodel'
|
|
||||||
})}
|
|
||||||
style={{ width: 180 }}
|
|
||||||
options={modelOptions}
|
|
||||||
onChange={(value, option) =>
|
|
||||||
handleQueryChange({
|
|
||||||
model_name: value,
|
|
||||||
page: 1
|
|
||||||
})
|
|
||||||
}
|
|
||||||
></BaseSelect> */}
|
|
||||||
<Button
|
<Button
|
||||||
type="text"
|
type="text"
|
||||||
style={{ color: 'var(--ant-color-text-tertiary)' }}
|
style={{ color: 'var(--ant-color-text-tertiary)' }}
|
||||||
|
|||||||
@@ -17,6 +17,9 @@ const ViewLogsModal: React.FC<ViewModalProps> = (props) => {
|
|||||||
const logsViewerRef = React.useRef<any>(null);
|
const logsViewerRef = React.useRef<any>(null);
|
||||||
const requestRef = React.useRef<any>(null);
|
const requestRef = React.useRef<any>(null);
|
||||||
const contentRef = React.useRef<any>(null);
|
const contentRef = React.useRef<any>(null);
|
||||||
|
const [params, setParams] = React.useState<any>({
|
||||||
|
follow: true
|
||||||
|
});
|
||||||
|
|
||||||
const handleCancel = useCallback(() => {
|
const handleCancel = useCallback(() => {
|
||||||
logsViewerRef.current?.abort();
|
logsViewerRef.current?.abort();
|
||||||
@@ -94,9 +97,7 @@ const ViewLogsModal: React.FC<ViewModalProps> = (props) => {
|
|||||||
tail={undefined}
|
tail={undefined}
|
||||||
enableScorllLoad={true}
|
enableScorllLoad={true}
|
||||||
isDownloading={false}
|
isDownloading={false}
|
||||||
params={{
|
params={params}
|
||||||
follow: true
|
|
||||||
}}
|
|
||||||
></LogsViewer>
|
></LogsViewer>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
@@ -87,6 +87,9 @@ export interface FormData {
|
|||||||
|
|
||||||
export interface BenchmarkListItem extends FormData {
|
export interface BenchmarkListItem extends FormData {
|
||||||
id: number;
|
id: number;
|
||||||
|
// Inherited from the parent cluster's owner_principal_id on the
|
||||||
|
// wire so per-row tenant filtering works without joining.
|
||||||
|
owner_principal_id?: number | null;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
state: string;
|
state: string;
|
||||||
|
|||||||
@@ -48,6 +48,15 @@ const BasicForm: React.FC = () => {
|
|||||||
);
|
);
|
||||||
}, [orgScoped, scopedClusterList, clusterList, scopeOrgId]);
|
}, [orgScoped, scopedClusterList, clusterList, scopeOrgId]);
|
||||||
|
|
||||||
|
const handleOnOrgChange = (value: any) => {
|
||||||
|
form.setFieldsValue({
|
||||||
|
model_name: undefined,
|
||||||
|
model_id: undefined,
|
||||||
|
model_instance_name: undefined,
|
||||||
|
model_instance: undefined
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (action === PageAction.CREATE && orgScoped) {
|
if (action === PageAction.CREATE && orgScoped) {
|
||||||
fetchScopedClusters({ page: -1 });
|
fetchScopedClusters({ page: -1 });
|
||||||
@@ -108,7 +117,10 @@ const BasicForm: React.FC = () => {
|
|||||||
required
|
required
|
||||||
></CInput.Input>
|
></CInput.Input>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<PluginExtraFields name="CreateOrgScopeField" context={{ action }} />
|
<PluginExtraFields
|
||||||
|
name="CreateOrgScopeField"
|
||||||
|
context={{ action, onChange: handleOnOrgChange }}
|
||||||
|
/>
|
||||||
<Form.Item<FormData>
|
<Form.Item<FormData>
|
||||||
name="cluster_id"
|
name="cluster_id"
|
||||||
rules={[
|
rules={[
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ interface ProviderFormProps {
|
|||||||
datasetList: Global.BaseOption<number | string>[];
|
datasetList: Global.BaseOption<number | string>[];
|
||||||
profilesOptions: Global.BaseOption<string>[];
|
profilesOptions: Global.BaseOption<string>[];
|
||||||
onFinish: (values: FormData) => Promise<void>;
|
onFinish: (values: FormData) => Promise<void>;
|
||||||
|
onFinishFailed?: (errorInfo: any) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const TABKeysMap = {
|
const TABKeysMap = {
|
||||||
@@ -42,6 +43,7 @@ const ProviderForm: React.FC<ProviderFormProps> = forwardRef((props, ref) => {
|
|||||||
action,
|
action,
|
||||||
currentData,
|
currentData,
|
||||||
onFinish,
|
onFinish,
|
||||||
|
onFinishFailed,
|
||||||
open,
|
open,
|
||||||
clusterList,
|
clusterList,
|
||||||
profilesOptions,
|
profilesOptions,
|
||||||
@@ -122,6 +124,7 @@ const ProviderForm: React.FC<ProviderFormProps> = forwardRef((props, ref) => {
|
|||||||
<Form
|
<Form
|
||||||
form={form}
|
form={form}
|
||||||
onFinish={onFinish}
|
onFinish={onFinish}
|
||||||
|
onFinishFailed={onFinishFailed}
|
||||||
initialValues={{
|
initialValues={{
|
||||||
dataset_input_tokens: null,
|
dataset_input_tokens: null,
|
||||||
dataset_output_tokens: null,
|
dataset_output_tokens: null,
|
||||||
|
|||||||
@@ -9,8 +9,9 @@ import { useQueryModelInstancesList } from '@/pages/llmodels/services/use-query-
|
|||||||
import { useQueryModelList } from '@/pages/llmodels/services/use-query-model-list';
|
import { useQueryModelList } from '@/pages/llmodels/services/use-query-model-list';
|
||||||
import { Cascader as SealCascader, useAppUtils } from '@gpustack/core-ui';
|
import { Cascader as SealCascader, useAppUtils } from '@gpustack/core-ui';
|
||||||
import { useIntl } from '@umijs/max';
|
import { useIntl } from '@umijs/max';
|
||||||
|
import { useMemoizedFn } from 'ahooks';
|
||||||
import { Form, Tooltip } from 'antd';
|
import { Form, Tooltip } from 'antd';
|
||||||
import React, { useEffect, useRef } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { useFormContext } from '../config/form-context';
|
import { useFormContext } from '../config/form-context';
|
||||||
import { FormData } from '../config/types';
|
import { FormData } from '../config/types';
|
||||||
|
|
||||||
@@ -45,11 +46,7 @@ const ModelInstanceForm: React.FC = () => {
|
|||||||
const form = Form.useFormInstance();
|
const form = Form.useFormInstance();
|
||||||
const { getRuleMessage } = useAppUtils();
|
const { getRuleMessage } = useAppUtils();
|
||||||
const { action, open } = useFormContext();
|
const { action, open } = useFormContext();
|
||||||
// Owned by the create-scope picker slot (admin "All" view). The model list
|
const clusterId = Form.useWatch('cluster_id', form);
|
||||||
// is tenant-scoped by the request header, so refetch it when the org
|
|
||||||
// changes so only the chosen org's models/instances are offered.
|
|
||||||
const scopeOrgId = Form.useWatch('organization_id', form);
|
|
||||||
const prevScopeRef = useRef<number | null | undefined>(undefined);
|
|
||||||
const [modelList, setModelList] = React.useState<any[]>([]);
|
const [modelList, setModelList] = React.useState<any[]>([]);
|
||||||
const {
|
const {
|
||||||
loading: modelLoading,
|
loading: modelLoading,
|
||||||
@@ -84,6 +81,16 @@ const ModelInstanceForm: React.FC = () => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const clearModelInstance = () => {
|
||||||
|
setModelList([]);
|
||||||
|
form.setFieldsValue({
|
||||||
|
model_name: '',
|
||||||
|
model_id: '',
|
||||||
|
model_instance_name: '',
|
||||||
|
model_instance: ''
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const loadInstances = async (selectedOptions: any[]) => {
|
const loadInstances = async (selectedOptions: any[]) => {
|
||||||
const targetOption = selectedOptions[selectedOptions.length - 1];
|
const targetOption = selectedOptions[selectedOptions.length - 1];
|
||||||
if (targetOption && targetOption.children.length === 0) {
|
if (targetOption && targetOption.children.length === 0) {
|
||||||
@@ -107,10 +114,12 @@ const ModelInstanceForm: React.FC = () => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
const initModelInstance = useMemoizedFn(async () => {
|
||||||
const initModelInstance = async () => {
|
if (!clusterId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
// fetch model list when dropdown is opened
|
// fetch model list when dropdown is opened
|
||||||
const list = await fetchModelList({ page: -1 });
|
const list = await fetchModelList({ page: -1, cluster_id: clusterId });
|
||||||
const modelOptions = list
|
const modelOptions = list
|
||||||
.filter((model: any) => model.replicas > 0)
|
.filter((model: any) => model.replicas > 0)
|
||||||
.map((model: any) => ({
|
.map((model: any) => ({
|
||||||
@@ -124,6 +133,7 @@ const ModelInstanceForm: React.FC = () => {
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
if (modelOptions.length === 0) {
|
if (modelOptions.length === 0) {
|
||||||
|
clearModelInstance();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -133,6 +143,12 @@ const ModelInstanceForm: React.FC = () => {
|
|||||||
);
|
);
|
||||||
if (!selectedllmModel) {
|
if (!selectedllmModel) {
|
||||||
setModelList(modelOptions);
|
setModelList(modelOptions);
|
||||||
|
form.setFieldsValue({
|
||||||
|
model_name: '',
|
||||||
|
model_id: '',
|
||||||
|
model_instance_name: '',
|
||||||
|
model_instance: ''
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const instanceList = await fetchInstanceList({ id: selectedllmModel.id });
|
const instanceList = await fetchInstanceList({ id: selectedllmModel.id });
|
||||||
@@ -159,33 +175,18 @@ const ModelInstanceForm: React.FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setModelList(modelOptions);
|
setModelList(modelOptions);
|
||||||
};
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (open && action === PageAction.CREATE) {
|
if (open && action === PageAction.CREATE) {
|
||||||
// On a genuine org change, clear the stale (possibly cross-org) target
|
|
||||||
// so the refetched list re-selects within the new org.
|
|
||||||
if (
|
|
||||||
prevScopeRef.current !== undefined &&
|
|
||||||
prevScopeRef.current !== scopeOrgId
|
|
||||||
) {
|
|
||||||
form.setFieldsValue({
|
|
||||||
model_name: undefined,
|
|
||||||
model_id: undefined,
|
|
||||||
model_instance_name: undefined,
|
|
||||||
model_instance: undefined
|
|
||||||
});
|
|
||||||
}
|
|
||||||
prevScopeRef.current = scopeOrgId;
|
|
||||||
initModelInstance();
|
initModelInstance();
|
||||||
}
|
}
|
||||||
if (!open) {
|
if (!open) {
|
||||||
prevScopeRef.current = undefined;
|
|
||||||
cancelModelRequest();
|
cancelModelRequest();
|
||||||
cancelInstanceRequest();
|
cancelInstanceRequest();
|
||||||
clearBenchmarkTargetInstance();
|
clearBenchmarkTargetInstance();
|
||||||
}
|
}
|
||||||
}, [open, benchmarkTargetInstance, action, scopeOrgId]);
|
}, [open, action, clusterId]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form.Item<FormData>
|
<Form.Item<FormData>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
// columns.ts
|
// columns.ts
|
||||||
import { tableSorter } from '@/config/settings';
|
import { tableSorter } from '@/config/settings';
|
||||||
|
import { usePluginListColumns } from '@/plugins/list-extra-columns';
|
||||||
import { AutoTooltip } from '@gpustack/core-ui';
|
import { AutoTooltip } from '@gpustack/core-ui';
|
||||||
import { useIntl } from '@umijs/max';
|
import { useIntl } from '@umijs/max';
|
||||||
import { Typography } from 'antd';
|
import { Typography } from 'antd';
|
||||||
@@ -15,8 +16,15 @@ const useBenchmarkColumns = (params: {
|
|||||||
}): ColumnsType<ListItem> => {
|
}): ColumnsType<ListItem> => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const { onCellClick, handleSelect, columns } = params;
|
const { onCellClick, handleSelect, columns } = params;
|
||||||
|
const pluginCols = usePluginListColumns('benchmarks');
|
||||||
|
|
||||||
return useMemo(() => {
|
return useMemo(() => {
|
||||||
|
const pluginRendered = pluginCols.map((c) => ({
|
||||||
|
title: intl.formatMessage({ id: c.titleId }),
|
||||||
|
key: c.key,
|
||||||
|
ellipsis: { showTitle: false },
|
||||||
|
render: (_text: any, record: ListItem) => c.render(record)
|
||||||
|
}));
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
title: (
|
title: (
|
||||||
@@ -34,6 +42,7 @@ const useBenchmarkColumns = (params: {
|
|||||||
</AutoTooltip>
|
</AutoTooltip>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
...pluginRendered,
|
||||||
...columns,
|
...columns,
|
||||||
{
|
{
|
||||||
title: (
|
title: (
|
||||||
@@ -51,7 +60,7 @@ const useBenchmarkColumns = (params: {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
}, [intl, onCellClick, handleSelect, columns]);
|
}, [intl, onCellClick, handleSelect, columns, pluginCols]);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default useBenchmarkColumns;
|
export default useBenchmarkColumns;
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
import { IconFont } from '@gpustack/core-ui';
|
||||||
|
import { useIntl } from '@umijs/max';
|
||||||
|
import { Button, Result, Typography } from 'antd';
|
||||||
|
import PageBox from '../_components/page-box';
|
||||||
|
|
||||||
|
// OSS-only upsell page for the billing module. The real Billing UI ships
|
||||||
|
// in the enterprise plugin and shadows this route via
|
||||||
|
// `routes.extensions.ts` — when the enterprise plugin is loaded, the
|
||||||
|
// merger removes this entry by name. So this file is what users see on
|
||||||
|
// the OSS build only.
|
||||||
|
const Billing: React.FC = () => {
|
||||||
|
const intl = useIntl();
|
||||||
|
const features = [
|
||||||
|
'billing.upsell.feature.usage',
|
||||||
|
'billing.upsell.feature.invoices',
|
||||||
|
'billing.upsell.feature.budgets',
|
||||||
|
'billing.upsell.feature.chargeback'
|
||||||
|
];
|
||||||
|
return (
|
||||||
|
<PageBox>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
// Use a viewport-relative min-height so flex centering has
|
||||||
|
// something to work against — percentage heights through
|
||||||
|
// PageBox's wrapper chain don't always resolve. The offset
|
||||||
|
// accounts for the global header + the page title bar.
|
||||||
|
minHeight: 'calc(100vh - 180px)',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Result
|
||||||
|
icon={
|
||||||
|
<IconFont
|
||||||
|
type="icon-billing-outlined"
|
||||||
|
style={{ fontSize: 64, color: 'var(--ant-color-primary)' }}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
title={intl.formatMessage({ id: 'billing.upsell.title' })}
|
||||||
|
subTitle={intl.formatMessage({ id: 'billing.upsell.subtitle' })}
|
||||||
|
extra={
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
maxWidth: 520,
|
||||||
|
margin: '0 auto',
|
||||||
|
textAlign: 'left'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography.Title level={5} style={{ marginBottom: 12 }}>
|
||||||
|
{intl.formatMessage({
|
||||||
|
id: 'billing.upsell.featuresTitle'
|
||||||
|
})}
|
||||||
|
</Typography.Title>
|
||||||
|
<ul style={{ paddingLeft: 20, marginBottom: 24 }}>
|
||||||
|
{features.map((id) => (
|
||||||
|
<li key={id} style={{ marginBottom: 6 }}>
|
||||||
|
{intl.formatMessage({ id })}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
<div style={{ textAlign: 'center', marginTop: 56 }}>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
href="https://gpustack.ai/enterprise"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
{intl.formatMessage({ id: 'billing.upsell.cta' })}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</PageBox>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Billing;
|
||||||
@@ -59,9 +59,16 @@ const ClusterCreate: React.FC<{
|
|||||||
// empty-state CTAs that already know which kind of cluster the user
|
// empty-state CTAs that already know which kind of cluster the user
|
||||||
// is heading for (e.g. GPU Service's "Add a Kubernetes Cluster").
|
// is heading for (e.g. GPU Service's "Add a Kubernetes Cluster").
|
||||||
providerHint?: string;
|
providerHint?: string;
|
||||||
|
presetClusterType?: 'model' | 'gpu';
|
||||||
setCurrentTitle?: (title: string) => void;
|
setCurrentTitle?: (title: string) => void;
|
||||||
onClose?: () => void;
|
onClose?: () => void;
|
||||||
}> = ({ onClose, action, providerHint, setCurrentTitle }) => {
|
}> = ({
|
||||||
|
onClose,
|
||||||
|
action,
|
||||||
|
providerHint,
|
||||||
|
presetClusterType,
|
||||||
|
setCurrentTitle
|
||||||
|
}) => {
|
||||||
const stepList = useStepList();
|
const stepList = useStepList();
|
||||||
const [systemConfigState] = useAtom(systemConfigAtom);
|
const [systemConfigState] = useAtom(systemConfigAtom);
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
@@ -377,6 +384,7 @@ const ClusterCreate: React.FC<{
|
|||||||
)}
|
)}
|
||||||
<StepsContext.Provider
|
<StepsContext.Provider
|
||||||
value={{
|
value={{
|
||||||
|
presetClusterType: presetClusterType,
|
||||||
formValues: formValues,
|
formValues: formValues,
|
||||||
systemConfig: systemConfigState
|
systemConfig: systemConfigState
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -6,10 +6,10 @@ import ClusterCreate from './cluster-create';
|
|||||||
interface ClusterModalProps {
|
interface ClusterModalProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
title: string;
|
title: string;
|
||||||
// When set, ClusterCreate preselects this provider and skips the
|
pendingProviderHint?: {
|
||||||
// provider-catalog step. Used by feature pages (e.g. GPU Service)
|
providerHint?: string;
|
||||||
// whose empty state already implies which kind of cluster is needed.
|
presetClusterType?: 'model' | 'gpu';
|
||||||
providerHint?: string;
|
};
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -17,7 +17,7 @@ const ClusterModal: React.FC<ClusterModalProps> = ({
|
|||||||
open,
|
open,
|
||||||
onClose,
|
onClose,
|
||||||
title,
|
title,
|
||||||
providerHint
|
pendingProviderHint
|
||||||
}) => {
|
}) => {
|
||||||
const [currentTitle, setCurrentTitle] = React.useState<string>(title);
|
const [currentTitle, setCurrentTitle] = React.useState<string>(title);
|
||||||
const handleCancel = () => {
|
const handleCancel = () => {
|
||||||
@@ -46,7 +46,8 @@ const ClusterModal: React.FC<ClusterModalProps> = ({
|
|||||||
<ClusterCreate
|
<ClusterCreate
|
||||||
onClose={handleCancel}
|
onClose={handleCancel}
|
||||||
action={PageAction.CREATE}
|
action={PageAction.CREATE}
|
||||||
providerHint={providerHint}
|
providerHint={pendingProviderHint?.providerHint}
|
||||||
|
presetClusterType={pendingProviderHint?.presetClusterType}
|
||||||
setCurrentTitle={setCurrentTitle}
|
setCurrentTitle={setCurrentTitle}
|
||||||
></ClusterCreate>
|
></ClusterCreate>
|
||||||
</GSDrawer>
|
</GSDrawer>
|
||||||
|
|||||||
@@ -321,13 +321,20 @@ const Clusters: React.FC = () => {
|
|||||||
// the session atom is cleared right after we open the modal, but
|
// the session atom is cleared right after we open the modal, but
|
||||||
// ClusterCreate mounts a tick later and needs the value to skip the
|
// ClusterCreate mounts a tick later and needs the value to skip the
|
||||||
// provider-catalog step. Cache it locally and clear on close.
|
// provider-catalog step. Cache it locally and clear on close.
|
||||||
const [pendingProviderHint, setPendingProviderHint] = useState<
|
const [pendingProviderHint, setPendingProviderHint] = useState<{
|
||||||
string | undefined
|
providerHint?: string;
|
||||||
>(undefined);
|
presetClusterType?: 'model' | 'gpu';
|
||||||
|
}>({
|
||||||
|
providerHint: undefined,
|
||||||
|
presetClusterType: undefined
|
||||||
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (clusterSession?.firstAddCluster && dataSource.loadend) {
|
if (clusterSession?.firstAddCluster && dataSource.loadend) {
|
||||||
setPendingProviderHint(clusterSession.providerHint);
|
setPendingProviderHint({
|
||||||
|
providerHint: clusterSession.providerHint,
|
||||||
|
presetClusterType: clusterSession.presetClusterType
|
||||||
|
});
|
||||||
openClusterModal();
|
openClusterModal();
|
||||||
// reset session
|
// reset session
|
||||||
setClusterSession(null);
|
setClusterSession(null);
|
||||||
@@ -335,7 +342,10 @@ const Clusters: React.FC = () => {
|
|||||||
}, [clusterSession, dataSource.loadend]);
|
}, [clusterSession, dataSource.loadend]);
|
||||||
|
|
||||||
const handleClusterModalClose = () => {
|
const handleClusterModalClose = () => {
|
||||||
setPendingProviderHint(undefined);
|
setPendingProviderHint({
|
||||||
|
providerHint: undefined,
|
||||||
|
presetClusterType: undefined
|
||||||
|
});
|
||||||
closeClusterModal();
|
closeClusterModal();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -464,7 +474,7 @@ const Clusters: React.FC = () => {
|
|||||||
id: 'menu.resources.clusterCreate'
|
id: 'menu.resources.clusterCreate'
|
||||||
})}
|
})}
|
||||||
open={clusterModalStatus.open}
|
open={clusterModalStatus.open}
|
||||||
providerHint={pendingProviderHint}
|
pendingProviderHint={pendingProviderHint}
|
||||||
onClose={handleClusterModalClose}
|
onClose={handleClusterModalClose}
|
||||||
></ClusterModal>
|
></ClusterModal>
|
||||||
{AddWorkerModal}
|
{AddWorkerModal}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { PageAction } from '@/config';
|
import { PageAction } from '@/config';
|
||||||
import { PageActionType } from '@/config/types';
|
import { PageActionType } from '@/config/types';
|
||||||
|
import useSubmitLock from '@/hooks/use-submit-lock';
|
||||||
import { ExclamationCircleFilled } from '@ant-design/icons';
|
import { ExclamationCircleFilled } from '@ant-design/icons';
|
||||||
import { AlertBlockInfo, FormDrawer, ModalFooter } from '@gpustack/core-ui';
|
import { AlertBlockInfo, FormDrawer, ModalFooter } from '@gpustack/core-ui';
|
||||||
import { useIntl } from '@umijs/max';
|
import { useIntl } from '@umijs/max';
|
||||||
@@ -39,20 +40,23 @@ const AddCluster: React.FC<AddModalProps> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const form = useRef<any>(null);
|
const form = useRef<any>(null);
|
||||||
|
const { loading, guard, run, release } = useSubmitLock();
|
||||||
// Whether the user has changed any k8s_options field. Lifted from ClusterForm
|
// Whether the user has changed any k8s_options field. Lifted from ClusterForm
|
||||||
// so the "re-run registration" notice can sit in the drawer footer, above the
|
// so the "re-run registration" notice can sit in the drawer footer, above the
|
||||||
// Save/Cancel buttons (mirrors the model edit interaction).
|
// Save/Cancel buttons (mirrors the model edit interaction).
|
||||||
const [k8sOptionsChanged, setK8sOptionsChanged] = useState<boolean>(false);
|
const [k8sOptionsChanged, setK8sOptionsChanged] = useState<boolean>(false);
|
||||||
|
|
||||||
const handleSubmit = () => {
|
const handleSubmit = () => {
|
||||||
form.current?.submit();
|
guard(() => form.current?.submit());
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleOk = async (data: FormData) => {
|
const handleOk = async (data: FormData) => {
|
||||||
onOk({
|
await run(() =>
|
||||||
...data,
|
onOk({
|
||||||
provider
|
...data,
|
||||||
});
|
provider
|
||||||
|
})
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCancel = () => {
|
const handleCancel = () => {
|
||||||
@@ -84,6 +88,7 @@ const AddCluster: React.FC<AddModalProps> = ({
|
|||||||
<ModalFooter
|
<ModalFooter
|
||||||
onOk={handleSubmit}
|
onOk={handleSubmit}
|
||||||
onCancel={handleCancel}
|
onCancel={handleCancel}
|
||||||
|
loading={loading}
|
||||||
style={ModalFooterStyle}
|
style={ModalFooterStyle}
|
||||||
></ModalFooter>
|
></ModalFooter>
|
||||||
</>
|
</>
|
||||||
@@ -96,6 +101,7 @@ const AddCluster: React.FC<AddModalProps> = ({
|
|||||||
action={action}
|
action={action}
|
||||||
currentData={currentData}
|
currentData={currentData}
|
||||||
onFinish={handleOk}
|
onFinish={handleOk}
|
||||||
|
onFinishFailed={release}
|
||||||
onK8sOptionsChange={setK8sOptionsChanged}
|
onK8sOptionsChange={setK8sOptionsChanged}
|
||||||
/>
|
/>
|
||||||
</FormDrawer>
|
</FormDrawer>
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ type ViewModalProps = {
|
|||||||
containerName?: string;
|
containerName?: string;
|
||||||
gpustackDataVolume?: string;
|
gpustackDataVolume?: string;
|
||||||
advertisAddress?: string;
|
advertisAddress?: string;
|
||||||
|
dtkVersion?: string;
|
||||||
registrationInfo: {
|
registrationInfo: {
|
||||||
token: string;
|
token: string;
|
||||||
image: string;
|
image: string;
|
||||||
@@ -26,7 +27,8 @@ const AddWorkerCommand: React.FC<ViewModalProps> = ({
|
|||||||
cacheDir,
|
cacheDir,
|
||||||
currentGPU,
|
currentGPU,
|
||||||
containerName,
|
containerName,
|
||||||
gpustackDataVolume
|
gpustackDataVolume,
|
||||||
|
dtkVersion
|
||||||
}) => {
|
}) => {
|
||||||
const code = React.useMemo(() => {
|
const code = React.useMemo(() => {
|
||||||
const commandCode = addWorkerGuide['all'];
|
const commandCode = addWorkerGuide['all'];
|
||||||
@@ -42,6 +44,7 @@ const AddWorkerCommand: React.FC<ViewModalProps> = ({
|
|||||||
cacheDir: cacheDir,
|
cacheDir: cacheDir,
|
||||||
containerName: containerName,
|
containerName: containerName,
|
||||||
gpustackDataVolume: gpustackDataVolume,
|
gpustackDataVolume: gpustackDataVolume,
|
||||||
|
dtkVersion: dtkVersion,
|
||||||
image: registrationInfo.image,
|
image: registrationInfo.image,
|
||||||
token: registrationInfo.token || '${token}'
|
token: registrationInfo.token || '${token}'
|
||||||
})
|
})
|
||||||
@@ -56,7 +59,8 @@ const AddWorkerCommand: React.FC<ViewModalProps> = ({
|
|||||||
cacheDir,
|
cacheDir,
|
||||||
containerName,
|
containerName,
|
||||||
gpustackDataVolume,
|
gpustackDataVolume,
|
||||||
advertisAddress
|
advertisAddress,
|
||||||
|
dtkVersion
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -67,7 +67,6 @@ const AddWorkerSteps: React.FC<AddWorkerProps> = (props) => {
|
|||||||
);
|
);
|
||||||
const { update, summary, register } = useSummaryStatus();
|
const { update, summary, register } = useSummaryStatus();
|
||||||
const { addedCount, createModelsChunkRequest } = useAddWorkerMessage();
|
const { addedCount, createModelsChunkRequest } = useAddWorkerMessage();
|
||||||
console.log('addedCount=========', addedCount);
|
|
||||||
|
|
||||||
const onToggle = (open: boolean, key: string) => {
|
const onToggle = (open: boolean, key: string) => {
|
||||||
setCollapseKey(open ? new Set([key]) : new Set());
|
setCollapseKey(open ? new Set([key]) : new Set());
|
||||||
@@ -82,7 +81,6 @@ const AddWorkerSteps: React.FC<AddWorkerProps> = (props) => {
|
|||||||
setCollapseKey(new Set([stepList[0]]));
|
setCollapseKey(new Set([stepList[0]]));
|
||||||
}, [stepList]);
|
}, [stepList]);
|
||||||
|
|
||||||
console.log('actionSource=========', actionSource, registrationInfo);
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
// this effect is only triggered when used in cluster create page inner
|
// this effect is only triggered when used in cluster create page inner
|
||||||
if (actionSource === 'page' && registrationInfo?.cluster_id) {
|
if (actionSource === 'page' && registrationInfo?.cluster_id) {
|
||||||
@@ -101,24 +99,30 @@ const AddWorkerSteps: React.FC<AddWorkerProps> = (props) => {
|
|||||||
// Downstream steps (check env, run command, ...) only make sense after a
|
// Downstream steps (check env, run command, ...) only make sense after a
|
||||||
// GPU vendor has been chosen. If the user toggled off every vendor in
|
// GPU vendor has been chosen. If the user toggled off every vendor in
|
||||||
// multi-select, gate them shut so the wrong panel can't be opened.
|
// multi-select, gate them shut so the wrong panel can't be opened.
|
||||||
|
// Exception: for K8s clusters, Run Command is always accessible (CPU-only
|
||||||
|
// workers don't need a GPU vendor).
|
||||||
const selectedGPUs =
|
const selectedGPUs =
|
||||||
(summary.get('selectedGPUs') as string[] | undefined) || [];
|
(summary.get('selectedGPUs') as string[] | undefined) || [];
|
||||||
const currentGPU = (summary.get('currentGPU') as string | undefined) || '';
|
const currentGPU = (summary.get('currentGPU') as string | undefined) || '';
|
||||||
const noVendorSelected = !currentGPU && selectedGPUs.length === 0;
|
const noVendorSelected = !currentGPU && selectedGPUs.length === 0;
|
||||||
const downstreamDisabled = disabled || noVendorSelected;
|
const isK8s = provider === ProviderValueMap.Kubernetes;
|
||||||
|
// For non-K8s providers, all downstream steps are gated by vendor selection.
|
||||||
|
// For K8s, both CheckEnv and RunCommand are always accessible — CheckEnv
|
||||||
|
// shows a ready-nodes check when no vendor is selected (CPU-only workers).
|
||||||
|
const downstreamDisabled = disabled || (noVendorSelected && !isK8s);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
// If the user just deselected everything, collapse any downstream
|
// If the user just deselected everything on a non-K8s provider, collapse
|
||||||
// panel back to the GPU step so they aren't left looking at a stale
|
// any downstream panel back to the GPU step so they aren't left looking at
|
||||||
// disabled-but-open command. Functional updater so we don't have to
|
// a stale disabled-but-open command. For K8s, CheckEnv stays open with a
|
||||||
// depend on `collapseKey` and re-run the effect on every toggle.
|
// ready-nodes check, so no collapsing needed.
|
||||||
if (!noVendorSelected) return;
|
if (!noVendorSelected || isK8s) return;
|
||||||
setCollapseKey((prev) =>
|
setCollapseKey((prev) =>
|
||||||
prev.has(StepNamesMap.SelectGPU)
|
prev.has(StepNamesMap.SelectGPU)
|
||||||
? prev
|
? prev
|
||||||
: new Set([StepNamesMap.SelectGPU])
|
: new Set([StepNamesMap.SelectGPU])
|
||||||
);
|
);
|
||||||
}, [noVendorSelected]);
|
}, [noVendorSelected, isK8s]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AddWorkerContext.Provider
|
<AddWorkerContext.Provider
|
||||||
@@ -147,7 +151,9 @@ const AddWorkerSteps: React.FC<AddWorkerProps> = (props) => {
|
|||||||
!stepList.includes(StepNamesMap.SelectCluster)) && (
|
!stepList.includes(StepNamesMap.SelectCluster)) && (
|
||||||
<>
|
<>
|
||||||
<SelectVendor disabled={disabled}></SelectVendor>
|
<SelectVendor disabled={disabled}></SelectVendor>
|
||||||
<CheckEnvironment disabled={downstreamDisabled}></CheckEnvironment>
|
{(isK8s || !noVendorSelected) && (
|
||||||
|
<CheckEnvironment disabled={disabled}></CheckEnvironment>
|
||||||
|
)}
|
||||||
|
|
||||||
{provider === ProviderValueMap.Kubernetes && (
|
{provider === ProviderValueMap.Kubernetes && (
|
||||||
<K8sRunCommand disabled={downstreamDisabled}></K8sRunCommand>
|
<K8sRunCommand disabled={downstreamDisabled}></K8sRunCommand>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { BulbOutlined } from '@ant-design/icons';
|
import { BulbOutlined } from '@ant-design/icons';
|
||||||
import { useIntl } from '@umijs/max';
|
import { useIntl } from '@umijs/max';
|
||||||
import { Alert, Typography } from 'antd';
|
import { Alert, Typography } from 'antd';
|
||||||
|
import { ProviderValueMap } from '../../config';
|
||||||
import CheckEnvCommand from '../check-env-command';
|
import CheckEnvCommand from '../check-env-command';
|
||||||
import { useAddWorkerContext } from './add-worker-context';
|
import { useAddWorkerContext } from './add-worker-context';
|
||||||
import { AddWorkerStepProps, StepNamesMap } from './config';
|
import { AddWorkerStepProps, StepNamesMap } from './config';
|
||||||
@@ -10,8 +11,8 @@ import StepCollapse from './step-collapse';
|
|||||||
const CheckEnvironment: React.FC<AddWorkerStepProps> = ({ disabled }) => {
|
const CheckEnvironment: React.FC<AddWorkerStepProps> = ({ disabled }) => {
|
||||||
const { stepList, summary, provider } = useAddWorkerContext();
|
const { stepList, summary, provider } = useAddWorkerContext();
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const currentGPU = summary.get('currentGPU');
|
const currentGPU = summary.get('currentGPU') as string;
|
||||||
const currentGPUs: string[] = summary.get('selectedGPUs') || [];
|
const currentGPUs: string[] = (summary.get('selectedGPUs') as string[]) || [];
|
||||||
const workerCommand = summary.get('workerCommand') || {
|
const workerCommand = summary.get('workerCommand') || {
|
||||||
label: '',
|
label: '',
|
||||||
link: '',
|
link: '',
|
||||||
@@ -19,6 +20,8 @@ const CheckEnvironment: React.FC<AddWorkerStepProps> = ({ disabled }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const stepIndex = stepList.indexOf(StepNamesMap.CheckEnv) + 1;
|
const stepIndex = stepList.indexOf(StepNamesMap.CheckEnv) + 1;
|
||||||
|
const isK8s = provider === ProviderValueMap.Kubernetes;
|
||||||
|
const isCPUOnly = isK8s && !currentGPU && currentGPUs.length === 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StepCollapse
|
<StepCollapse
|
||||||
@@ -31,27 +34,37 @@ const CheckEnvironment: React.FC<AddWorkerStepProps> = ({ disabled }) => {
|
|||||||
</Title>
|
</Title>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Alert
|
{isCPUOnly ? (
|
||||||
type="info"
|
<Typography.Paragraph style={{ marginBottom: 8 }}>
|
||||||
showIcon
|
{intl.formatMessage({
|
||||||
icon={<BulbOutlined />}
|
id: 'clusters.addworker.checkEnv.cpuOnlyTips'
|
||||||
style={{
|
})}
|
||||||
marginBottom: 8
|
</Typography.Paragraph>
|
||||||
}}
|
) : (
|
||||||
title={
|
<>
|
||||||
<span
|
<Alert
|
||||||
dangerouslySetInnerHTML={{
|
type="info"
|
||||||
__html: intl.formatMessage(
|
showIcon
|
||||||
{ id: 'clusters.create.addworker.tips' },
|
icon={<BulbOutlined />}
|
||||||
{ label: workerCommand.label, link: workerCommand.link }
|
style={{
|
||||||
)
|
marginBottom: 8
|
||||||
}}
|
}}
|
||||||
></span>
|
title={
|
||||||
}
|
<span
|
||||||
></Alert>
|
dangerouslySetInnerHTML={{
|
||||||
<Typography.Paragraph style={{ marginBottom: 8 }}>
|
__html: intl.formatMessage(
|
||||||
{intl.formatMessage({ id: 'cluster.create.checkEnv.tips' })}
|
{ id: 'clusters.create.addworker.tips' },
|
||||||
</Typography.Paragraph>
|
{ label: workerCommand.label, link: workerCommand.link }
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
></span>
|
||||||
|
}
|
||||||
|
></Alert>
|
||||||
|
<Typography.Paragraph style={{ marginBottom: 8 }}>
|
||||||
|
{intl.formatMessage({ id: 'cluster.create.checkEnv.tips' })}
|
||||||
|
</Typography.Paragraph>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
<CheckEnvCommand
|
<CheckEnvCommand
|
||||||
provider={provider}
|
provider={provider}
|
||||||
currentGPU={currentGPU}
|
currentGPU={currentGPU}
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ export interface SummaryDataKeys {
|
|||||||
selectedGPUs: string[];
|
selectedGPUs: string[];
|
||||||
cluster_id: number;
|
cluster_id: number;
|
||||||
clusterName: string;
|
clusterName: string;
|
||||||
|
dtkVersion: string;
|
||||||
workerCommand: {
|
workerCommand: {
|
||||||
label: string;
|
label: string;
|
||||||
link: string;
|
link: string;
|
||||||
|
|||||||
@@ -41,6 +41,8 @@ const DockerRunCommand: React.FC<AddWorkerStepProps> = ({ disabled }) => {
|
|||||||
|
|
||||||
const currentGPU = summary.get('currentGPU') || '';
|
const currentGPU = summary.get('currentGPU') || '';
|
||||||
|
|
||||||
|
const dtkVersion = summary.get('dtkVersion') || '25.04';
|
||||||
|
|
||||||
const stepIndex = stepList.indexOf(StepNamesMap.RunCommand) + 1;
|
const stepIndex = stepList.indexOf(StepNamesMap.RunCommand) + 1;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -80,6 +82,7 @@ const DockerRunCommand: React.FC<AddWorkerStepProps> = ({ disabled }) => {
|
|||||||
gpustackDataVolumeConfig.enable ? gpustackDataVolumeConfig.path : ''
|
gpustackDataVolumeConfig.enable ? gpustackDataVolumeConfig.path : ''
|
||||||
}
|
}
|
||||||
currentGPU={currentGPU}
|
currentGPU={currentGPU}
|
||||||
|
dtkVersion={dtkVersion}
|
||||||
/>
|
/>
|
||||||
</StepCollapse>
|
</StepCollapse>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import {
|
|||||||
GPUsConfigs
|
GPUsConfigs
|
||||||
} from '@/pages/resources/config/gpu-driver';
|
} from '@/pages/resources/config/gpu-driver';
|
||||||
import { useIntl } from '@umijs/max';
|
import { useIntl } from '@umijs/max';
|
||||||
import { Tag } from 'antd';
|
|
||||||
import React, { useEffect, useRef, useState } from 'react';
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
import { ProviderValueMap } from '../../config';
|
import { ProviderValueMap } from '../../config';
|
||||||
import SupportedGPUs from '../support-gpus';
|
import SupportedGPUs from '../support-gpus';
|
||||||
@@ -94,7 +93,8 @@ const SelectVendor: React.FC<AddWorkerStepProps> = ({ disabled }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// init a default selection
|
// Default to NVIDIA for every provider, including K8s. Users can still
|
||||||
|
// deselect it (e.g. for CPU-only workers) or add more vendors on K8s.
|
||||||
handleSelect(GPUDriverMap.NVIDIA, {
|
handleSelect(GPUDriverMap.NVIDIA, {
|
||||||
label: 'NVIDIA',
|
label: 'NVIDIA',
|
||||||
hiddenTitle: true,
|
hiddenTitle: true,
|
||||||
@@ -116,18 +116,18 @@ const SelectVendor: React.FC<AddWorkerStepProps> = ({ disabled }) => {
|
|||||||
{stepIndex}.{' '}
|
{stepIndex}.{' '}
|
||||||
{intl.formatMessage({ id: 'clusters.addworker.selectGPU' })}
|
{intl.formatMessage({ id: 'clusters.addworker.selectGPU' })}
|
||||||
{multiCapable && (
|
{multiCapable && (
|
||||||
<Tag
|
<span
|
||||||
color="blue"
|
|
||||||
style={{
|
style={{
|
||||||
marginLeft: 8,
|
marginLeft: 8,
|
||||||
fontWeight: 400,
|
fontWeight: 400,
|
||||||
borderRadius: 4
|
fontSize: 13,
|
||||||
|
color: 'var(--ant-color-text-secondary)'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{intl.formatMessage({
|
{intl.formatMessage({
|
||||||
id: 'clusters.addworker.selectGPU.multiTag'
|
id: 'clusters.addworker.selectGPU.subtitle'
|
||||||
})}
|
})}
|
||||||
</Tag>
|
</span>
|
||||||
)}
|
)}
|
||||||
</Title>
|
</Title>
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user