Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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,19 @@ 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',
|
||||||
|
// 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)
|
||||||
|
|||||||
@@ -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.
@@ -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) => {
|
||||||
|
|||||||
@@ -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.',
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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.',
|
||||||
|
|||||||
@@ -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': 'イベント',
|
||||||
|
|||||||
@@ -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':
|
||||||
'Укажите имя для контейнера воркера.',
|
'Укажите имя для контейнера воркера.',
|
||||||
|
|||||||
@@ -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': 'Событие',
|
||||||
|
|||||||
@@ -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.',
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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 数据卷',
|
||||||
|
|||||||
@@ -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': '事件',
|
||||||
|
|||||||
@@ -21,6 +21,13 @@ export interface BarChartProps {
|
|||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,6 +41,7 @@ const BarChart: React.FC<BarChartProps> = (props) => {
|
|||||||
labelFormatter,
|
labelFormatter,
|
||||||
tooltipValueFormatter,
|
tooltipValueFormatter,
|
||||||
title,
|
title,
|
||||||
|
grid,
|
||||||
legendIsolate
|
legendIsolate
|
||||||
} = props;
|
} = props;
|
||||||
const { token } = theme.useToken();
|
const { token } = theme.useToken();
|
||||||
@@ -126,8 +134,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',
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)' }}
|
||||||
|
|||||||
@@ -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' }}>
|
||||||
|
<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;
|
||||||
@@ -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>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
|
import { GPUDriverMap } from '@/pages/resources/config/gpu-driver';
|
||||||
import { ExclamationCircleFilled } from '@ant-design/icons';
|
import { ExclamationCircleFilled } from '@ant-design/icons';
|
||||||
import { AlertBlockInfo as AlertInfoBlock } from '@gpustack/core-ui';
|
import {
|
||||||
|
AlertBlockInfo as AlertInfoBlock,
|
||||||
|
SwitchCard
|
||||||
|
} from '@gpustack/core-ui';
|
||||||
import { useIntl } from '@umijs/max';
|
import { useIntl } from '@umijs/max';
|
||||||
import { Input, Switch, Typography } from 'antd';
|
import { Input, Radio, Switch, Typography } from 'antd';
|
||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import { useAddWorkerContext } from './add-worker-context';
|
import { useAddWorkerContext } from './add-worker-context';
|
||||||
@@ -111,6 +115,14 @@ const SpecifyArguments: React.FC<AddWorkerStepProps> = ({ disabled }) => {
|
|||||||
path: 'gpustack-data'
|
path: 'gpustack-data'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const selectedGPUs = summary.get('selectedGPUs') || [];
|
||||||
|
|
||||||
|
const dtkVersion = summary.get('dtkVersion') || '25.04';
|
||||||
|
|
||||||
|
// The DTK version selector only applies to Hygon DCUs, so only surface it
|
||||||
|
// when Hygon ('dtk') is among the selected vendors.
|
||||||
|
const showDtkVersion = selectedGPUs.includes(GPUDriverMap.HYGON);
|
||||||
|
|
||||||
const setWorkerIPConfig = (config: {
|
const setWorkerIPConfig = (config: {
|
||||||
enable: boolean;
|
enable: boolean;
|
||||||
ip?: string;
|
ip?: string;
|
||||||
@@ -166,12 +178,14 @@ const SpecifyArguments: React.FC<AddWorkerStepProps> = ({ disabled }) => {
|
|||||||
const unregisterGpustackDataVolume = registerField(
|
const unregisterGpustackDataVolume = registerField(
|
||||||
'gpustackDataVolumeConfig'
|
'gpustackDataVolumeConfig'
|
||||||
);
|
);
|
||||||
|
const unregisterVersion = registerField('dtkVersion');
|
||||||
return () => {
|
return () => {
|
||||||
unregisterWorkerIP();
|
unregisterWorkerIP();
|
||||||
unregisterModelDir();
|
unregisterModelDir();
|
||||||
unregisterCacheDir();
|
unregisterCacheDir();
|
||||||
unregisterContainerName();
|
unregisterContainerName();
|
||||||
unregisterGpustackDataVolume();
|
unregisterGpustackDataVolume();
|
||||||
|
unregisterVersion();
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -207,6 +221,7 @@ const SpecifyArguments: React.FC<AddWorkerStepProps> = ({ disabled }) => {
|
|||||||
ip: '',
|
ip: '',
|
||||||
required: false
|
required: false
|
||||||
});
|
});
|
||||||
|
updateField('dtkVersion', '25.04');
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -229,6 +244,30 @@ const SpecifyArguments: React.FC<AddWorkerStepProps> = ({ disabled }) => {
|
|||||||
marginBottom: 8
|
marginBottom: 8
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
{showDtkVersion && (
|
||||||
|
<SwitchCard
|
||||||
|
label={intl.formatMessage({
|
||||||
|
id: 'clusters.addworker.dtkVersion'
|
||||||
|
})}
|
||||||
|
defaultValue={true}
|
||||||
|
showSwitch={false}
|
||||||
|
styles={{
|
||||||
|
wrapper: {
|
||||||
|
borderRadius: 4
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Radio.Group
|
||||||
|
defaultValue={dtkVersion}
|
||||||
|
value={dtkVersion}
|
||||||
|
onChange={(e) => updateField('dtkVersion', e.target.value)}
|
||||||
|
style={{ marginTop: 8, display: 'flex', gap: 12 }}
|
||||||
|
>
|
||||||
|
<Radio value="25.04">25.04</Radio>
|
||||||
|
<Radio value="26.04">26.04</Radio>
|
||||||
|
</Radio.Group>
|
||||||
|
</SwitchCard>
|
||||||
|
)}
|
||||||
{/* worker IP config */}
|
{/* worker IP config */}
|
||||||
<SwitchSetting
|
<SwitchSetting
|
||||||
label={
|
label={
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
import { addWorkerGuide } from '@/pages/resources/config';
|
import { addWorkerGuide } from '@/pages/resources/config';
|
||||||
import { HighlightCode } from '@gpustack/core-ui';
|
import { HighlightCode } from '@gpustack/core-ui';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { ProviderType } from '../config';
|
import { ProviderType, ProviderValueMap } from '../config';
|
||||||
|
|
||||||
|
// CPU-only (no GPU vendor selected) check for Kubernetes: verify at least one
|
||||||
|
// ready node exists in the target cluster.
|
||||||
|
const K8S_CPU_ONLY_CHECK =
|
||||||
|
'kubectl get nodes -o jsonpath=\'{.items[*].status.conditions[?(@.type=="Ready")].status}\' 2>/dev/null | grep -q "True" && echo "Ready nodes found. You are registering a CPU-only cluster." || (echo "No ready nodes found"; exit 1)';
|
||||||
|
|
||||||
type ViewModalProps = {
|
type ViewModalProps = {
|
||||||
provider: ProviderType;
|
provider: ProviderType;
|
||||||
@@ -25,6 +30,12 @@ const AddWorkerCommand: React.FC<ViewModalProps> = ({
|
|||||||
: currentGPU
|
: currentGPU
|
||||||
? [currentGPU]
|
? [currentGPU]
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
|
// CPU-only K8s flow: no GPU vendor selected, check for ready nodes instead.
|
||||||
|
if (!keys.length && provider === ProviderValueMap.Kubernetes) {
|
||||||
|
return K8S_CPU_ONLY_CHECK;
|
||||||
|
}
|
||||||
|
|
||||||
if (!keys.length) return '';
|
if (!keys.length) return '';
|
||||||
const lines = keys
|
const lines = keys
|
||||||
.map((k) => configs.checkEnvCommand(k)?.[provider || ''])
|
.map((k) => configs.checkEnvCommand(k)?.[provider || ''])
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ type AddModalProps = {
|
|||||||
provider: ProviderType;
|
provider: ProviderType;
|
||||||
credentialList: Global.BaseOption<number>[];
|
credentialList: Global.BaseOption<number>[];
|
||||||
onFinish: (values: FormData) => void;
|
onFinish: (values: FormData) => void;
|
||||||
|
onFinishFailed?: (errorInfo: any) => void;
|
||||||
// Reports whether the user has changed any k8s_options field, so the parent
|
// Reports whether the user has changed any k8s_options field, so the parent
|
||||||
// can show the "re-run registration" notice in the footer.
|
// can show the "re-run registration" notice in the footer.
|
||||||
onK8sOptionsChange?: (changed: boolean) => void;
|
onK8sOptionsChange?: (changed: boolean) => void;
|
||||||
@@ -49,6 +50,7 @@ const ClusterForm: React.FC<AddModalProps> = forwardRef(
|
|||||||
currentData,
|
currentData,
|
||||||
credentialList,
|
credentialList,
|
||||||
onFinish,
|
onFinish,
|
||||||
|
onFinishFailed,
|
||||||
onK8sOptionsChange
|
onK8sOptionsChange
|
||||||
},
|
},
|
||||||
ref
|
ref
|
||||||
@@ -212,8 +214,9 @@ const ClusterForm: React.FC<AddModalProps> = forwardRef(
|
|||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const handleOnFinishFailed = () => {
|
const handleOnFinishFailed = (errorInfo: any) => {
|
||||||
setSubmitAttempted(true);
|
setSubmitAttempted(true);
|
||||||
|
onFinishFailed?.(errorInfo);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -260,10 +263,6 @@ const ClusterForm: React.FC<AddModalProps> = forwardRef(
|
|||||||
<Form.Item<FormData>
|
<Form.Item<FormData>
|
||||||
name="description"
|
name="description"
|
||||||
rules={[{ required: false }]}
|
rules={[{ required: false }]}
|
||||||
// For Kubernetes the cluster type selector follows directly, so
|
|
||||||
// fall back to the default item margin (matching the name field)
|
|
||||||
// to keep the description spacing symmetric; other providers keep
|
|
||||||
// the tighter gap before the advanced panel.
|
|
||||||
style={{
|
style={{
|
||||||
marginBottom:
|
marginBottom:
|
||||||
provider === ProviderValueMap.Kubernetes ? undefined : 8
|
provider === ProviderValueMap.Kubernetes ? undefined : 8
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { Input as CInput, LabelSelector } from '@gpustack/core-ui';
|
|||||||
import { useIntl } from '@umijs/max';
|
import { useIntl } from '@umijs/max';
|
||||||
import { Form } from 'antd';
|
import { Form } from 'antd';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import React, { useEffect, useId } from 'react';
|
import React, { useEffect, useId, useMemo } from 'react';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import { ClusterListItem as ListItem } from '../config/types';
|
import { ClusterListItem as ListItem } from '../config/types';
|
||||||
import ImageCredential from './image-credential';
|
import ImageCredential from './image-credential';
|
||||||
@@ -56,7 +56,6 @@ export const OperatorImageForm: React.FC = () => {
|
|||||||
<SectionWrap>
|
<SectionWrap>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name={['k8s_options', 'operatorImage']}
|
name={['k8s_options', 'operatorImage']}
|
||||||
style={{ marginBottom: 0 }}
|
|
||||||
normalize={(value) => value || null}
|
normalize={(value) => value || null}
|
||||||
>
|
>
|
||||||
<CInput.Input
|
<CInput.Input
|
||||||
@@ -289,10 +288,9 @@ export const GpuInstancesStaticAddressForm: React.FC = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Strip UI-only / undefined-valued noise so two k8s_options snapshots compare
|
// Strip UI-only noise so two k8s_options snapshots compare on real content.
|
||||||
// on real content. `sourceType` is derived from `volumeSource` purely for the
|
// `sourceType` is derived from `volumeSource` purely for the volume-mount UI
|
||||||
// volume-mount UI (see cluster-form init), and the JSON round-trip drops
|
// (see cluster-form init).
|
||||||
// undefined-valued keys so a missing key and `key: undefined` compare equal.
|
|
||||||
const cleanK8sOptions = (opts: any) => {
|
const cleanK8sOptions = (opts: any) => {
|
||||||
const cloned = _.cloneDeep(opts || {});
|
const cloned = _.cloneDeep(opts || {});
|
||||||
if (Array.isArray(cloned.volumeMounts)) {
|
if (Array.isArray(cloned.volumeMounts)) {
|
||||||
@@ -300,24 +298,33 @@ const cleanK8sOptions = (opts: any) => {
|
|||||||
({ sourceType, ...rest }: any) => rest
|
({ sourceType, ...rest }: any) => rest
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
// The edit form always seeds k8s_options.volumeMounts to [] even when the
|
return _.pickBy(cloned, (value: any) => !_.isNil(value));
|
||||||
// saved cluster had no value, so an absent field would otherwise read as a
|
};
|
||||||
// change the moment the drawer opens. Drop empty top-level arrays on both
|
|
||||||
// sides: "absent" and "empty list" both mean nothing configured. A
|
// Custom comparator for isEqualWith: treats null, undefined, empty strings,
|
||||||
// non-empty -> empty edit is still detected, since only the empty side drops.
|
// empty arrays, and empty objects as equivalent. Form normalize converts empty
|
||||||
Object.keys(cloned).forEach((key) => {
|
// inputs to null while the API may omit absent keys (read as undefined when
|
||||||
if (Array.isArray(cloned[key]) && cloned[key].length === 0) {
|
// accessed on the object). This lets "user clears field back to original"
|
||||||
delete cloned[key];
|
// compare as unchanged without needing to recursively strip nullish values.
|
||||||
}
|
const nullishCustomizer = (val1: any, val2: any) => {
|
||||||
});
|
if (
|
||||||
return JSON.parse(JSON.stringify(cloned));
|
(val1 == null && val2 === '') ||
|
||||||
|
(val1 === '' && val2 == null) ||
|
||||||
|
(_.isEmpty(val1) && val2 == null) ||
|
||||||
|
(val1 == null && _.isEmpty(val2)) ||
|
||||||
|
(_.isNil(val1) && _.isNil(val2))
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Headless watcher: in EDIT mode it reports (via onChange) whether the user has
|
// Headless watcher: in EDIT mode it reports (via onChange) whether the user has
|
||||||
// changed any k8s_options field from the cluster's saved values. It renders
|
// changed any k8s_options field or the top-level system_default_container_registry
|
||||||
// nothing — the notice itself is shown in the form footer, above Save/Cancel
|
// from the cluster's saved values. It renders nothing — the notice itself is shown
|
||||||
// (see cluster-create.tsx), mirroring the model edit interaction. Must be
|
// in the form footer, above Save/Cancel (see cluster-create.tsx), mirroring the
|
||||||
// mounted inside the cluster <Form> so the watch reads the form store.
|
// model edit interaction. Must be mounted inside the cluster <Form> so the watch
|
||||||
|
// reads the form store.
|
||||||
export const K8sOptionsChangeWatcher: React.FC<{
|
export const K8sOptionsChangeWatcher: React.FC<{
|
||||||
action: PageActionType;
|
action: PageActionType;
|
||||||
currentData?: ListItem;
|
currentData?: ListItem;
|
||||||
@@ -327,13 +334,31 @@ export const K8sOptionsChangeWatcher: React.FC<{
|
|||||||
// gpuInstanceOptions which is toggled via setFieldValue without a mounted
|
// gpuInstanceOptions which is toggled via setFieldValue without a mounted
|
||||||
// Form.Item (mirrors ClusterTypeSelector).
|
// Form.Item (mirrors ClusterTypeSelector).
|
||||||
const k8sOptions = Form.useWatch(['k8s_options'], { preserve: true });
|
const k8sOptions = Form.useWatch(['k8s_options'], { preserve: true });
|
||||||
|
const containerRegistry = Form.useWatch('system_default_container_registry', {
|
||||||
|
preserve: true
|
||||||
|
});
|
||||||
|
|
||||||
|
// currentData?.k8s_options is static for the form's lifetime — memoize the
|
||||||
|
// cleaned version to avoid redundant deep-clone on every render
|
||||||
|
// (Form.useWatch triggers re-render on each keystroke).
|
||||||
|
const currentK8sOptionsCleaned = useMemo(
|
||||||
|
() => cleanK8sOptions(currentData?.k8s_options),
|
||||||
|
[currentData?.k8s_options]
|
||||||
|
);
|
||||||
|
|
||||||
|
const k8sOptionsChanged = !_.isEqualWith(
|
||||||
|
currentK8sOptionsCleaned,
|
||||||
|
cleanK8sOptions(k8sOptions),
|
||||||
|
nullishCustomizer
|
||||||
|
);
|
||||||
|
const registryChanged = !_.isEqualWith(
|
||||||
|
currentData?.system_default_container_registry,
|
||||||
|
containerRegistry,
|
||||||
|
nullishCustomizer
|
||||||
|
);
|
||||||
|
|
||||||
const changed =
|
const changed =
|
||||||
action === PageAction.EDIT &&
|
action === PageAction.EDIT && (k8sOptionsChanged || registryChanged);
|
||||||
!_.isEqual(
|
|
||||||
cleanK8sOptions(currentData?.k8s_options),
|
|
||||||
cleanK8sOptions(k8sOptions)
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
onChange(changed);
|
onChange(changed);
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { CheckOutlined } from '@ant-design/icons';
|
||||||
import { IconFont, TemplateCard } from '@gpustack/core-ui';
|
import { IconFont, TemplateCard } from '@gpustack/core-ui';
|
||||||
import { useIntl } from '@umijs/max';
|
import { useIntl } from '@umijs/max';
|
||||||
import { Tooltip } from 'antd';
|
import { Tooltip } from 'antd';
|
||||||
@@ -62,6 +63,29 @@ const Header = styled.div`
|
|||||||
const CardBox = styled.div`
|
const CardBox = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const ActiveBadge = styled.div`
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
background-color: var(--ant-color-primary);
|
||||||
|
clip-path: path('M 20 0 L 20 15 Q 20 20 15 20 L 0 20 Z');
|
||||||
|
z-index: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
justify-content: flex-end;
|
||||||
|
pointer-events: none;
|
||||||
|
|
||||||
|
.anticon {
|
||||||
|
color: #fff;
|
||||||
|
font-size: 10px;
|
||||||
|
margin-right: 2px;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
interface ProviderCatalogProps {
|
interface ProviderCatalogProps {
|
||||||
@@ -160,6 +184,13 @@ const ProviderCatalog: React.FC<ProviderCatalogProps> = ({
|
|||||||
header={renderTitle(action)}
|
header={renderTitle(action)}
|
||||||
icon={action.icon}
|
icon={action.icon}
|
||||||
></TemplateCard>
|
></TemplateCard>
|
||||||
|
{(Array.isArray(current)
|
||||||
|
? current.includes(action.key)
|
||||||
|
: current === action.key) && (
|
||||||
|
<ActiveBadge>
|
||||||
|
<CheckOutlined />
|
||||||
|
</ActiveBadge>
|
||||||
|
)}
|
||||||
</CardBox>
|
</CardBox>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ const Box = styled.div`
|
|||||||
background-color: rgba(255, 255, 255, 0.1);
|
background-color: rgba(255, 255, 255, 0.1);
|
||||||
&.active {
|
&.active {
|
||||||
background-color: var(--ant-color-fill-tertiary);
|
background-color: var(--ant-color-fill-tertiary);
|
||||||
|
border-color: var(--ant-color-primary);
|
||||||
}
|
}
|
||||||
.template-card-icon {
|
.template-card-icon {
|
||||||
margin-right: 0;
|
margin-right: 0;
|
||||||
@@ -57,6 +58,7 @@ const Box = styled.div`
|
|||||||
}
|
}
|
||||||
&.active {
|
&.active {
|
||||||
background-color: var(--ant-color-bg-solid);
|
background-color: var(--ant-color-bg-solid);
|
||||||
|
border-color: var(--ant-color-primary);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ export interface CredentialListItem {
|
|||||||
access_key: string;
|
access_key: string;
|
||||||
secret_key: string;
|
secret_key: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
|
owner_principal_id?: number | null;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import { systemConfigAtom } from '@/atoms/system';
|
import { systemConfigAtom } from '@/atoms/system';
|
||||||
import { tableSorter } from '@/config/settings';
|
import { tableSorter } from '@/config/settings';
|
||||||
import { getGPUStackPlugin } from '@/plugins';
|
import { getGPUStackPlugin } from '@/plugins';
|
||||||
|
import { usePluginListColumns } from '@/plugins/list-extra-columns';
|
||||||
import { StarFilled } from '@ant-design/icons';
|
import { StarFilled } from '@ant-design/icons';
|
||||||
import {
|
import {
|
||||||
AutoTooltip,
|
AutoTooltip,
|
||||||
@@ -80,6 +81,7 @@ const useClusterColumns = (
|
|||||||
): SealColumnProps[] => {
|
): SealColumnProps[] => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const systemConfig = useAtomValue(systemConfigAtom);
|
const systemConfig = useAtomValue(systemConfigAtom);
|
||||||
|
const pluginCols = usePluginListColumns('clusters');
|
||||||
// The cluster-detail page is shipped in OSS source, but OSS keeps
|
// The cluster-detail page is shipped in OSS source, but OSS keeps
|
||||||
// it unreachable from the cluster list — the link is only
|
// it unreachable from the cluster list — the link is only
|
||||||
// surfaced when a plugin opts in via
|
// surfaced when a plugin opts in via
|
||||||
@@ -102,6 +104,40 @@ const useClusterColumns = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
return useMemo(() => {
|
return useMemo(() => {
|
||||||
|
// Two prebuilt span maps for the 24-unit SealTable grid: one for
|
||||||
|
// the default layout, one for when a plugin contributes an extra
|
||||||
|
// column (currently always the 3-span Organization cell — wider
|
||||||
|
// would visually dominate this row of 2/3-span built-ins). Width
|
||||||
|
// absorbed comes from the columns whose content underfills its
|
||||||
|
// slot (single-digit `provider` / `models` / `status` tag), not
|
||||||
|
// from `created_at` (a full date string) or `workers` (the
|
||||||
|
// `x / y` digit pair reads better with breathing room). Picking
|
||||||
|
// by map rather than a `pluginSpan`-driven formula trades the
|
||||||
|
// formula's built-in handling of multi-column / variable-width
|
||||||
|
// plugins for readability and easier tweaks — the sole consumer
|
||||||
|
// today is one fixed-width column, so the formula's generality
|
||||||
|
// was unused.
|
||||||
|
const SPANS_DEFAULT = {
|
||||||
|
provider: 3,
|
||||||
|
deployments: 3,
|
||||||
|
workers: 3,
|
||||||
|
status: 3,
|
||||||
|
createTime: 4
|
||||||
|
};
|
||||||
|
const SPANS_WITH_PLUGIN = {
|
||||||
|
provider: 2,
|
||||||
|
deployments: 2,
|
||||||
|
workers: 3,
|
||||||
|
status: 2,
|
||||||
|
createTime: 4
|
||||||
|
};
|
||||||
|
const spans = pluginCols.length > 0 ? SPANS_WITH_PLUGIN : SPANS_DEFAULT;
|
||||||
|
const pluginRendered = pluginCols.map((c) => ({
|
||||||
|
title: intl.formatMessage({ id: c.titleId }),
|
||||||
|
dataIndex: c.key,
|
||||||
|
span: c.span ?? 4,
|
||||||
|
render: (_value: any, record: ClusterListItem) => c.render(record)
|
||||||
|
}));
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
title: intl.formatMessage({ id: 'common.table.name' }),
|
title: intl.formatMessage({ id: 'common.table.name' }),
|
||||||
@@ -133,11 +169,12 @@ const useClusterColumns = (
|
|||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
...pluginRendered,
|
||||||
{
|
{
|
||||||
title: intl.formatMessage({ id: 'clusters.table.provider' }),
|
title: intl.formatMessage({ id: 'clusters.table.provider' }),
|
||||||
dataIndex: 'provider',
|
dataIndex: 'provider',
|
||||||
sorter: tableSorter(2),
|
sorter: tableSorter(2),
|
||||||
span: 3,
|
span: spans.provider,
|
||||||
render: (value: string) => (
|
render: (value: string) => (
|
||||||
<AutoTooltip ghost minWidth={20}>
|
<AutoTooltip ghost minWidth={20}>
|
||||||
{ProviderLabelMap[value]}
|
{ProviderLabelMap[value]}
|
||||||
@@ -145,7 +182,7 @@ const useClusterColumns = (
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'GPUs',
|
title: intl.formatMessage({ id: 'menu.resources.gpus' }),
|
||||||
dataIndex: 'gpus',
|
dataIndex: 'gpus',
|
||||||
span: 2,
|
span: 2,
|
||||||
sorter: tableSorter(3),
|
sorter: tableSorter(3),
|
||||||
@@ -155,14 +192,14 @@ const useClusterColumns = (
|
|||||||
title: intl.formatMessage({ id: 'clusters.table.deployments' }),
|
title: intl.formatMessage({ id: 'clusters.table.deployments' }),
|
||||||
dataIndex: 'models',
|
dataIndex: 'models',
|
||||||
sorter: tableSorter(4),
|
sorter: tableSorter(4),
|
||||||
span: 3,
|
span: spans.deployments,
|
||||||
render: (value: number) => <span>{value}</span>
|
render: (value: number) => <span>{value}</span>
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: intl.formatMessage({ id: 'resources.nodes' }),
|
title: intl.formatMessage({ id: 'resources.nodes' }),
|
||||||
dataIndex: 'workers',
|
dataIndex: 'workers',
|
||||||
sorter: tableSorter(5),
|
sorter: tableSorter(5),
|
||||||
span: 3,
|
span: spans.workers,
|
||||||
render: (value: number, record: ClusterListItem) => (
|
render: (value: number, record: ClusterListItem) => (
|
||||||
<span>
|
<span>
|
||||||
{record.ready_workers} / {record.workers}
|
{record.ready_workers} / {record.workers}
|
||||||
@@ -172,7 +209,7 @@ const useClusterColumns = (
|
|||||||
{
|
{
|
||||||
title: intl.formatMessage({ id: 'common.table.status' }),
|
title: intl.formatMessage({ id: 'common.table.status' }),
|
||||||
dataIndex: 'state',
|
dataIndex: 'state',
|
||||||
span: 3,
|
span: spans.status,
|
||||||
render: (value: number, record: ClusterListItem) => (
|
render: (value: number, record: ClusterListItem) => (
|
||||||
<StatusTag
|
<StatusTag
|
||||||
statusValue={{
|
statusValue={{
|
||||||
@@ -187,7 +224,7 @@ const useClusterColumns = (
|
|||||||
title: intl.formatMessage({ id: 'common.table.createTime' }),
|
title: intl.formatMessage({ id: 'common.table.createTime' }),
|
||||||
dataIndex: 'created_at',
|
dataIndex: 'created_at',
|
||||||
sorter: tableSorter(6),
|
sorter: tableSorter(6),
|
||||||
span: 4,
|
span: spans.createTime,
|
||||||
render: (value: string) => (
|
render: (value: string) => (
|
||||||
<AutoTooltip ghost minWidth={20}>
|
<AutoTooltip ghost minWidth={20}>
|
||||||
{dayjs(value).format('YYYY-MM-DD HH:mm:ss')}
|
{dayjs(value).format('YYYY-MM-DD HH:mm:ss')}
|
||||||
@@ -206,7 +243,7 @@ const useClusterColumns = (
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
}, [handleSelect, onCellClick]);
|
}, [handleSelect, onCellClick, intl, pluginCols]);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default useClusterColumns;
|
export default useClusterColumns;
|
||||||
|
|||||||
@@ -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, DropdownButtons } from '@gpustack/core-ui';
|
import { AutoTooltip, DropdownButtons } from '@gpustack/core-ui';
|
||||||
import { useIntl } from '@umijs/max';
|
import { useIntl } from '@umijs/max';
|
||||||
import { ColumnsType } from 'antd/es/table';
|
import { ColumnsType } from 'antd/es/table';
|
||||||
@@ -12,8 +13,15 @@ const useCredentialColumns = (
|
|||||||
handleSelect: (val: string, record: ListItem) => void
|
handleSelect: (val: string, record: ListItem) => void
|
||||||
): ColumnsType<ListItem> => {
|
): ColumnsType<ListItem> => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
const pluginCols = usePluginListColumns('cloudCredentials');
|
||||||
|
|
||||||
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' }),
|
||||||
@@ -25,6 +33,7 @@ const useCredentialColumns = (
|
|||||||
</AutoTooltip>
|
</AutoTooltip>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
...pluginRendered,
|
||||||
{
|
{
|
||||||
title: intl.formatMessage({ id: 'clusters.table.provider' }),
|
title: intl.formatMessage({ id: 'clusters.table.provider' }),
|
||||||
dataIndex: 'provider',
|
dataIndex: 'provider',
|
||||||
@@ -68,7 +77,7 @@ const useCredentialColumns = (
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
}, [intl, handleSelect]);
|
}, [intl, handleSelect, pluginCols]);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default useCredentialColumns;
|
export default useCredentialColumns;
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import { PageAction } from '@/config';
|
import { PageAction } from '@/config';
|
||||||
import { PageActionType } from '@/config/types';
|
import { PageActionType } from '@/config/types';
|
||||||
import useUserSettings from '@/hooks/use-user-settings';
|
import useUserSettings from '@/hooks/use-user-settings';
|
||||||
import { ExclamationCircleFilled } from '@ant-design/icons';
|
import { Input as CInput, IconFont } from '@gpustack/core-ui';
|
||||||
import { AlertBlockInfo, Input as CInput, IconFont } from '@gpustack/core-ui';
|
|
||||||
import { YamlEditor } from '@gpustack/core-ui/yaml-editor';
|
import { YamlEditor } from '@gpustack/core-ui/yaml-editor';
|
||||||
import { useIntl } from '@umijs/max';
|
import { useIntl } from '@umijs/max';
|
||||||
import { Button, Form } from 'antd';
|
import { Button, Form } from 'antd';
|
||||||
@@ -98,7 +97,6 @@ const ClusterAdvanceConfig: React.FC<{
|
|||||||
this column). */}
|
this column). */}
|
||||||
<Form.Item<FormData>
|
<Form.Item<FormData>
|
||||||
name="system_default_container_registry"
|
name="system_default_container_registry"
|
||||||
style={{ marginBottom: 16 }}
|
|
||||||
normalize={(value) => value?.trim?.() || null}
|
normalize={(value) => value?.trim?.() || null}
|
||||||
>
|
>
|
||||||
<CInput.Input
|
<CInput.Input
|
||||||
@@ -120,14 +118,6 @@ const ClusterAdvanceConfig: React.FC<{
|
|||||||
<Title>
|
<Title>
|
||||||
{intl.formatMessage({ id: 'clusters.create.workerConfig' })}
|
{intl.formatMessage({ id: 'clusters.create.workerConfig' })}
|
||||||
</Title>
|
</Title>
|
||||||
{action === PageAction.EDIT && (
|
|
||||||
<AlertBlockInfo
|
|
||||||
type="warning"
|
|
||||||
style={{ marginBottom: 8 }}
|
|
||||||
icon={<ExclamationCircleFilled />}
|
|
||||||
message={intl.formatMessage({ id: 'clusters.edit.workerConfig.tip' })}
|
|
||||||
></AlertBlockInfo>
|
|
||||||
)}
|
|
||||||
<YamlEditor
|
<YamlEditor
|
||||||
ref={editorRef}
|
ref={editorRef}
|
||||||
isDarkTheme={isDarkTheme}
|
isDarkTheme={isDarkTheme}
|
||||||
|
|||||||
@@ -163,10 +163,6 @@ const UsageByModel: React.FC<UsageByModelProps> = ({
|
|||||||
[apiData, pickedNames, othersLabel]
|
[apiData, pickedNames, othersLabel]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Build a shared name->color map from the projected data so legend toggles
|
|
||||||
// both donuts together and matching slices read the same color. Others
|
|
||||||
// sticks to the end and uses a muted token so it visually reads as "the
|
|
||||||
// long tail" rather than a real route.
|
|
||||||
const { legendNames, colorMap } = useMemo(() => {
|
const { legendNames, colorMap } = useMemo(() => {
|
||||||
const names: string[] = [];
|
const names: string[] = [];
|
||||||
const seen = new Set<string>();
|
const seen = new Set<string>();
|
||||||
@@ -231,15 +227,6 @@ const UsageByModel: React.FC<UsageByModelProps> = ({
|
|||||||
},
|
},
|
||||||
legend: {
|
legend: {
|
||||||
orient: 'vertical',
|
orient: 'vertical',
|
||||||
// Anchor the legend column's LEFT edge at 85% of the chart canvas
|
|
||||||
// (right edge fills to canvas right). ECharts' legend.align only
|
|
||||||
// controls intra-item marker/text ordering — not item-in-container
|
|
||||||
// alignment — so a right-anchored + fixed-width legend always
|
|
||||||
// packs items flush right. Anchoring from the left instead makes
|
|
||||||
// items naturally start at the container's left edge, so a short
|
|
||||||
// label like `qwen3-0.6b` stays close to the donut instead of
|
|
||||||
// floating in a void at the canvas edge.
|
|
||||||
left: '85%',
|
|
||||||
right: 0,
|
right: 0,
|
||||||
top: 18,
|
top: 18,
|
||||||
bottom: 18,
|
bottom: 18,
|
||||||
@@ -249,12 +236,16 @@ const UsageByModel: React.FC<UsageByModelProps> = ({
|
|||||||
textStyle: {
|
textStyle: {
|
||||||
color: token.colorTextTertiary,
|
color: token.colorTextTertiary,
|
||||||
overflow: 'truncate',
|
overflow: 'truncate',
|
||||||
// 15% of typical xl card canvas (~680-900px) ≈ 102-135px column;
|
width: 180
|
||||||
// marker(8) + gap(5) + 90 = 103 fits even at the narrow end.
|
|
||||||
width: 90
|
|
||||||
},
|
},
|
||||||
data: legendNames,
|
data: legendNames,
|
||||||
show: legendNames.length > 0
|
show: legendNames.length > 0,
|
||||||
|
tooltip: {
|
||||||
|
show: true,
|
||||||
|
backgroundColor: token.colorBgElevated,
|
||||||
|
borderColor: 'transparent',
|
||||||
|
textStyle: { color: token.colorText, fontSize: 12 }
|
||||||
|
}
|
||||||
},
|
},
|
||||||
series: [
|
series: [
|
||||||
{
|
{
|
||||||
@@ -295,12 +286,6 @@ const UsageByModel: React.FC<UsageByModelProps> = ({
|
|||||||
apiLabel
|
apiLabel
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Association: hovering a slice in one donut highlights the same route in
|
|
||||||
// the other donut. The Chart wrapper's imperative handle is committed
|
|
||||||
// before its own init effect runs, so chartRef can be { chart: undefined }
|
|
||||||
// on the first useEffect tick — poll via rAF until the instance shows up,
|
|
||||||
// then bind once. ECharts' clear() preserves user listeners, so we don't
|
|
||||||
// need to re-bind on every options change.
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let raf: number | undefined;
|
let raf: number | undefined;
|
||||||
let detach: (() => void) | undefined;
|
let detach: (() => void) | undefined;
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import { getGPUStackPlugin } from '@/plugins';
|
||||||
|
import { ThemeTag } from '@gpustack/core-ui';
|
||||||
|
import { useAccess, useIntl } from '@umijs/max';
|
||||||
|
import useUserDirectory from '../hooks/use-user-directory';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Owner tag for template cards, disambiguating same-name templates in
|
||||||
|
* the admin's cross-tenant view. Renders nothing for non-admin callers
|
||||||
|
* (the management page is `mine`-scoped for them) and when a plugin
|
||||||
|
* provides its own `OwnerScopeTag` slot.
|
||||||
|
*/
|
||||||
|
const OwnerTag: React.FC<{ ownerId?: number | null }> = ({ ownerId }) => {
|
||||||
|
const intl = useIntl();
|
||||||
|
const access = useAccess();
|
||||||
|
const pluginOwnsTag = !!getGPUStackPlugin()?.components?.OwnerScopeTag;
|
||||||
|
const show = !!access.canSeeAdmin && !pluginOwnsTag;
|
||||||
|
const users = useUserDirectory(show);
|
||||||
|
|
||||||
|
if (!show) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ownerId == null) {
|
||||||
|
return (
|
||||||
|
<ThemeTag color="gold" style={{ fontWeight: 400 }}>
|
||||||
|
{intl.formatMessage({ id: 'gpuservice.owner.global' })}
|
||||||
|
</ThemeTag>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const username = users.get(ownerId);
|
||||||
|
if (!username) {
|
||||||
|
// Directory still loading, or a non-USER principal (built-in
|
||||||
|
// platform principal) — nothing meaningful to show on a card.
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ThemeTag color="blue" style={{ fontWeight: 400 }}>
|
||||||
|
{username}
|
||||||
|
</ThemeTag>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default OwnerTag;
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
import { getGPUStackPlugin } from '@/plugins';
|
||||||
|
import { AutoTooltip } from '@gpustack/core-ui';
|
||||||
|
import { useAccess, useIntl } from '@umijs/max';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
import useUserDirectory from './use-user-directory';
|
||||||
|
|
||||||
|
type CreatorAware = {
|
||||||
|
creator_id?: number | null;
|
||||||
|
owner_principal_id?: number | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Concrete column shape rather than antd's `ColumnsType` union:
|
||||||
|
// spreading a union-typed array into a page's column literal breaks
|
||||||
|
// contextual typing for the sibling inline columns (their render
|
||||||
|
// params degrade to implicit `any`), while a concrete object type
|
||||||
|
// composes cleanly — same contract the plugin columns use.
|
||||||
|
type CreatorColumn<T> = {
|
||||||
|
title: string;
|
||||||
|
key: string;
|
||||||
|
ellipsis: { showTitle: false };
|
||||||
|
render: (text: any, record: T) => React.ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CreatorLabel: React.FC<{
|
||||||
|
record: CreatorAware;
|
||||||
|
users: Map<number, string>;
|
||||||
|
}> = ({ record, users }) => {
|
||||||
|
// Legacy rows predate `creator_id`. A personal-scope row's creator
|
||||||
|
// IS its owner, so the owner id doubles as the fallback — the same
|
||||||
|
// derivation the backend migration backfills with; keeping it here
|
||||||
|
// covers servers that haven't run the migration yet.
|
||||||
|
const id = record.creator_id ?? record.owner_principal_id;
|
||||||
|
const username = id != null ? users.get(id) : undefined;
|
||||||
|
if (username) {
|
||||||
|
return (
|
||||||
|
<AutoTooltip ghost maxWidth={240}>
|
||||||
|
{username}
|
||||||
|
</AutoTooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Unresolvable: the directory is still loading, or a legacy row
|
||||||
|
// whose only attribution is a non-USER principal (platform-owned).
|
||||||
|
return <span>-</span>;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* "Creator" column for the GPU-service list pages, attributing each
|
||||||
|
* row to the user who created it — disambiguates same-name rows from
|
||||||
|
* different users in the admin's cross-tenant view, and reads as a
|
||||||
|
* plain username for admin-created rows too. Returns `[]` for
|
||||||
|
* non-admin callers (their lists only ever contain their own rows)
|
||||||
|
* and when a plugin registers list columns for `pageKey` — the
|
||||||
|
* plugin's own attribution column covers the same ground.
|
||||||
|
*/
|
||||||
|
const useCreatorColumn = <T extends CreatorAware>(
|
||||||
|
pageKey: string
|
||||||
|
): CreatorColumn<T>[] => {
|
||||||
|
const intl = useIntl();
|
||||||
|
const access = useAccess();
|
||||||
|
const pluginOwnsSlot = !!(
|
||||||
|
getGPUStackPlugin()?.listExtraColumns as Record<string, unknown> | undefined
|
||||||
|
)?.[pageKey];
|
||||||
|
const show = !!access.canSeeAdmin && !pluginOwnsSlot;
|
||||||
|
const users = useUserDirectory(show);
|
||||||
|
|
||||||
|
return useMemo(() => {
|
||||||
|
if (!show) return [];
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
title: intl.formatMessage({ id: 'gpuservice.creator' }),
|
||||||
|
key: 'creator',
|
||||||
|
ellipsis: { showTitle: false as const },
|
||||||
|
render: (_text: any, record: T) => (
|
||||||
|
<CreatorLabel record={record} users={users} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}, [show, users, intl]);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useCreatorColumn;
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import { queryUserDirectory } from '@/pages/users/apis';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
// Module-level cache: the directory is fetched once per app session and
|
||||||
|
// shared by every consumer (owner columns on the Instances / Public Keys /
|
||||||
|
// Storage tables, the template owner tags, the create-instance template
|
||||||
|
// picker). A failed fetch clears the cache so the next consumer retries.
|
||||||
|
let directoryPromise: Promise<Map<number, string>> | null = null;
|
||||||
|
|
||||||
|
const fetchDirectory = (): Promise<Map<number, string>> => {
|
||||||
|
directoryPromise ??= queryUserDirectory({ page: -1 })
|
||||||
|
.then(
|
||||||
|
(res) =>
|
||||||
|
new Map(
|
||||||
|
(res.items || [])
|
||||||
|
.filter((u) => u.id != null)
|
||||||
|
.map((u) => [u.id as number, u.username])
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.catch((error) => {
|
||||||
|
directoryPromise = null;
|
||||||
|
throw error;
|
||||||
|
});
|
||||||
|
return directoryPromise;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* id → username map of all users, for resolving `owner_principal_id`
|
||||||
|
* to a display name (a USER principal's id IS the user's id).
|
||||||
|
*
|
||||||
|
* Only fetches when `enabled` — callers gate on platform admin, since
|
||||||
|
* `/user-directory` 403s for regular users (who only ever see their
|
||||||
|
* own resources anyway). Returns an empty map until loaded.
|
||||||
|
*/
|
||||||
|
export default function useUserDirectory(enabled: boolean) {
|
||||||
|
const [users, setUsers] = useState<Map<number, string>>(new Map());
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!enabled) return undefined;
|
||||||
|
let alive = true;
|
||||||
|
fetchDirectory()
|
||||||
|
.then((map) => {
|
||||||
|
if (alive) setUsers(map);
|
||||||
|
})
|
||||||
|
.catch(() => {});
|
||||||
|
return () => {
|
||||||
|
alive = false;
|
||||||
|
};
|
||||||
|
}, [enabled]);
|
||||||
|
|
||||||
|
return users;
|
||||||
|
}
|
||||||
@@ -1,7 +1,10 @@
|
|||||||
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 { useQueryClusterList } from '@/pages/cluster-management/services/use-query-cluster-list';
|
import { useQueryClusterList } from '@/pages/cluster-management/services/use-query-cluster-list';
|
||||||
|
import useUserDirectory from '@/pages/gpu-service/hooks/use-user-directory';
|
||||||
import Separator from '@/pages/llmodels/components/separator';
|
import Separator from '@/pages/llmodels/components/separator';
|
||||||
|
import { getGPUStackPlugin } from '@/plugins';
|
||||||
import { SearchOutlined } from '@ant-design/icons';
|
import { SearchOutlined } from '@ant-design/icons';
|
||||||
import {
|
import {
|
||||||
AlertBlockInfo,
|
AlertBlockInfo,
|
||||||
@@ -9,14 +12,15 @@ import {
|
|||||||
GSDrawer,
|
GSDrawer,
|
||||||
ModalFooter
|
ModalFooter
|
||||||
} from '@gpustack/core-ui';
|
} from '@gpustack/core-ui';
|
||||||
import { useIntl } from '@umijs/max';
|
import { useIntl, useModel } from '@umijs/max';
|
||||||
import { Empty, Input, Typography } from 'antd';
|
import { Empty, Input, Typography } from 'antd';
|
||||||
|
import _ from 'lodash';
|
||||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { ListItem as TemplateItem } from '../../templates/config/types';
|
import { ListItem as TemplateItem } from '../../templates/config/types';
|
||||||
import useQueryTemplates from '../../templates/services/use-query-templates';
|
import useQueryTemplates from '../../templates/services/use-query-templates';
|
||||||
import { FormData, InstanceTypeItem, ListItem } from '../config/types';
|
import { FormData, InstanceTypeItem, ListItem } from '../config/types';
|
||||||
import GPUServiceInstanceForm from '../forms';
|
import GPUServiceInstanceForm from '../forms';
|
||||||
import TemplateSelector from '../forms/template-selector';
|
import TemplateSelector, { TemplateGroup } from '../forms/template-selector';
|
||||||
import useQueryInstanceTypes from '../services/use-query-instance-types';
|
import useQueryInstanceTypes from '../services/use-query-instance-types';
|
||||||
import styles from '../styles/instances.module.less';
|
import styles from '../styles/instances.module.less';
|
||||||
import InstanceTypeList from './instance-type-list';
|
import InstanceTypeList from './instance-type-list';
|
||||||
@@ -77,6 +81,12 @@ const AddModal: React.FC<AddModalProps> = ({
|
|||||||
realAction
|
realAction
|
||||||
}) => {
|
}) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
const { initialState } = useModel('@@initialState') || {};
|
||||||
|
const currentUser = initialState?.currentUser;
|
||||||
|
const pluginActive = !!getGPUStackPlugin();
|
||||||
|
const userDirectory = useUserDirectory(
|
||||||
|
!!currentUser?.is_admin && !pluginActive
|
||||||
|
);
|
||||||
const form = useRef<any>(null);
|
const form = useRef<any>(null);
|
||||||
const sessionRef = useRef(0);
|
const sessionRef = useRef(0);
|
||||||
const [instanceTypeSelection, setInstanceTypeSelection] = useState<{
|
const [instanceTypeSelection, setInstanceTypeSelection] = useState<{
|
||||||
@@ -89,7 +99,8 @@ const AddModal: React.FC<AddModalProps> = ({
|
|||||||
const [templateId, setTemplateId] = useState<number | undefined>();
|
const [templateId, setTemplateId] = useState<number | undefined>();
|
||||||
const [instanceKeyword, setInstanceKeyword] = useState('');
|
const [instanceKeyword, setInstanceKeyword] = useState('');
|
||||||
const [templateKeyword, setTemplateKeyword] = useState('');
|
const [templateKeyword, setTemplateKeyword] = useState('');
|
||||||
const [loading, setLoading] = useState(false);
|
const { loading, guard, run, release } = useSubmitLock();
|
||||||
|
const initializedRef = useRef(false);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
detailData: instanceTypeList,
|
detailData: instanceTypeList,
|
||||||
@@ -169,17 +180,27 @@ const AddModal: React.FC<AddModalProps> = ({
|
|||||||
return JSON.stringify({
|
return JSON.stringify({
|
||||||
name: instanceType.name,
|
name: instanceType.name,
|
||||||
spec: {
|
spec: {
|
||||||
...instanceType.spec
|
..._.omit(instanceType.spec, ['cache', 'cpu']),
|
||||||
|
cpu: _.pick(instanceType.spec?.cpu, [
|
||||||
|
'manufacturer',
|
||||||
|
'product',
|
||||||
|
'family'
|
||||||
|
])
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// GPU types carry their accelerator vendor; non-acceleratable (CPU) types
|
||||||
|
// all map to the single 'cpu' bucket used to match templates.
|
||||||
|
const manufacturerOf = (instanceType: InstanceTypeItem) =>
|
||||||
|
instanceType.spec.acceleratable ? instanceType.spec?.manufacturer : 'cpu';
|
||||||
|
|
||||||
// apply the selection of instance type and template
|
// apply the selection of instance type and template
|
||||||
const applySelection = (
|
const applySelection = (
|
||||||
instanceType: InstanceTypeItem,
|
instanceType: InstanceTypeItem,
|
||||||
template: TemplateItem | undefined
|
template: TemplateItem | undefined
|
||||||
) => {
|
) => {
|
||||||
const manufacturer = instanceType.spec?.manufacturer;
|
const manufacturer = manufacturerOf(instanceType);
|
||||||
|
|
||||||
setInstanceTypeSelection({
|
setInstanceTypeSelection({
|
||||||
instanceType: instanceType.name,
|
instanceType: instanceType.name,
|
||||||
@@ -210,6 +231,35 @@ const AddModal: React.FC<AddModalProps> = ({
|
|||||||
form.current?.applyInstanceType?.(instanceType);
|
form.current?.applyInstanceType?.(instanceType);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Drop the instance-type-derived selection + form state. Used when no
|
||||||
|
// candidate is available (empty segment / org with no clusters) so a stale
|
||||||
|
// type / cluster never survives a switch or reload.
|
||||||
|
const clearSelection = () => {
|
||||||
|
setInstanceTypeSelection({
|
||||||
|
instanceType: undefined,
|
||||||
|
manufacturer: undefined
|
||||||
|
});
|
||||||
|
setTemplateId(undefined);
|
||||||
|
form.current?.applyInstanceType?.(undefined);
|
||||||
|
form.current?.setFieldValue?.('clusterId', null);
|
||||||
|
form.current?.setFieldValue?.(['spec', 'type'], undefined);
|
||||||
|
};
|
||||||
|
|
||||||
|
const autoSelectFirst = (
|
||||||
|
types: InstanceTypeItem[],
|
||||||
|
templates: TemplateItem[]
|
||||||
|
) => {
|
||||||
|
const first = types.find((item) => !item.disabled);
|
||||||
|
if (!first) {
|
||||||
|
clearSelection();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
applySelection(
|
||||||
|
first,
|
||||||
|
findTemplateByManufacturer(manufacturerOf(first), templates)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const findAggregateOf = (
|
const findAggregateOf = (
|
||||||
candidateName: string | undefined,
|
candidateName: string | undefined,
|
||||||
clusterId: number | null | undefined,
|
clusterId: number | null | undefined,
|
||||||
@@ -246,7 +296,7 @@ const AddModal: React.FC<AddModalProps> = ({
|
|||||||
if (aggregate) {
|
if (aggregate) {
|
||||||
setInstanceTypeSelection({
|
setInstanceTypeSelection({
|
||||||
instanceType: aggregate.name,
|
instanceType: aggregate.name,
|
||||||
manufacturer: aggregate.spec?.manufacturer
|
manufacturer: manufacturerOf(aggregate)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@@ -254,31 +304,10 @@ const AddModal: React.FC<AddModalProps> = ({
|
|||||||
|
|
||||||
// Scope to clusters the chosen org owns (admin "All" view).
|
// Scope to clusters the chosen org owns (admin "All" view).
|
||||||
const owned = filterTypesByOwner(instanceTypes, clusters || [], orgId);
|
const owned = filterTypesByOwner(instanceTypes, clusters || [], orgId);
|
||||||
const first = owned.find((item) => !item.disabled);
|
|
||||||
|
|
||||||
if (!first) {
|
// On create, auto-select the first available instance type (clears the
|
||||||
// The chosen org has no clusters (hence no instance types). Clear any
|
// selection when the chosen org has none).
|
||||||
// prior pick so a stale instance type / cross-org cluster isn't left
|
autoSelectFirst(owned, templates);
|
||||||
// on the form.
|
|
||||||
setInstanceTypeSelection({
|
|
||||||
instanceType: undefined,
|
|
||||||
manufacturer: undefined
|
|
||||||
});
|
|
||||||
setTemplateId(undefined);
|
|
||||||
form.current?.applyInstanceType?.(undefined);
|
|
||||||
form.current?.setFieldValue?.('clusterId', null);
|
|
||||||
form.current?.setFieldValue?.(['spec', 'type'], undefined);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// On create, auto-select the first instance type in the list
|
|
||||||
|
|
||||||
const template = findTemplateByManufacturer(
|
|
||||||
first.spec?.manufacturer,
|
|
||||||
templates
|
|
||||||
);
|
|
||||||
|
|
||||||
applySelection(first, template);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Fetch the (tenant-scoped) instance types + templates and auto-select.
|
// Fetch the (tenant-scoped) instance types + templates and auto-select.
|
||||||
@@ -287,19 +316,25 @@ const AddModal: React.FC<AddModalProps> = ({
|
|||||||
// picker settling on its default) the latest scope's result wins.
|
// picker settling on its default) the latest scope's result wins.
|
||||||
const loadCreateResources = (orgId?: number | null) => {
|
const loadCreateResources = (orgId?: number | null) => {
|
||||||
const session = ++sessionRef.current;
|
const session = ++sessionRef.current;
|
||||||
Promise.all([
|
try {
|
||||||
fetchData({ page: -1 }),
|
Promise.all([
|
||||||
fetchTemplates({ page: -1 }),
|
fetchData({ page: -1 }),
|
||||||
fetchClusterList({ page: -1 })
|
fetchTemplates({ page: -1 }),
|
||||||
]).then(([instanceResItems, templatesRes, clusters]) => {
|
fetchClusterList({ page: -1 })
|
||||||
if (sessionRef.current !== session) return;
|
]).then(([instanceResItems, templatesRes, clusters]) => {
|
||||||
applyAutoSelection(
|
if (sessionRef.current !== session) return;
|
||||||
instanceResItems || [],
|
applyAutoSelection(
|
||||||
templatesRes?.items || [],
|
instanceResItems || [],
|
||||||
(Array.isArray(clusters) ? clusters : (clusters as any)?.items) || [],
|
templatesRes?.items || [],
|
||||||
orgId
|
(Array.isArray(clusters) ? clusters : (clusters as any)?.items) || [],
|
||||||
);
|
orgId
|
||||||
});
|
);
|
||||||
|
initializedRef.current = true;
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
} finally {
|
||||||
|
initializedRef.current = true;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Platform admin retargeted the create to another org (or Global). The
|
// Platform admin retargeted the create to another org (or Global). The
|
||||||
@@ -309,26 +344,21 @@ const AddModal: React.FC<AddModalProps> = ({
|
|||||||
const handleScopeChange = (orgId?: number | null) => {
|
const handleScopeChange = (orgId?: number | null) => {
|
||||||
if (!open || action !== PageAction.CREATE) return;
|
if (!open || action !== PageAction.CREATE) return;
|
||||||
setScopeOrgId(orgId);
|
setScopeOrgId(orgId);
|
||||||
setInstanceTypeSelection({
|
// Drop the instance-type-derived selection + form state (the selected type
|
||||||
instanceType: undefined,
|
|
||||||
manufacturer: undefined
|
|
||||||
});
|
|
||||||
setTemplateId(undefined);
|
|
||||||
// Also clear the instance-type-derived form state (the selected type
|
|
||||||
// card + its limits, the cluster, and spec.type). The cluster decides
|
// card + its limits, the cluster, and spec.type). The cluster decides
|
||||||
// where the instance is scheduled, so a stale pick from the previous
|
// where the instance is scheduled, so a stale pick from the previous
|
||||||
// scope must not survive — otherwise an instance owned by the newly
|
// scope must not survive — otherwise an instance owned by the newly
|
||||||
// chosen org could land on the old org's cluster. The reload's
|
// chosen org could land on the old org's cluster. The reload's
|
||||||
// owner-scoped auto-selection re-fills them from the new org, or leaves
|
// owner-scoped auto-selection re-fills them from the new org, or leaves
|
||||||
// them empty (blocking submit) when the chosen org has no clusters.
|
// them empty (blocking submit) when the chosen org has no clusters.
|
||||||
form.current?.applyInstanceType?.(undefined);
|
clearSelection();
|
||||||
form.current?.setFieldValue?.('clusterId', null);
|
initializedRef.current = false;
|
||||||
form.current?.setFieldValue?.(['spec', 'type'], undefined);
|
|
||||||
loadCreateResources(orgId);
|
loadCreateResources(orgId);
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!open) {
|
if (!open) {
|
||||||
|
initializedRef.current = false;
|
||||||
sessionRef.current += 1;
|
sessionRef.current += 1;
|
||||||
setInstanceTypeSelection({
|
setInstanceTypeSelection({
|
||||||
instanceType: undefined,
|
instanceType: undefined,
|
||||||
@@ -373,8 +403,91 @@ const AddModal: React.FC<AddModalProps> = ({
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Group the picker by owning scope so same-name templates stay
|
||||||
|
// distinguishable: the caller's own templates first, then the
|
||||||
|
// admin-curated Global presets, then — platform admin's cross-tenant
|
||||||
|
// view only — other users' templates, one group per owner.
|
||||||
|
//
|
||||||
|
// The default buckets below assume every non-Global owner is a USER
|
||||||
|
// principal. A plugin's principal model may scope templates to other
|
||||||
|
// owner kinds (no user-directory entry, not the caller's user id),
|
||||||
|
// which these buckets would mislabel — so a plugin can take over
|
||||||
|
// grouping via `hooks.useTemplateOwnerGroups`. The registry is wired
|
||||||
|
// at boot, so the conditional hook call is render-stable — same
|
||||||
|
// contract as `usePluginListColumns`' function entries.
|
||||||
|
const usePluginTemplateGroups = getGPUStackPlugin()?.hooks
|
||||||
|
?.useTemplateOwnerGroups as
|
||||||
|
| ((items: TemplateItem[]) => TemplateGroup[])
|
||||||
|
| undefined;
|
||||||
|
const pluginTemplateGroups = usePluginTemplateGroups?.(filteredTemplates);
|
||||||
|
|
||||||
|
const templateGroups: TemplateGroup[] = useMemo(() => {
|
||||||
|
if (pluginTemplateGroups) {
|
||||||
|
return pluginTemplateGroups;
|
||||||
|
}
|
||||||
|
if (pluginActive) {
|
||||||
|
// Plugin present but without the grouping hook (older plugin
|
||||||
|
// build): keep the flat list rather than mislabeling owners
|
||||||
|
// outside the USER-principal model.
|
||||||
|
return filteredTemplates.length
|
||||||
|
? [{ key: 'all', label: null, items: filteredTemplates }]
|
||||||
|
: [];
|
||||||
|
}
|
||||||
|
const yours: TemplateItem[] = [];
|
||||||
|
const globals: TemplateItem[] = [];
|
||||||
|
const byOwner = new Map<number, TemplateItem[]>();
|
||||||
|
|
||||||
|
filteredTemplates.forEach((item) => {
|
||||||
|
if (item.owner_principal_id == null) {
|
||||||
|
globals.push(item);
|
||||||
|
} else if (item.owner_principal_id === currentUser?.id) {
|
||||||
|
yours.push(item);
|
||||||
|
} else {
|
||||||
|
const list = byOwner.get(item.owner_principal_id) || [];
|
||||||
|
list.push(item);
|
||||||
|
byOwner.set(item.owner_principal_id, list);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const groups: TemplateGroup[] = [];
|
||||||
|
if (yours.length) {
|
||||||
|
groups.push({
|
||||||
|
key: 'yours',
|
||||||
|
label: intl.formatMessage({ id: 'gpuservice.template.group.yours' }),
|
||||||
|
items: yours
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (globals.length) {
|
||||||
|
groups.push({
|
||||||
|
key: 'global',
|
||||||
|
label: intl.formatMessage({ id: 'gpuservice.template.group.global' }),
|
||||||
|
items: globals
|
||||||
|
});
|
||||||
|
}
|
||||||
|
groups.push(
|
||||||
|
...[...byOwner.entries()]
|
||||||
|
.map(([ownerId, items]) => ({
|
||||||
|
key: `owner-${ownerId}`,
|
||||||
|
// `#id` is a placeholder for the moment before the user
|
||||||
|
// directory resolves (the memo recomputes once it lands)
|
||||||
|
// and for the API-only case of a non-USER owner.
|
||||||
|
label: userDirectory.get(ownerId) || `#${ownerId}`,
|
||||||
|
items
|
||||||
|
}))
|
||||||
|
.sort((a, b) => String(a.label).localeCompare(String(b.label)))
|
||||||
|
);
|
||||||
|
return groups;
|
||||||
|
}, [
|
||||||
|
filteredTemplates,
|
||||||
|
currentUser?.id,
|
||||||
|
userDirectory,
|
||||||
|
intl,
|
||||||
|
pluginActive,
|
||||||
|
pluginTemplateGroups
|
||||||
|
]);
|
||||||
|
|
||||||
const handleSubmit = () => {
|
const handleSubmit = () => {
|
||||||
form.current?.submit();
|
guard(() => form.current?.submit());
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCancel = () => {
|
const handleCancel = () => {
|
||||||
@@ -383,20 +496,17 @@ const AddModal: React.FC<AddModalProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const onFinish = async (values: FormData) => {
|
const onFinish = async (values: FormData) => {
|
||||||
setLoading(true);
|
await run(async () => {
|
||||||
try {
|
|
||||||
await onOk({
|
await onOk({
|
||||||
...values
|
...values
|
||||||
});
|
});
|
||||||
console.log('submit form values', values);
|
console.log('submit form values', values);
|
||||||
} finally {
|
});
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleInstanceTypeChange = (item: InstanceTypeItem) => {
|
const handleInstanceTypeChange = (item: InstanceTypeItem) => {
|
||||||
const template = findTemplateByManufacturer(
|
const template = findTemplateByManufacturer(
|
||||||
item.spec?.manufacturer,
|
manufacturerOf(item),
|
||||||
templateList
|
templateList
|
||||||
);
|
);
|
||||||
applySelection(item, template);
|
applySelection(item, template);
|
||||||
@@ -456,7 +566,9 @@ const AddModal: React.FC<AddModalProps> = ({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ColTitle style={{ paddingBottom: 0 }}>
|
<ColTitle style={{ paddingBottom: 0 }}>
|
||||||
{intl.formatMessage({ id: 'gpuservice.instance.types' })}
|
{intl.formatMessage({
|
||||||
|
id: 'gpuservice.instance.types'
|
||||||
|
})}
|
||||||
</ColTitle>
|
</ColTitle>
|
||||||
<Input
|
<Input
|
||||||
allowClear
|
allowClear
|
||||||
@@ -507,10 +619,10 @@ const AddModal: React.FC<AddModalProps> = ({
|
|||||||
onChange={(e) => setTemplateKeyword(e.target.value)}
|
onChange={(e) => setTemplateKeyword(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{filteredTemplates.length > 0 ? (
|
{filteredTemplates.length > 0 && initializedRef.current ? (
|
||||||
<TemplateSelector
|
<TemplateSelector
|
||||||
value={templateId}
|
value={templateId}
|
||||||
dataList={filteredTemplates}
|
groups={templateGroups}
|
||||||
onChange={handleTemplateChange}
|
onChange={handleTemplateChange}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
@@ -565,6 +677,7 @@ const AddModal: React.FC<AddModalProps> = ({
|
|||||||
currentData={data}
|
currentData={data}
|
||||||
disabled={readonly}
|
disabled={readonly}
|
||||||
onFinish={onFinish}
|
onFinish={onFinish}
|
||||||
|
onFinishFailed={release}
|
||||||
onScopeChange={handleScopeChange}
|
onScopeChange={handleScopeChange}
|
||||||
open={open}
|
open={open}
|
||||||
instanceTypeList={ownedInstanceTypes}
|
instanceTypeList={ownedInstanceTypes}
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ const InstanceTypeCell: React.FC<{
|
|||||||
}> = ({ title, name, sections }) => {
|
}> = ({ title, name, sections }) => {
|
||||||
const specInfo = (
|
const specInfo = (
|
||||||
<div style={{ minWidth: 200 }}>
|
<div style={{ minWidth: 200 }}>
|
||||||
{name && <div style={{ fontWeight: 600, marginBottom: 8 }}>{name}</div>}
|
|
||||||
{sections.map((sec) => {
|
{sections.map((sec) => {
|
||||||
const rows = sec.rows.filter(([, v]) => v && v !== '-');
|
const rows = sec.rows.filter(([, v]) => v && v !== '-');
|
||||||
if (!rows.length) return null;
|
if (!rows.length) return null;
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user