Compare commits
66
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
17527b0fa5 | ||
|
|
2f04e01f4f | ||
|
|
a51f44c135 | ||
|
|
7f14eb544c | ||
|
|
c6e34db329 | ||
|
|
acb90531b2 | ||
|
|
91c3d0b814 | ||
|
|
95e03d11c1 | ||
|
|
88ab927755 | ||
|
|
81c7e2ee61 | ||
|
|
b5ed64de51 | ||
|
|
921a2a0d81 | ||
|
|
5195b1e5fc | ||
|
|
f886422f91 | ||
|
|
27d57d7563 | ||
|
|
2ebb8f44b7 | ||
|
|
a0b408e8cb | ||
|
|
858e0dae09 | ||
|
|
7bee337554 | ||
|
|
7944b3f579 | ||
|
|
5e889563ca | ||
|
|
aad3b458c8 | ||
|
|
acef2ef89a | ||
|
|
d99c25dd03 | ||
|
|
f0a1dad48e | ||
|
|
1e334903dd | ||
|
|
342583f8d1 | ||
|
|
6af18275c4 | ||
|
|
9c519d937d | ||
|
|
4b14fa6ca8 | ||
|
|
dc69e51042 | ||
|
|
052d5aa474 | ||
|
|
0c55c1c1b4 | ||
|
|
6f896b4f94 | ||
|
|
e7a376db70 | ||
|
|
15cf4d89b7 | ||
|
|
c38fcc494a | ||
|
|
44e5b78d29 | ||
|
|
38107a67fe | ||
|
|
0274bcb8cd | ||
|
|
a91c1545f8 | ||
|
|
0b3884b1a5 | ||
|
|
bcea46fa8f | ||
|
|
abc3c2c25a | ||
|
|
db7a8a702d | ||
|
|
288bfebc13 | ||
|
|
c7996d3e14 | ||
|
|
0d9b6325f9 | ||
|
|
baf1508644 | ||
|
|
d04604abe5 | ||
|
|
bf09784fad | ||
|
|
67f5c794e1 | ||
|
|
55fbe118d5 | ||
|
|
ae8ec8572c | ||
|
|
38b91a558b | ||
|
|
a001f83abb | ||
|
|
87c8a3a64d | ||
|
|
c6e7075bf5 | ||
|
|
5d75cfdfc5 | ||
|
|
3f9852c932 | ||
|
|
9dfaf990fd | ||
|
|
1bef1637e0 | ||
|
|
451d5bfe47 | ||
|
|
faa1f935b7 | ||
|
|
c428fe175a | ||
|
|
9767c8bf39 |
@@ -15,3 +15,4 @@
|
|||||||
.idea
|
.idea
|
||||||
.claude
|
.claude
|
||||||
/dist.zip
|
/dist.zip
|
||||||
|
.cache
|
||||||
@@ -0,0 +1,265 @@
|
|||||||
|
# React State and Request Patterns
|
||||||
|
|
||||||
|
These guidelines define preferred patterns for request handling, state updates, and side-effect management in React applications.
|
||||||
|
|
||||||
|
The primary goal is to keep data flow explicit, predictable, maintainable, and performant while avoiding unnecessary rerenders and effect-driven logic.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Avoid Effect-Driven Requests
|
||||||
|
|
||||||
|
Do not use request functions themselves as dependencies in `useEffect`.
|
||||||
|
|
||||||
|
Avoid patterns like:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
useEffect(() => {
|
||||||
|
fetchData();
|
||||||
|
}, [fetchData]);
|
||||||
|
```
|
||||||
|
|
||||||
|
Requests should be triggered explicitly by user actions or lifecycle entry points.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Form Requests Should Be Action-Driven
|
||||||
|
|
||||||
|
For form-related requests (such as loading `Select` options):
|
||||||
|
|
||||||
|
- Fetch data when the form is opened for the first time.
|
||||||
|
- If later requests depend on user interactions, trigger them directly inside the interaction handler.
|
||||||
|
- Do not rely on `useEffect` dependency changes to trigger requests.
|
||||||
|
|
||||||
|
Recommended:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const handleOnChange = (value) => {
|
||||||
|
fetchData(value);
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
Avoid:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
useEffect(() => {
|
||||||
|
fetchData(value);
|
||||||
|
}, [value]);
|
||||||
|
```
|
||||||
|
|
||||||
|
The action itself should control the request.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Update Related States Together
|
||||||
|
|
||||||
|
If a single action updates multiple related states:
|
||||||
|
|
||||||
|
- Do not synchronize them through `useEffect`
|
||||||
|
- Do not derive them indirectly through `useMemo`
|
||||||
|
|
||||||
|
Instead, update all related states directly inside the action handler.
|
||||||
|
|
||||||
|
Recommended:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const handleOnChange = (value) => {
|
||||||
|
setState1(...);
|
||||||
|
setState2(...);
|
||||||
|
buildState(...);
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
Avoid implicit state synchronization chains.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Group Strongly Related State
|
||||||
|
|
||||||
|
If multiple states are always updated together:
|
||||||
|
|
||||||
|
- Do not split them into multiple `useState` calls.
|
||||||
|
- Prefer a single state object.
|
||||||
|
|
||||||
|
Recommended:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const [state, setState] = useState({
|
||||||
|
state1: ...,
|
||||||
|
state2: ...,
|
||||||
|
state3: ...,
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
This reduces unnecessary rerenders and keeps state transitions predictable.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Prefer Explicit State Flow
|
||||||
|
|
||||||
|
Avoid chaining business logic through multiple `useEffect` hooks.
|
||||||
|
|
||||||
|
Keep:
|
||||||
|
|
||||||
|
- request execution
|
||||||
|
- state updates
|
||||||
|
- derived calculations
|
||||||
|
|
||||||
|
close to the triggering action whenever possible.
|
||||||
|
|
||||||
|
Prefer:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const handleAction = () => {
|
||||||
|
fetchData();
|
||||||
|
setTableData(...);
|
||||||
|
setSelectedRow(...);
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
Over:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
useEffect(() => {
|
||||||
|
buildTable();
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
updateSelection();
|
||||||
|
}, [tableData]);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Avoid Premature Memoization
|
||||||
|
|
||||||
|
Do not use `useMemo` or `useCallback` unless there is a confirmed rendering or computation bottleneck.
|
||||||
|
|
||||||
|
Overusing memoization:
|
||||||
|
|
||||||
|
- increases complexity
|
||||||
|
- makes state flow harder to understand
|
||||||
|
- may introduce stale dependency issues
|
||||||
|
|
||||||
|
Prefer simple and explicit logic first.
|
||||||
|
|
||||||
|
Optimize only when necessary.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Keep Request Logic Predictable
|
||||||
|
|
||||||
|
A user interaction should clearly show:
|
||||||
|
|
||||||
|
- what request is triggered
|
||||||
|
- which states are updated
|
||||||
|
- how the UI changes
|
||||||
|
|
||||||
|
Avoid indirect update chains caused by dependency-driven effects.
|
||||||
|
|
||||||
|
The code should make the request and update flow easy to trace.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Prefer Action-Driven Architecture
|
||||||
|
|
||||||
|
Prefer:
|
||||||
|
|
||||||
|
- action-driven updates
|
||||||
|
- explicit handlers
|
||||||
|
- localized state transitions
|
||||||
|
|
||||||
|
Over:
|
||||||
|
|
||||||
|
- effect-driven synchronization
|
||||||
|
- cross-hook implicit updates
|
||||||
|
- reactive chains between states
|
||||||
|
|
||||||
|
The triggering action should remain the primary source of truth for UI updates.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Form
|
||||||
|
|
||||||
|
Form-specific patterns that build on the rules above. The theme: keep cascading selections (pick A → derive B → write form) on a single, predictable path.
|
||||||
|
|
||||||
|
## 1. No Fallback for Derived Selection
|
||||||
|
|
||||||
|
When "pick A then auto-pick B", match by rule and return `undefined` if no match — let the corresponding form field stay empty.
|
||||||
|
|
||||||
|
Do not silently fall back to `list[0]` or another default. A fallback hides data issues and tricks the user into thinking they have a valid selection.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const findB = (key, list) =>
|
||||||
|
key ? list.find((x) => x.key === key) : undefined;
|
||||||
|
```
|
||||||
|
|
||||||
|
For form fields, prefer clearing with `undefined` over `''`. With Ant Design, `undefined` restores the placeholder; `''` is treated as a real value.
|
||||||
|
|
||||||
|
## 2. Async Race Protection
|
||||||
|
|
||||||
|
For fetches triggered by a lifecycle entry (e.g., modal open), tag each invocation with a session ref. Discard stale results if the session has rotated (the modal was closed and re-opened) by the time the response arrives.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const sessionRef = useRef(0);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open) {
|
||||||
|
sessionRef.current += 1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const session = ++sessionRef.current;
|
||||||
|
Promise.all([fetchA(), fetchB()]).then(([as, bs]) => {
|
||||||
|
if (sessionRef.current !== session) return;
|
||||||
|
applySelection(as.items[0], findB(as.items[0].key, bs.items));
|
||||||
|
});
|
||||||
|
}, [open]);
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. Reference Template
|
||||||
|
|
||||||
|
A typical form with two cascading selectors backed by a single shared state:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
type Selection = { a?: string; b?: number };
|
||||||
|
|
||||||
|
const [selection, setSelection] = useState<Selection>({});
|
||||||
|
const sessionRef = useRef(0);
|
||||||
|
|
||||||
|
const findB = (key, list) =>
|
||||||
|
key ? list.find((x) => x.key === key) : undefined;
|
||||||
|
|
||||||
|
// Single atomic write: state + form together.
|
||||||
|
const applySelection = (a, b) => {
|
||||||
|
setSelection({ a: a.name, b: b?.id });
|
||||||
|
form.current?.setFieldsValue({
|
||||||
|
field: b?.field,
|
||||||
|
spec: { ...currentSpec, ...b?.spec }
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Trigger 1: modal opened
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open) {
|
||||||
|
sessionRef.current++;
|
||||||
|
setSelection({});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const session = ++sessionRef.current;
|
||||||
|
Promise.all([fetchA(), fetchB()]).then(([as, bs]) => {
|
||||||
|
if (sessionRef.current !== session) return;
|
||||||
|
const first = as.items[0];
|
||||||
|
applySelection(first, findB(first.key, bs.items));
|
||||||
|
});
|
||||||
|
}, [open]);
|
||||||
|
|
||||||
|
// Trigger 2: user picks A
|
||||||
|
const handleAChange = (a) => {
|
||||||
|
applySelection(a, findB(a.key, listB));
|
||||||
|
};
|
||||||
|
|
||||||
|
// Trigger 3: user picks B
|
||||||
|
const handleBChange = (b) => {
|
||||||
|
setSelection((prev) => ({ ...prev, b: b.id }));
|
||||||
|
form.current?.setFieldsValue({ ...b.fields });
|
||||||
|
};
|
||||||
|
```
|
||||||
+11
-1
@@ -213,10 +213,20 @@ const baseRoutes = [
|
|||||||
name: 'storage',
|
name: 'storage',
|
||||||
path: '/gpu-service/storage',
|
path: '/gpu-service/storage',
|
||||||
key: 'gpuServiceStorage',
|
key: 'gpuServiceStorage',
|
||||||
|
icon: 'icon-database-outlined',
|
||||||
|
selectedIcon: 'icon-database-filled',
|
||||||
|
defaultIcon: 'icon-database-outlined',
|
||||||
|
component: './gpu-service/storage'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'storageTypes',
|
||||||
|
path: '/gpu-service/storage-types',
|
||||||
|
key: 'gpuServiceStorageTypes',
|
||||||
icon: 'icon-storage-outlined',
|
icon: 'icon-storage-outlined',
|
||||||
|
access: 'canSeeAdmin',
|
||||||
selectedIcon: 'icon-storage-filled',
|
selectedIcon: 'icon-storage-filled',
|
||||||
defaultIcon: 'icon-storage-outlined',
|
defaultIcon: 'icon-storage-outlined',
|
||||||
component: './gpu-service/storage'
|
component: './gpu-service/storage-types'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'publicKeys',
|
name: 'publicKeys',
|
||||||
|
|||||||
+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.10",
|
"@gpustack/core-ui": "^1.0.16",
|
||||||
"@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",
|
||||||
|
|||||||
Generated
+79
-38
@@ -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.10
|
specifier: ^1.0.16
|
||||||
version: 1.0.10(czdvzceysqw7iv6pct2ucnb23e)
|
version: 1.0.16(czdvzceysqw7iv6pct2ucnb23e)
|
||||||
'@huggingface/gguf':
|
'@huggingface/gguf':
|
||||||
specifier: ^0.1.7
|
specifier: ^0.1.7
|
||||||
version: 0.1.18
|
version: 0.1.18
|
||||||
@@ -49,7 +49,7 @@ importers:
|
|||||||
version: 4.17.24
|
version: 4.17.24
|
||||||
'@umijs/max':
|
'@umijs/max':
|
||||||
specifier: ^4.6.15
|
specifier: ^4.6.15
|
||||||
version: 4.6.51(@babel/core@7.29.0)(@types/node@25.6.2)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(lightningcss@1.22.1)(prettier@3.8.3)(rc-field-form@2.7.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)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.29.0)(react@18.3.1))(terser@5.47.1)(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14))
|
version: 4.6.51(@babel/core@7.29.0)(@types/node@25.6.2)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(lightningcss@1.22.1)(prettier@3.8.3)(rc-field-form@2.7.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)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.29.0)(react@18.3.1))(terser@5.48.0)(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14))
|
||||||
'@xterm/addon-fit':
|
'@xterm/addon-fit':
|
||||||
specifier: ^0.10.0
|
specifier: ^0.10.0
|
||||||
version: 0.10.0(@xterm/xterm@5.5.0)
|
version: 0.10.0(@xterm/xterm@5.5.0)
|
||||||
@@ -205,7 +205,7 @@ importers:
|
|||||||
version: 6.4.1(css-to-react-native@3.2.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
version: 6.4.1(css-to-react-native@3.2.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||||
umi-presets-pro:
|
umi-presets-pro:
|
||||||
specifier: ^2.0.3
|
specifier: ^2.0.3
|
||||||
version: 2.0.3(@babel/core@7.29.0)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(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))(chokidar@3.6.0)(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(encoding@0.1.13)(rc-field-form@2.7.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)(typescript@5.9.3)(umi@4.6.51(@babel/core@7.29.0)(@types/node@25.6.2)(@types/react@18.3.28)(eslint@9.39.4(jiti@2.7.0))(lightningcss@1.22.1)(prettier@3.8.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.29.0)(react@18.3.1))(stylelint@14.8.2)(terser@5.47.1)(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14)))
|
version: 2.0.3(@babel/core@7.29.0)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(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))(chokidar@3.6.0)(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(encoding@0.1.13)(rc-field-form@2.7.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)(typescript@5.9.3)(umi@4.6.51(@babel/core@7.29.0)(@types/node@25.6.2)(@types/react@18.3.28)(eslint@9.39.4(jiti@2.7.0))(lightningcss@1.22.1)(prettier@3.8.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.29.0)(react@18.3.1))(stylelint@14.8.2)(terser@5.48.0)(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14)))
|
||||||
wavesurfer.js:
|
wavesurfer.js:
|
||||||
specifier: ^7.8.8
|
specifier: ^7.8.8
|
||||||
version: 7.12.6
|
version: 7.12.6
|
||||||
@@ -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.10':
|
'@gpustack/core-ui@1.0.16':
|
||||||
resolution: {integrity: sha512-gw1dlkb0NzcKy23aGa+4EPe9tHI0hgxoUf/ZJZkjvjQQjAZ+zxZFtLscHGvlTVxwc7GE5FOeGjOIGlbIxW+7EA==, tarball: https://registry.npmjs.org/@gpustack/core-ui/-/core-ui-1.0.10.tgz}
|
resolution: {integrity: sha512-wFKDv7X0FXRmAmZPt4WKYV0+aGHcx82/3EZSJkrut/49XQd5XdrqKaimGtlFWFCNEWqsouwzs4uqaC7JFqfOkQ==, tarball: https://registry.npmjs.org/@gpustack/core-ui/-/core-ui-1.0.16.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
|
||||||
@@ -2463,6 +2463,9 @@ packages:
|
|||||||
'@types/node@25.6.2':
|
'@types/node@25.6.2':
|
||||||
resolution: {integrity: sha512-sokuT28dxf9JT5Kady1fsXOvI4HVpjZa95NKT5y9PNTIrs2AsobR4GFAA90ZG8M+nxVRLysCXsVj6eGC7Vbrlw==, tarball: https://registry.npmjs.org/@types/node/-/node-25.6.2.tgz}
|
resolution: {integrity: sha512-sokuT28dxf9JT5Kady1fsXOvI4HVpjZa95NKT5y9PNTIrs2AsobR4GFAA90ZG8M+nxVRLysCXsVj6eGC7Vbrlw==, tarball: https://registry.npmjs.org/@types/node/-/node-25.6.2.tgz}
|
||||||
|
|
||||||
|
'@types/node@25.9.1':
|
||||||
|
resolution: {integrity: sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg==, tarball: https://registry.npmjs.org/@types/node/-/node-25.9.1.tgz}
|
||||||
|
|
||||||
'@types/normalize-package-data@2.4.4':
|
'@types/normalize-package-data@2.4.4':
|
||||||
resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==, tarball: https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz}
|
resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==, tarball: https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz}
|
||||||
|
|
||||||
@@ -2492,6 +2495,9 @@ packages:
|
|||||||
'@types/react@18.3.28':
|
'@types/react@18.3.28':
|
||||||
resolution: {integrity: sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==, tarball: https://registry.npmjs.org/@types/react/-/react-18.3.28.tgz}
|
resolution: {integrity: sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==, tarball: https://registry.npmjs.org/@types/react/-/react-18.3.28.tgz}
|
||||||
|
|
||||||
|
'@types/react@18.3.29':
|
||||||
|
resolution: {integrity: sha512-ch0qJdr2JY0r04NXSprbK6TXOgnaJ1Tz23fm5W+z0/CBah6BSBc3n96h7K9GOtwh0HrilNWHIBzE1Ko4Dcw/Wg==, tarball: https://registry.npmjs.org/@types/react/-/react-18.3.29.tgz}
|
||||||
|
|
||||||
'@types/resolve@1.20.6':
|
'@types/resolve@1.20.6':
|
||||||
resolution: {integrity: sha512-A4STmOXPhMUtHH+S6ymgE2GiBSMqf4oTvcQZMcHzokuTLVYzXTB8ttjcgxOVaAp2lGwEdzZ0J+cRbbeevQj1UQ==, tarball: https://registry.npmjs.org/@types/resolve/-/resolve-1.20.6.tgz}
|
resolution: {integrity: sha512-A4STmOXPhMUtHH+S6ymgE2GiBSMqf4oTvcQZMcHzokuTLVYzXTB8ttjcgxOVaAp2lGwEdzZ0J+cRbbeevQj1UQ==, tarball: https://registry.npmjs.org/@types/resolve/-/resolve-1.20.6.tgz}
|
||||||
|
|
||||||
@@ -4402,6 +4408,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-xe9vQb5kReirPUxgQrXA3ihgbCqssmTiM7cOZ+Gzu+VeGWgpV98lLZvp0dl4yriyAePcewxGUs9UpKD8PET9KQ==, tarball: https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.21.2.tgz}
|
resolution: {integrity: sha512-xe9vQb5kReirPUxgQrXA3ihgbCqssmTiM7cOZ+Gzu+VeGWgpV98lLZvp0dl4yriyAePcewxGUs9UpKD8PET9KQ==, tarball: https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.21.2.tgz}
|
||||||
engines: {node: '>=10.13.0'}
|
engines: {node: '>=10.13.0'}
|
||||||
|
|
||||||
|
enhanced-resolve@5.22.0:
|
||||||
|
resolution: {integrity: sha512-xYcDWrpELkFzz9SpZ3PlI6Eu6eD93Yf0WLDRxikGhWJ3MAir2SNZTIVCVZqZ/NUyx8AdMc2gT9C0gPiw18kG+A==, tarball: https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.22.0.tgz}
|
||||||
|
engines: {node: '>=10.13.0'}
|
||||||
|
|
||||||
enhanced-resolve@5.9.3:
|
enhanced-resolve@5.9.3:
|
||||||
resolution: {integrity: sha512-Bq9VSor+kjvW3f9/MiiR4eE3XYgOl7/rS8lnSxbRbF3kS0B2r+Y9w5krBWxZgDxASVZbdYrn5wT4j/Wb0J9qow==, tarball: https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.9.3.tgz}
|
resolution: {integrity: sha512-Bq9VSor+kjvW3f9/MiiR4eE3XYgOl7/rS8lnSxbRbF3kS0B2r+Y9w5krBWxZgDxASVZbdYrn5wT4j/Wb0J9qow==, tarball: https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.9.3.tgz}
|
||||||
engines: {node: '>=10.13.0'}
|
engines: {node: '>=10.13.0'}
|
||||||
@@ -8605,6 +8615,11 @@ packages:
|
|||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
terser@5.48.0:
|
||||||
|
resolution: {integrity: sha512-J/9An6vs9Us6wKRriSFXBWdRZapREHqFzdNUKk0pmu804EMR6dr6winwo7e5JDxN4xahxQsuysyYFwlwj4XN/Q==, tarball: https://registry.npmjs.org/terser/-/terser-5.48.0.tgz}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
test-exclude@6.0.0:
|
test-exclude@6.0.0:
|
||||||
resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==, tarball: https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz}
|
resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==, tarball: https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
@@ -8818,6 +8833,9 @@ packages:
|
|||||||
undici-types@7.19.2:
|
undici-types@7.19.2:
|
||||||
resolution: {integrity: sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==, tarball: https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz}
|
resolution: {integrity: sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==, tarball: https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz}
|
||||||
|
|
||||||
|
undici-types@7.24.6:
|
||||||
|
resolution: {integrity: sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==, tarball: https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz}
|
||||||
|
|
||||||
unfetch@5.0.0:
|
unfetch@5.0.0:
|
||||||
resolution: {integrity: sha512-3xM2c89siXg0nHvlmYsQ2zkLASvVMBisZm5lF3gFDqfF2xonNStDJyMpvaOBe0a1Edxmqrf2E0HBdmy9QyZaeg==, tarball: https://registry.npmjs.org/unfetch/-/unfetch-5.0.0.tgz}
|
resolution: {integrity: sha512-3xM2c89siXg0nHvlmYsQ2zkLASvVMBisZm5lF3gFDqfF2xonNStDJyMpvaOBe0a1Edxmqrf2E0HBdmy9QyZaeg==, tarball: https://registry.npmjs.org/unfetch/-/unfetch-5.0.0.tgz}
|
||||||
|
|
||||||
@@ -9050,8 +9068,8 @@ packages:
|
|||||||
engines: {node: '>= 10.13.0'}
|
engines: {node: '>= 10.13.0'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
webpack-sources@3.4.1:
|
webpack-sources@3.5.0:
|
||||||
resolution: {integrity: sha512-eACpxRN02yaawnt+uUNIF7Qje6A9zArxBbcAJjK1PK3S9Ycg5jIuJ8pW4q8EMnwNZCEGltcjkRx1QzOxOkKD8A==, tarball: https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.4.1.tgz}
|
resolution: {integrity: sha512-HPuy+uuoTCaaoEoI1LQ3JN9+vrPBvEesnnX1jADHy728cHSMlq4wUc4afYqahq2B1mhQVZxCXOkNTnXltr+2vQ==, tarball: https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.5.0.tgz}
|
||||||
engines: {node: '>=10.13.0'}
|
engines: {node: '>=10.13.0'}
|
||||||
|
|
||||||
webpack@5.106.2:
|
webpack@5.106.2:
|
||||||
@@ -10790,7 +10808,7 @@ snapshots:
|
|||||||
|
|
||||||
'@formatjs/intl-utils@2.3.0': {}
|
'@formatjs/intl-utils@2.3.0': {}
|
||||||
|
|
||||||
'@gpustack/core-ui@1.0.10(czdvzceysqw7iv6pct2ucnb23e)':
|
'@gpustack/core-ui@1.0.16(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)
|
||||||
@@ -11811,7 +11829,7 @@ snapshots:
|
|||||||
|
|
||||||
'@types/history@5.0.0':
|
'@types/history@5.0.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
history: 5.3.0
|
history: 4.10.1
|
||||||
|
|
||||||
'@types/hoist-non-react-statics@3.3.7(@types/react@18.3.28)':
|
'@types/hoist-non-react-statics@3.3.7(@types/react@18.3.28)':
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -11865,6 +11883,10 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
undici-types: 7.19.2
|
undici-types: 7.19.2
|
||||||
|
|
||||||
|
'@types/node@25.9.1':
|
||||||
|
dependencies:
|
||||||
|
undici-types: 7.24.6
|
||||||
|
|
||||||
'@types/normalize-package-data@2.4.4': {}
|
'@types/normalize-package-data@2.4.4': {}
|
||||||
|
|
||||||
'@types/parse-json@4.0.2': {}
|
'@types/parse-json@4.0.2': {}
|
||||||
@@ -11885,26 +11907,31 @@ snapshots:
|
|||||||
'@types/react-router-dom@4.3.5':
|
'@types/react-router-dom@4.3.5':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/history': 5.0.0
|
'@types/history': 5.0.0
|
||||||
'@types/react': 18.3.28
|
'@types/react': 18.3.29
|
||||||
'@types/react-router': 5.1.20
|
'@types/react-router': 5.1.20
|
||||||
|
|
||||||
'@types/react-router-redux@5.0.27':
|
'@types/react-router-redux@5.0.27':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/history': 4.7.11
|
'@types/history': 4.7.11
|
||||||
'@types/react': 18.3.28
|
'@types/react': 18.3.29
|
||||||
'@types/react-router': 5.1.20
|
'@types/react-router': 5.1.20
|
||||||
redux: 4.2.1
|
redux: 3.7.2
|
||||||
|
|
||||||
'@types/react-router@5.1.20':
|
'@types/react-router@5.1.20':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/history': 4.7.11
|
'@types/history': 4.7.11
|
||||||
'@types/react': 18.3.28
|
'@types/react': 18.3.29
|
||||||
|
|
||||||
'@types/react@18.3.28':
|
'@types/react@18.3.28':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/prop-types': 15.7.15
|
'@types/prop-types': 15.7.15
|
||||||
csstype: 3.2.3
|
csstype: 3.2.3
|
||||||
|
|
||||||
|
'@types/react@18.3.29':
|
||||||
|
dependencies:
|
||||||
|
'@types/prop-types': 15.7.15
|
||||||
|
csstype: 3.2.3
|
||||||
|
|
||||||
'@types/resolve@1.20.6': {}
|
'@types/resolve@1.20.6': {}
|
||||||
|
|
||||||
'@types/semver@7.7.1': {}
|
'@types/semver@7.7.1': {}
|
||||||
@@ -12287,18 +12314,18 @@ snapshots:
|
|||||||
- webpack-hot-middleware
|
- webpack-hot-middleware
|
||||||
- webpack-plugin-serve
|
- webpack-plugin-serve
|
||||||
|
|
||||||
'@umijs/bundler-vite@4.6.51(@types/node@25.6.2)(lightningcss@1.22.1)(postcss@8.5.14)(rollup@3.30.0)(sass@1.54.0)(terser@5.47.1)':
|
'@umijs/bundler-vite@4.6.51(@types/node@25.6.2)(lightningcss@1.22.1)(postcss@8.5.14)(rollup@3.30.0)(sass@1.54.0)(terser@5.48.0)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@svgr/core': 6.5.1
|
'@svgr/core': 6.5.1
|
||||||
'@umijs/bundler-utils': 4.6.51
|
'@umijs/bundler-utils': 4.6.51
|
||||||
'@umijs/utils': 4.6.51
|
'@umijs/utils': 4.6.51
|
||||||
'@vitejs/plugin-react': 4.0.0(vite@4.5.2(@types/node@25.6.2)(less@4.1.3)(lightningcss@1.22.1)(sass@1.54.0)(terser@5.47.1))
|
'@vitejs/plugin-react': 4.0.0(vite@4.5.2(@types/node@25.6.2)(less@4.1.3)(lightningcss@1.22.1)(sass@1.54.0)(terser@5.48.0))
|
||||||
core-js: 3.34.0
|
core-js: 3.34.0
|
||||||
less: 4.1.3
|
less: 4.1.3
|
||||||
postcss-preset-env: 7.5.0(postcss@8.5.14)
|
postcss-preset-env: 7.5.0(postcss@8.5.14)
|
||||||
rollup-plugin-visualizer: 5.9.0(rollup@3.30.0)
|
rollup-plugin-visualizer: 5.9.0(rollup@3.30.0)
|
||||||
systemjs: 6.15.1
|
systemjs: 6.15.1
|
||||||
vite: 4.5.2(@types/node@25.6.2)(less@4.1.3)(lightningcss@1.22.1)(sass@1.54.0)(terser@5.47.1)
|
vite: 4.5.2(@types/node@25.6.2)(less@4.1.3)(lightningcss@1.22.1)(sass@1.54.0)(terser@5.48.0)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- '@types/node'
|
- '@types/node'
|
||||||
- lightningcss
|
- lightningcss
|
||||||
@@ -12526,14 +12553,14 @@ snapshots:
|
|||||||
- supports-color
|
- supports-color
|
||||||
- typescript
|
- typescript
|
||||||
|
|
||||||
'@umijs/max@4.6.51(@babel/core@7.29.0)(@types/node@25.6.2)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(lightningcss@1.22.1)(prettier@3.8.3)(rc-field-form@2.7.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)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.29.0)(react@18.3.1))(terser@5.47.1)(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14))':
|
'@umijs/max@4.6.51(@babel/core@7.29.0)(@types/node@25.6.2)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(lightningcss@1.22.1)(prettier@3.8.3)(rc-field-form@2.7.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)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.29.0)(react@18.3.1))(terser@5.48.0)(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@umijs/lint': 4.6.51(eslint@8.35.0)(stylelint@14.8.2)(typescript@5.9.3)
|
'@umijs/lint': 4.6.51(eslint@8.35.0)(stylelint@14.8.2)(typescript@5.9.3)
|
||||||
'@umijs/plugins': 4.6.51(@babel/core@7.29.0)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(antd@4.24.16(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(rc-field-form@2.7.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)
|
'@umijs/plugins': 4.6.51(@babel/core@7.29.0)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(antd@4.24.16(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(rc-field-form@2.7.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)
|
||||||
antd: 4.24.16(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
antd: 4.24.16(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||||
eslint: 8.35.0
|
eslint: 8.35.0
|
||||||
stylelint: 14.8.2
|
stylelint: 14.8.2
|
||||||
umi: 4.6.51(@babel/core@7.29.0)(@types/node@25.6.2)(@types/react@18.3.28)(eslint@8.35.0)(lightningcss@1.22.1)(prettier@3.8.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.29.0)(react@18.3.1))(stylelint@14.8.2)(terser@5.47.1)(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14))
|
umi: 4.6.51(@babel/core@7.29.0)(@types/node@25.6.2)(@types/react@18.3.28)(eslint@8.35.0)(lightningcss@1.22.1)(prettier@3.8.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.29.0)(react@18.3.1))(stylelint@14.8.2)(terser@5.48.0)(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14))
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- '@babel/core'
|
- '@babel/core'
|
||||||
- '@rspack/core'
|
- '@rspack/core'
|
||||||
@@ -12750,7 +12777,7 @@ snapshots:
|
|||||||
- react-native
|
- react-native
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
'@umijs/preset-umi@4.6.51(@types/node@25.6.2)(@types/react@18.3.28)(lightningcss@1.22.1)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.29.0)(react@18.3.1))(terser@5.47.1)(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14))':
|
'@umijs/preset-umi@4.6.51(@types/node@25.6.2)(@types/react@18.3.28)(lightningcss@1.22.1)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.29.0)(react@18.3.1))(terser@5.48.0)(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@iconify/utils': 2.1.1
|
'@iconify/utils': 2.1.1
|
||||||
'@stagewise/toolbar': 0.6.2
|
'@stagewise/toolbar': 0.6.2
|
||||||
@@ -12761,7 +12788,7 @@ snapshots:
|
|||||||
'@umijs/bundler-mako': 0.11.10(postcss@8.5.14)(sass@1.54.0)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14))
|
'@umijs/bundler-mako': 0.11.10(postcss@8.5.14)(sass@1.54.0)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14))
|
||||||
'@umijs/bundler-utils': 4.6.51
|
'@umijs/bundler-utils': 4.6.51
|
||||||
'@umijs/bundler-utoopack': 4.6.51(styled-jsx@5.1.7(@babel/core@7.29.0)(react@18.3.1))(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14))
|
'@umijs/bundler-utoopack': 4.6.51(styled-jsx@5.1.7(@babel/core@7.29.0)(react@18.3.1))(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14))
|
||||||
'@umijs/bundler-vite': 4.6.51(@types/node@25.6.2)(lightningcss@1.22.1)(postcss@8.5.14)(rollup@3.30.0)(sass@1.54.0)(terser@5.47.1)
|
'@umijs/bundler-vite': 4.6.51(@types/node@25.6.2)(lightningcss@1.22.1)(postcss@8.5.14)(rollup@3.30.0)(sass@1.54.0)(terser@5.48.0)
|
||||||
'@umijs/bundler-webpack': 4.6.51(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14))
|
'@umijs/bundler-webpack': 4.6.51(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14))
|
||||||
'@umijs/core': 4.6.51
|
'@umijs/core': 4.6.51
|
||||||
'@umijs/did-you-know': 1.0.4
|
'@umijs/did-you-know': 1.0.4
|
||||||
@@ -12843,13 +12870,13 @@ snapshots:
|
|||||||
react-helmet-async: 1.3.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
react-helmet-async: 1.3.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||||
react-router-dom: 6.3.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
react-router-dom: 6.3.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||||
|
|
||||||
'@umijs/request-record@1.1.4(umi@4.6.51(@babel/core@7.29.0)(@types/node@25.6.2)(@types/react@18.3.28)(eslint@9.39.4(jiti@2.7.0))(lightningcss@1.22.1)(prettier@3.8.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.29.0)(react@18.3.1))(stylelint@14.8.2)(terser@5.47.1)(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14)))':
|
'@umijs/request-record@1.1.4(umi@4.6.51(@babel/core@7.29.0)(@types/node@25.6.2)(@types/react@18.3.28)(eslint@9.39.4(jiti@2.7.0))(lightningcss@1.22.1)(prettier@3.8.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.29.0)(react@18.3.1))(stylelint@14.8.2)(terser@5.48.0)(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14)))':
|
||||||
dependencies:
|
dependencies:
|
||||||
chokidar: 3.6.0
|
chokidar: 3.6.0
|
||||||
express: 4.22.1
|
express: 4.22.1
|
||||||
lodash: 4.18.1
|
lodash: 4.18.1
|
||||||
prettier: 2.8.8
|
prettier: 2.8.8
|
||||||
umi: 4.6.51(@babel/core@7.29.0)(@types/node@25.6.2)(@types/react@18.3.28)(eslint@9.39.4(jiti@2.7.0))(lightningcss@1.22.1)(prettier@3.8.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.29.0)(react@18.3.1))(stylelint@14.8.2)(terser@5.47.1)(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14))
|
umi: 4.6.51(@babel/core@7.29.0)(@types/node@25.6.2)(@types/react@18.3.28)(eslint@9.39.4(jiti@2.7.0))(lightningcss@1.22.1)(prettier@3.8.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.29.0)(react@18.3.1))(stylelint@14.8.2)(terser@5.48.0)(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14))
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
@@ -13027,13 +13054,13 @@ snapshots:
|
|||||||
- supports-color
|
- supports-color
|
||||||
- utf-8-validate
|
- utf-8-validate
|
||||||
|
|
||||||
'@vitejs/plugin-react@4.0.0(vite@4.5.2(@types/node@25.6.2)(less@4.1.3)(lightningcss@1.22.1)(sass@1.54.0)(terser@5.47.1))':
|
'@vitejs/plugin-react@4.0.0(vite@4.5.2(@types/node@25.6.2)(less@4.1.3)(lightningcss@1.22.1)(sass@1.54.0)(terser@5.48.0))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/core': 7.29.0
|
'@babel/core': 7.29.0
|
||||||
'@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.29.0)
|
'@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.29.0)
|
||||||
'@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.29.0)
|
'@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.29.0)
|
||||||
react-refresh: 0.14.2
|
react-refresh: 0.14.2
|
||||||
vite: 4.5.2(@types/node@25.6.2)(less@4.1.3)(lightningcss@1.22.1)(sass@1.54.0)(terser@5.47.1)
|
vite: 4.5.2(@types/node@25.6.2)(less@4.1.3)(lightningcss@1.22.1)(sass@1.54.0)(terser@5.48.0)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
@@ -14645,6 +14672,11 @@ snapshots:
|
|||||||
graceful-fs: 4.2.11
|
graceful-fs: 4.2.11
|
||||||
tapable: 2.3.3
|
tapable: 2.3.3
|
||||||
|
|
||||||
|
enhanced-resolve@5.22.0:
|
||||||
|
dependencies:
|
||||||
|
graceful-fs: 4.2.11
|
||||||
|
tapable: 2.3.3
|
||||||
|
|
||||||
enhanced-resolve@5.9.3:
|
enhanced-resolve@5.9.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
graceful-fs: 4.2.11
|
graceful-fs: 4.2.11
|
||||||
@@ -16358,7 +16390,7 @@ snapshots:
|
|||||||
|
|
||||||
jest-worker@27.5.1:
|
jest-worker@27.5.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 25.6.2
|
'@types/node': 25.9.1
|
||||||
merge-stream: 2.0.0
|
merge-stream: 2.0.0
|
||||||
supports-color: 8.1.1
|
supports-color: 8.1.1
|
||||||
|
|
||||||
@@ -19928,7 +19960,7 @@ snapshots:
|
|||||||
'@jridgewell/trace-mapping': 0.3.31
|
'@jridgewell/trace-mapping': 0.3.31
|
||||||
jest-worker: 27.5.1
|
jest-worker: 27.5.1
|
||||||
schema-utils: 4.3.3
|
schema-utils: 4.3.3
|
||||||
terser: 5.47.1
|
terser: 5.48.0
|
||||||
webpack: 5.106.2(lightningcss@1.22.1)(postcss@8.5.14)
|
webpack: 5.106.2(lightningcss@1.22.1)(postcss@8.5.14)
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
lightningcss: 1.22.1
|
lightningcss: 1.22.1
|
||||||
@@ -19941,6 +19973,13 @@ snapshots:
|
|||||||
commander: 2.20.3
|
commander: 2.20.3
|
||||||
source-map-support: 0.5.21
|
source-map-support: 0.5.21
|
||||||
|
|
||||||
|
terser@5.48.0:
|
||||||
|
dependencies:
|
||||||
|
'@jridgewell/source-map': 0.3.11
|
||||||
|
acorn: 8.16.0
|
||||||
|
commander: 2.20.3
|
||||||
|
source-map-support: 0.5.21
|
||||||
|
|
||||||
test-exclude@6.0.0:
|
test-exclude@6.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@istanbuljs/schema': 0.1.6
|
'@istanbuljs/schema': 0.1.6
|
||||||
@@ -20125,12 +20164,12 @@ snapshots:
|
|||||||
|
|
||||||
ua-parser-js@0.7.41: {}
|
ua-parser-js@0.7.41: {}
|
||||||
|
|
||||||
umi-presets-pro@2.0.3(@babel/core@7.29.0)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(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))(chokidar@3.6.0)(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(encoding@0.1.13)(rc-field-form@2.7.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)(typescript@5.9.3)(umi@4.6.51(@babel/core@7.29.0)(@types/node@25.6.2)(@types/react@18.3.28)(eslint@9.39.4(jiti@2.7.0))(lightningcss@1.22.1)(prettier@3.8.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.29.0)(react@18.3.1))(stylelint@14.8.2)(terser@5.47.1)(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14))):
|
umi-presets-pro@2.0.3(@babel/core@7.29.0)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(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))(chokidar@3.6.0)(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(encoding@0.1.13)(rc-field-form@2.7.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)(typescript@5.9.3)(umi@4.6.51(@babel/core@7.29.0)(@types/node@25.6.2)(@types/react@18.3.28)(eslint@9.39.4(jiti@2.7.0))(lightningcss@1.22.1)(prettier@3.8.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.29.0)(react@18.3.1))(stylelint@14.8.2)(terser@5.48.0)(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14))):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@alita/plugins': 3.5.5(@babel/core@7.29.0)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(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))(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(rc-field-form@2.7.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)
|
'@alita/plugins': 3.5.5(@babel/core@7.29.0)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(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))(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(rc-field-form@2.7.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)
|
||||||
'@umijs/max-plugin-openapi': 2.0.3(chokidar@3.6.0)(encoding@0.1.13)(typescript@5.9.3)
|
'@umijs/max-plugin-openapi': 2.0.3(chokidar@3.6.0)(encoding@0.1.13)(typescript@5.9.3)
|
||||||
'@umijs/plugins': 4.6.51(@babel/core@7.29.0)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(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))(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(rc-field-form@2.7.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)
|
'@umijs/plugins': 4.6.51(@babel/core@7.29.0)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(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))(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(rc-field-form@2.7.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)
|
||||||
'@umijs/request-record': 1.1.4(umi@4.6.51(@babel/core@7.29.0)(@types/node@25.6.2)(@types/react@18.3.28)(eslint@9.39.4(jiti@2.7.0))(lightningcss@1.22.1)(prettier@3.8.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.29.0)(react@18.3.1))(stylelint@14.8.2)(terser@5.47.1)(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14)))
|
'@umijs/request-record': 1.1.4(umi@4.6.51(@babel/core@7.29.0)(@types/node@25.6.2)(@types/react@18.3.28)(eslint@9.39.4(jiti@2.7.0))(lightningcss@1.22.1)(prettier@3.8.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.29.0)(react@18.3.1))(stylelint@14.8.2)(terser@5.48.0)(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14)))
|
||||||
swagger-ui-dist: 4.19.1
|
swagger-ui-dist: 4.19.1
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- '@babel/core'
|
- '@babel/core'
|
||||||
@@ -20154,14 +20193,14 @@ snapshots:
|
|||||||
isomorphic-fetch: 2.2.1
|
isomorphic-fetch: 2.2.1
|
||||||
qs: 6.15.1
|
qs: 6.15.1
|
||||||
|
|
||||||
umi@4.6.51(@babel/core@7.29.0)(@types/node@25.6.2)(@types/react@18.3.28)(eslint@8.35.0)(lightningcss@1.22.1)(prettier@3.8.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.29.0)(react@18.3.1))(stylelint@14.8.2)(terser@5.47.1)(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14)):
|
umi@4.6.51(@babel/core@7.29.0)(@types/node@25.6.2)(@types/react@18.3.28)(eslint@8.35.0)(lightningcss@1.22.1)(prettier@3.8.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.29.0)(react@18.3.1))(stylelint@14.8.2)(terser@5.48.0)(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14)):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/runtime': 7.23.6
|
'@babel/runtime': 7.23.6
|
||||||
'@umijs/bundler-utils': 4.6.51
|
'@umijs/bundler-utils': 4.6.51
|
||||||
'@umijs/bundler-webpack': 4.6.51(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14))
|
'@umijs/bundler-webpack': 4.6.51(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14))
|
||||||
'@umijs/core': 4.6.51
|
'@umijs/core': 4.6.51
|
||||||
'@umijs/lint': 4.6.51(eslint@8.35.0)(stylelint@14.8.2)(typescript@5.9.3)
|
'@umijs/lint': 4.6.51(eslint@8.35.0)(stylelint@14.8.2)(typescript@5.9.3)
|
||||||
'@umijs/preset-umi': 4.6.51(@types/node@25.6.2)(@types/react@18.3.28)(lightningcss@1.22.1)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.29.0)(react@18.3.1))(terser@5.47.1)(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14))
|
'@umijs/preset-umi': 4.6.51(@types/node@25.6.2)(@types/react@18.3.28)(lightningcss@1.22.1)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.29.0)(react@18.3.1))(terser@5.48.0)(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14))
|
||||||
'@umijs/renderer-react': 4.6.51(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
'@umijs/renderer-react': 4.6.51(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||||
'@umijs/server': 4.6.51
|
'@umijs/server': 4.6.51
|
||||||
'@umijs/test': 4.6.51(@babel/core@7.29.0)
|
'@umijs/test': 4.6.51(@babel/core@7.29.0)
|
||||||
@@ -20208,14 +20247,14 @@ snapshots:
|
|||||||
- webpack-hot-middleware
|
- webpack-hot-middleware
|
||||||
- webpack-plugin-serve
|
- webpack-plugin-serve
|
||||||
|
|
||||||
umi@4.6.51(@babel/core@7.29.0)(@types/node@25.6.2)(@types/react@18.3.28)(eslint@9.39.4(jiti@2.7.0))(lightningcss@1.22.1)(prettier@3.8.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.29.0)(react@18.3.1))(stylelint@14.8.2)(terser@5.47.1)(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14)):
|
umi@4.6.51(@babel/core@7.29.0)(@types/node@25.6.2)(@types/react@18.3.28)(eslint@9.39.4(jiti@2.7.0))(lightningcss@1.22.1)(prettier@3.8.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.29.0)(react@18.3.1))(stylelint@14.8.2)(terser@5.48.0)(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14)):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/runtime': 7.23.6
|
'@babel/runtime': 7.23.6
|
||||||
'@umijs/bundler-utils': 4.6.51
|
'@umijs/bundler-utils': 4.6.51
|
||||||
'@umijs/bundler-webpack': 4.6.51(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14))
|
'@umijs/bundler-webpack': 4.6.51(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14))
|
||||||
'@umijs/core': 4.6.51
|
'@umijs/core': 4.6.51
|
||||||
'@umijs/lint': 4.6.51(eslint@9.39.4(jiti@2.7.0))(stylelint@14.8.2)(typescript@5.9.3)
|
'@umijs/lint': 4.6.51(eslint@9.39.4(jiti@2.7.0))(stylelint@14.8.2)(typescript@5.9.3)
|
||||||
'@umijs/preset-umi': 4.6.51(@types/node@25.6.2)(@types/react@18.3.28)(lightningcss@1.22.1)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.29.0)(react@18.3.1))(terser@5.47.1)(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14))
|
'@umijs/preset-umi': 4.6.51(@types/node@25.6.2)(@types/react@18.3.28)(lightningcss@1.22.1)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.29.0)(react@18.3.1))(terser@5.48.0)(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14))
|
||||||
'@umijs/renderer-react': 4.6.51(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
'@umijs/renderer-react': 4.6.51(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||||
'@umijs/server': 4.6.51
|
'@umijs/server': 4.6.51
|
||||||
'@umijs/test': 4.6.51(@babel/core@7.29.0)
|
'@umijs/test': 4.6.51(@babel/core@7.29.0)
|
||||||
@@ -20273,6 +20312,8 @@ snapshots:
|
|||||||
|
|
||||||
undici-types@7.19.2: {}
|
undici-types@7.19.2: {}
|
||||||
|
|
||||||
|
undici-types@7.24.6: {}
|
||||||
|
|
||||||
unfetch@5.0.0: {}
|
unfetch@5.0.0: {}
|
||||||
|
|
||||||
unified@11.0.5:
|
unified@11.0.5:
|
||||||
@@ -20457,7 +20498,7 @@ snapshots:
|
|||||||
'@types/unist': 3.0.3
|
'@types/unist': 3.0.3
|
||||||
vfile-message: 4.0.3
|
vfile-message: 4.0.3
|
||||||
|
|
||||||
vite@4.5.2(@types/node@25.6.2)(less@4.1.3)(lightningcss@1.22.1)(sass@1.54.0)(terser@5.47.1):
|
vite@4.5.2(@types/node@25.6.2)(less@4.1.3)(lightningcss@1.22.1)(sass@1.54.0)(terser@5.48.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
esbuild: 0.18.20
|
esbuild: 0.18.20
|
||||||
postcss: 8.5.14
|
postcss: 8.5.14
|
||||||
@@ -20468,7 +20509,7 @@ snapshots:
|
|||||||
less: 4.1.3
|
less: 4.1.3
|
||||||
lightningcss: 1.22.1
|
lightningcss: 1.22.1
|
||||||
sass: 1.54.0
|
sass: 1.54.0
|
||||||
terser: 5.47.1
|
terser: 5.48.0
|
||||||
|
|
||||||
vm-browserify@1.1.2: {}
|
vm-browserify@1.1.2: {}
|
||||||
|
|
||||||
@@ -20530,7 +20571,7 @@ snapshots:
|
|||||||
- bufferutil
|
- bufferutil
|
||||||
- utf-8-validate
|
- utf-8-validate
|
||||||
|
|
||||||
webpack-sources@3.4.1: {}
|
webpack-sources@3.5.0: {}
|
||||||
|
|
||||||
webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14):
|
webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14):
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -20544,7 +20585,7 @@ snapshots:
|
|||||||
acorn-import-phases: 1.0.4(acorn@8.16.0)
|
acorn-import-phases: 1.0.4(acorn@8.16.0)
|
||||||
browserslist: 4.28.2
|
browserslist: 4.28.2
|
||||||
chrome-trace-event: 1.0.4
|
chrome-trace-event: 1.0.4
|
||||||
enhanced-resolve: 5.21.2
|
enhanced-resolve: 5.22.0
|
||||||
es-module-lexer: 2.1.0
|
es-module-lexer: 2.1.0
|
||||||
eslint-scope: 5.1.1
|
eslint-scope: 5.1.1
|
||||||
events: 3.3.0
|
events: 3.3.0
|
||||||
@@ -20557,7 +20598,7 @@ snapshots:
|
|||||||
tapable: 2.3.3
|
tapable: 2.3.3
|
||||||
terser-webpack-plugin: 5.6.0(lightningcss@1.22.1)(postcss@8.5.14)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14))
|
terser-webpack-plugin: 5.6.0(lightningcss@1.22.1)(postcss@8.5.14)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14))
|
||||||
watchpack: 2.5.1
|
watchpack: 2.5.1
|
||||||
webpack-sources: 3.4.1
|
webpack-sources: 3.5.0
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- '@minify-html/node'
|
- '@minify-html/node'
|
||||||
- '@swc/core'
|
- '@swc/core'
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 96 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 5.2 KiB |
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 286 64"><g><g><defs><path id="SVGID_1_" d="M47.5 17.6L25 4.8v52.6l9-5.2V37.4l6.8 3.9-.1-10.1-6.7-3.9v-5.9l13.5 7.9z"/></defs><clipPath id="SVGID_2_"><use xlink:href="#SVGID_1_" overflow="visible"/></clipPath><g clip-path="url(#SVGID_2_)"><linearGradient id="SVGID_3_" gradientUnits="userSpaceOnUse" x1="-1.6" y1="335.05" x2="53.6" y2="335.05" gradientTransform="translate(0 -304)"><stop offset="0" stop-color="#ff6f00"/><stop offset="1" stop-color="#ffa800"/></linearGradient><path d="M-1.6 4.6h55.2v52.9H-1.6V4.6z" fill="url(#SVGID_3_)"/></g></g></g><g><g><defs><path id="SVGID_4_" d="M.5 17.6L23 4.8v52.6l-9-5.2V21.4L.5 29.3z"/></defs><clipPath id="SVGID_5_"><use xlink:href="#SVGID_4_" overflow="visible"/></clipPath><g clip-path="url(#SVGID_5_)"><linearGradient id="SVGID_6_" gradientUnits="userSpaceOnUse" x1="-1.9" y1="335.05" x2="53.3" y2="335.05" gradientTransform="translate(0 -304)"><stop offset="0" stop-color="#ff6f00"/><stop offset="1" stop-color="#ffa800"/></linearGradient><path d="M-1.9 4.6h55.2v52.9H-1.9V4.6z" fill="url(#SVGID_6_)"/></g></g></g><path style="fill:#425066" d="M88.2 21.1h-10v27.7h-5.6V21.1h-10v-4.5h25.6v4.5z"/><path style="fill:#425066" d="M94.9 49.2c-3.4 0-6.2-1.1-8.3-3.2-2.1-2.1-3.2-5-3.2-8.6v-.7c0-2.2.4-4.4 1.4-6.4.9-1.8 2.2-3.3 3.9-4.4 1.7-1.1 3.6-1.6 5.6-1.6 3.3 0 5.8 1 7.6 3.1s2.7 5 2.7 8.8v2.2H88.9c.1 1.8.8 3.4 2 4.7 1.2 1.2 2.7 1.8 4.4 1.7 2.4.1 4.6-1.1 6-3l2.9 2.8c-1 1.4-2.3 2.6-3.8 3.3-1.8 1-3.6 1.4-5.5 1.3zm-.6-20.5c-1.4-.1-2.7.5-3.6 1.5-1 1.2-1.6 2.7-1.7 4.3h10.3v-.4c-.1-1.8-.6-3.2-1.4-4.1-1-.8-2.3-1.4-3.6-1.3zm19.4-3.9l.2 2.8c1.7-2.1 4.3-3.3 7-3.2 5 0 7.5 2.9 7.6 8.6v15.8h-5.4V33.3c0-1.5-.3-2.6-1-3.4-.7-.7-1.7-1.1-3.2-1.1-2.1-.1-4 1.1-4.9 2.9v17h-5.4v-24l5.1.1zm32.2 17.5c0-.9-.4-1.7-1.2-2.2-1.2-.7-2.6-1.1-3.9-1.3-1.6-.3-3.1-.8-4.6-1.5-2.7-1.3-4-3.2-4-5.6 0-2 1-4 2.6-5.2 1.7-1.4 4-2.1 6.6-2.1 2.9 0 5.2.7 6.9 2.1 1.7 1.3 2.7 3.4 2.6 5.5h-5.4c0-1-.4-1.9-1.2-2.6-.9-.7-1.9-1.1-3.1-1-1 0-2 .2-2.9.8-.7.5-1.1 1.3-1.1 2.2 0 .8.4 1.5 1 1.9.7.5 2.1.9 4.2 1.4 1.7.3 3.4.9 5 1.7 1.1.5 2 1.3 2.7 2.3.6 1 .9 2.1.9 3.3 0 2.1-1 4-2.7 5.2-1.8 1.3-4.1 2-7 2-1.8 0-3.6-.3-5.2-1.1-1.4-.6-2.7-1.6-3.6-2.9-.8-1.2-1.3-2.6-1.3-4h5.2c0 1.1.5 2.2 1.4 2.9 1 .7 2.3 1.1 3.5 1 1.4 0 2.5-.3 3.2-.8 1-.4 1.4-1.2 1.4-2zm8.1-5.7c0-2.2.4-4.4 1.4-6.3.9-1.8 2.2-3.3 3.9-4.3 1.8-1 3.8-1.6 5.8-1.5 3.2 0 5.9 1 7.9 3.1s3.1 4.8 3.3 8.3v1.3c0 2.2-.4 4.3-1.4 6.3-.8 1.8-2.2 3.3-3.9 4.3-1.8 1-3.8 1.6-5.9 1.5-3.4 0-6.1-1.1-8.1-3.4-2-2.2-3-5.2-3.1-9l.1-.3zm5.3.5c0 2.5.5 4.4 1.5 5.8 1.8 2.3 5.1 2.8 7.5 1 .4-.3.7-.6 1-1 1-1.4 1.5-3.5 1.5-6.2 0-2.4-.5-4.3-1.6-5.8-1.7-2.3-5-2.8-7.4-1.1-.4.3-.8.7-1.1 1-.8 1.4-1.4 3.5-1.4 6.3zm33.1-7.3c-.7-.1-1.5-.2-2.2-.2-2.5 0-4.1.9-5 2.8v16.4h-5.4v-24h5.1l.1 2.7c1.3-2.1 3.1-3.1 5.4-3.1.6 0 1.3.1 1.9.3l.1 5.1zm22.5 5.3h-13v13.7h-5.6V16.6h20.5v4.5h-14.9v9.6h13v4.4zm10.7 13.7h-5.4V16.5h5.4v32.3zm3.9-12.2c0-2.2.4-4.4 1.4-6.3.9-1.8 2.2-3.3 3.9-4.3 1.8-1 3.8-1.6 5.8-1.5 3.2 0 5.9 1 7.9 3.1s3.1 4.8 3.3 8.3v1.3c0 2.2-.4 4.3-1.3 6.3-.8 1.8-2.2 3.3-3.9 4.3-1.8 1-3.8 1.6-5.9 1.5-3.4 0-6.1-1.1-8.1-3.4-2-2.2-3.1-5.2-3.1-9v-.3zm5.4.5c0 2.5.5 4.4 1.5 5.8 1 1.4 2.6 2.2 4.3 2.1 1.7.1 3.3-.7 4.2-2.1 1-1.4 1.5-3.5 1.5-6.2 0-2.4-.5-4.3-1.6-5.8-1.7-2.3-5-2.8-7.4-1.1-.4.3-.8.7-1.1 1-.9 1.4-1.4 3.5-1.4 6.3zm41.2 4.3l3.8-16.5h5.2l-6.5 24h-4.4l-5.1-16.5-5.1 16.5h-4.4l-6.6-24h5.3l3.9 16.4 4.9-16.4h4.1l4.9 16.5z"/></svg>
|
||||||
|
After Width: | Height: | Size: 3.4 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 21 KiB |
@@ -4,3 +4,7 @@ import { atom } from 'jotai';
|
|||||||
export const currentClusterAtom = atom<
|
export const currentClusterAtom = atom<
|
||||||
(Partial<ClusterListItem> & { label?: string; value?: number }) | null
|
(Partial<ClusterListItem> & { label?: string; value?: number }) | null
|
||||||
>(null);
|
>(null);
|
||||||
|
|
||||||
|
export const addSSHKeyPageAtom = atom<{ create: boolean }>({
|
||||||
|
create: false
|
||||||
|
});
|
||||||
|
|||||||
+7
-5
@@ -30,9 +30,11 @@ export const initialPasswordAtom = atomWithStorage<string>(
|
|||||||
|
|
||||||
// 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``
|
||||||
// helper — ``gpustack-{slug}`` — because the GPU-instance / storage CRDs
|
// helper — ``gpustack-{name}`` — because the GPU-instance / storage CRDs
|
||||||
// (worker.gpustack.ai/v1) are namespaced and the server-side admission
|
// (worker.gpustack.ai/v1) are namespaced and the server-side admission
|
||||||
// keys off this exact name.
|
// keys off this exact name. The identifier column on the unified
|
||||||
|
// Principal table is now ``name`` (post identity-consolidation rename
|
||||||
|
// of the legacy ``slug``); the namespace prefix is unchanged.
|
||||||
//
|
//
|
||||||
// Resolution path:
|
// Resolution path:
|
||||||
// 1. The Org the caller is currently acting under — the enterprise
|
// 1. The Org the caller is currently acting under — the enterprise
|
||||||
@@ -75,7 +77,7 @@ const getStoredCurrentOrgId = (): number | null => {
|
|||||||
// the caller's member orgs; ``allOrganizations`` is admin-only (every
|
// the caller's member orgs; ``allOrganizations`` is admin-only (every
|
||||||
// Org on the platform) so admin sessions can resolve any owner Org id.
|
// Org on the platform) so admin sessions can resolve any owner Org id.
|
||||||
// Both are checked because ``currentOrganizationId`` is null in the
|
// Both are checked because ``currentOrganizationId`` is null in the
|
||||||
// admin "All" view but a member org's slug might still cover the
|
// admin "All" view but a member org's ``name`` might still cover the
|
||||||
// cluster-owner fallback.
|
// cluster-owner fallback.
|
||||||
const ORG_CACHE_KEYS = ['organizationList', 'allOrganizations'] as const;
|
const ORG_CACHE_KEYS = ['organizationList', 'allOrganizations'] as const;
|
||||||
|
|
||||||
@@ -89,10 +91,10 @@ const lookupOrgNamespace = (id: number | null): string | null => {
|
|||||||
try {
|
try {
|
||||||
const raw = localStorage.getItem(key);
|
const raw = localStorage.getItem(key);
|
||||||
if (!raw) continue;
|
if (!raw) continue;
|
||||||
const list = JSON.parse(raw) as Array<{ id: number; slug?: string }>;
|
const list = JSON.parse(raw) as Array<{ id: number; name?: string }>;
|
||||||
if (!Array.isArray(list)) continue;
|
if (!Array.isArray(list)) continue;
|
||||||
const match = list.find((item) => String(item?.id) === target);
|
const match = list.find((item) => String(item?.id) === target);
|
||||||
if (match?.slug) return `gpustack-${match.slug}`;
|
if (match?.name) return `gpustack-${match.name}`;
|
||||||
} catch {
|
} catch {
|
||||||
// ignore malformed cache; continue checking other keys
|
// ignore malformed cache; continue checking other keys
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,12 +6,17 @@ export const clearStorageUserSettings = () => {
|
|||||||
const savedSettings = JSON.parse(
|
const savedSettings = JSON.parse(
|
||||||
localStorage.getItem('userSettings') || '{}'
|
localStorage.getItem('userSettings') || '{}'
|
||||||
);
|
);
|
||||||
|
// colorPrimary is an enterprise-wide branding setting (set by admins
|
||||||
|
// and applied by `onAppInit` from /enterprise/settings), not a per-user
|
||||||
|
// preference. Preserve it across login — otherwise the next layout
|
||||||
|
// mount triggers `atomWithStorage.onMount`, re-reads localStorage,
|
||||||
|
// and falls back to the default color until a full page refresh
|
||||||
|
// re-runs `applyEnterpriseSettings`.
|
||||||
localStorage.setItem(
|
localStorage.setItem(
|
||||||
'userSettings',
|
'userSettings',
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
...savedSettings,
|
...savedSettings,
|
||||||
hideAddResourceModal: false,
|
hideAddResourceModal: false
|
||||||
colorPrimary: undefined
|
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
+1
-1
@@ -84,4 +84,4 @@ export const modelNameReg =
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
export const validateLabelNameRegxFor63 =
|
export const validateLabelNameRegxFor63 =
|
||||||
/^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
|
/^(?![0-9])(?!.*--)[a-z](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ export default {
|
|||||||
rowSelectedBg: 'transparent',
|
rowSelectedBg: 'transparent',
|
||||||
headerSortActiveBg: 'transparent',
|
headerSortActiveBg: 'transparent',
|
||||||
headerSortHoverBg: 'transparent',
|
headerSortHoverBg: 'transparent',
|
||||||
|
headerSplitColor: '#e8e8e8',
|
||||||
headerBg: 'none'
|
headerBg: 'none'
|
||||||
},
|
},
|
||||||
Button: {
|
Button: {
|
||||||
|
|||||||
+1
-1
@@ -6,6 +6,7 @@
|
|||||||
html {
|
html {
|
||||||
--page-header-height: 56px;
|
--page-header-height: 56px;
|
||||||
--page-content-padding: 8px;
|
--page-content-padding: 8px;
|
||||||
|
--app-banner-height: 0px;
|
||||||
--color-text-light-1: rgba(255, 255, 255, 90%);
|
--color-text-light-1: rgba(255, 255, 255, 90%);
|
||||||
--color-fill-1: var(--ant-color-bg-container);
|
--color-fill-1: var(--ant-color-bg-container);
|
||||||
--color-scrollbar-thumb: rgba(193, 193, 193, 80%);
|
--color-scrollbar-thumb: rgba(193, 193, 193, 80%);
|
||||||
@@ -237,7 +238,6 @@ body {
|
|||||||
|
|
||||||
// ============== new theme style start ===============
|
// ============== new theme style start ===============
|
||||||
.ant-pro-layout {
|
.ant-pro-layout {
|
||||||
background-color: var(--color-fill-1);
|
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
||||||
.ant-pro-sider-footer {
|
.ant-pro-sider-footer {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { TABLE_SORT_DIRECTIONS } from '@/config/settings';
|
import { PaginationKey, TABLE_SORT_DIRECTIONS } from '@/config/settings';
|
||||||
import useSetChunkRequest, {
|
import useSetChunkRequest, {
|
||||||
createAxiosToken
|
createAxiosToken
|
||||||
} from '@/hooks/use-chunk-request';
|
} from '@/hooks/use-chunk-request';
|
||||||
@@ -8,7 +8,6 @@ import { handleBatchRequest } from '@/utils';
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import qs from 'query-string';
|
import qs from 'query-string';
|
||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import { PaginationKey } from '../config/settings';
|
|
||||||
import { usePaginationStatus } from './use-pagination-status';
|
import { usePaginationStatus } from './use-pagination-status';
|
||||||
import { useTableMultiSort } from './use-table-sort';
|
import { useTableMultiSort } from './use-table-sort';
|
||||||
|
|
||||||
@@ -265,6 +264,7 @@ export default function useTableFetch<T>(
|
|||||||
url: `${API}?${qs.stringify(_.pickBy(query, (val: any) => !!val))}`,
|
url: `${API}?${qs.stringify(_.pickBy(query, (val: any) => !!val))}`,
|
||||||
handler: updateHandler
|
handler: updateHandler
|
||||||
});
|
});
|
||||||
|
// eslint-disable-next-line react-hooks/purity
|
||||||
triggerAtRef.current = Date.now();
|
triggerAtRef.current = Date.now();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// ignore
|
// ignore
|
||||||
@@ -361,6 +361,9 @@ export default function useTableFetch<T>(
|
|||||||
...modalRef.current?.configuration
|
...modalRef.current?.configuration
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// remove the deleted id from selected ids in row selection
|
||||||
|
rowSelection.removeSelectedKeys([row.id]);
|
||||||
|
|
||||||
// ======== to avoid fetch data twice, because of debounceFetchData has been run =======
|
// ======== to avoid fetch data twice, because of debounceFetchData has been run =======
|
||||||
if (!updateManually) {
|
if (!updateManually) {
|
||||||
fetchData();
|
fetchData();
|
||||||
|
|||||||
+31
-20
@@ -1,6 +1,7 @@
|
|||||||
import { routeCacheAtom, setRouteCache } from '@/atoms/route-cache';
|
import { routeCacheAtom, setRouteCache } from '@/atoms/route-cache';
|
||||||
import { userAtom } from '@/atoms/user';
|
import { userAtom } from '@/atoms/user';
|
||||||
import DarkMask from '@/components/dark-mask';
|
import DarkMask from '@/components/dark-mask';
|
||||||
|
import PluginExtraFields from '@/components/plugin-extra-fields';
|
||||||
import routeCachekey from '@/config/route-cachekey';
|
import routeCachekey from '@/config/route-cachekey';
|
||||||
import { DEFAULT_ENTER_PAGE, GPUSTACK_API_BASE_URL } from '@/config/settings';
|
import { DEFAULT_ENTER_PAGE, GPUSTACK_API_BASE_URL } from '@/config/settings';
|
||||||
import { COLOR_PRIMARY } from '@/config/theme';
|
import { COLOR_PRIMARY } from '@/config/theme';
|
||||||
@@ -25,6 +26,7 @@ import {
|
|||||||
useOverlayScroller
|
useOverlayScroller
|
||||||
} from '@gpustack/core-ui';
|
} from '@gpustack/core-ui';
|
||||||
import {
|
import {
|
||||||
|
Access,
|
||||||
Outlet,
|
Outlet,
|
||||||
dropByCacheKey,
|
dropByCacheKey,
|
||||||
getAllLocales,
|
getAllLocales,
|
||||||
@@ -64,9 +66,7 @@ const NO_CONTAINER_PAGES = [
|
|||||||
'clusterCreate',
|
'clusterCreate',
|
||||||
'benchmarkDetail',
|
'benchmarkDetail',
|
||||||
'deployment',
|
'deployment',
|
||||||
'video',
|
'video'
|
||||||
'instances',
|
|
||||||
'storage'
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const CHECK_RESOURCE_PATH = [
|
const CHECK_RESOURCE_PATH = [
|
||||||
@@ -356,7 +356,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_r6z6oew38db.js',
|
iconUrl: '//at.alicdn.com/t/c/font_4613488_jmdepcs90im.js',
|
||||||
isDarkTheme: userSettings.isDarkTheme,
|
isDarkTheme: userSettings.isDarkTheme,
|
||||||
defaultColorPrimary: COLOR_PRIMARY
|
defaultColorPrimary: COLOR_PRIMARY
|
||||||
}}
|
}}
|
||||||
@@ -387,6 +387,7 @@ export default (props: any) => {
|
|||||||
writeState
|
writeState
|
||||||
}}
|
}}
|
||||||
slots={coreUISlots}
|
slots={coreUISlots}
|
||||||
|
access={{ Access, useAccess }}
|
||||||
>
|
>
|
||||||
<DarkMask></DarkMask>
|
<DarkMask></DarkMask>
|
||||||
<ProLayout
|
<ProLayout
|
||||||
@@ -422,23 +423,33 @@ export default (props: any) => {
|
|||||||
{...runtimeConfig}
|
{...runtimeConfig}
|
||||||
ErrorBoundary={ErrorBoundary}
|
ErrorBoundary={ErrorBoundary}
|
||||||
>
|
>
|
||||||
<Exception
|
<div
|
||||||
route={matchedRoute}
|
style={{
|
||||||
notFound={runtimeConfig?.notFound}
|
display: 'flex',
|
||||||
noFound={runtimeConfig?.noFound}
|
flexDirection: 'column',
|
||||||
unAccessible={runtimeConfig?.unAccessible}
|
height: '100vh',
|
||||||
noAccessible={runtimeConfig?.noAccessible}
|
overflow: 'hidden'
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{isNoContainerPage ? (
|
<PluginExtraFields name="GlobalLicenseBanner" />
|
||||||
<Outlet />
|
<Exception
|
||||||
) : (
|
route={matchedRoute}
|
||||||
<PageContainerInner>
|
notFound={runtimeConfig?.notFound}
|
||||||
<div>
|
noFound={runtimeConfig?.noFound}
|
||||||
<Outlet />
|
unAccessible={runtimeConfig?.unAccessible}
|
||||||
</div>
|
noAccessible={runtimeConfig?.noAccessible}
|
||||||
</PageContainerInner>
|
>
|
||||||
)}
|
{isNoContainerPage ? (
|
||||||
</Exception>
|
<Outlet />
|
||||||
|
) : (
|
||||||
|
<PageContainerInner>
|
||||||
|
<div>
|
||||||
|
<Outlet />
|
||||||
|
</div>
|
||||||
|
</PageContainerInner>
|
||||||
|
)}
|
||||||
|
</Exception>
|
||||||
|
</div>
|
||||||
{NoResourceModal}
|
{NoResourceModal}
|
||||||
{contextHolder}
|
{contextHolder}
|
||||||
</ProLayout>
|
</ProLayout>
|
||||||
|
|||||||
@@ -133,8 +133,7 @@ const useStyles = createStyles(({ css, token }) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const SiderMenu: React.FC<SiderMenuProps> = (props) => {
|
const SiderMenu: React.FC<SiderMenuProps> = (props) => {
|
||||||
const { menuData, collapsed, initialState } = props;
|
const { menuData, collapsed } = props;
|
||||||
const is_admin = initialState?.currentUser?.is_admin || false;
|
|
||||||
const { styles, cx } = useStyles();
|
const { styles, cx } = useStyles();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const [collapseKeys, setCollapseKeys] = useState<Set<string>>(new Set());
|
const [collapseKeys, setCollapseKeys] = useState<Set<string>>(new Set());
|
||||||
@@ -237,9 +236,9 @@ const SiderMenu: React.FC<SiderMenuProps> = (props) => {
|
|||||||
rotate={collapseKeys.has(item.key) ? -90 : 0}
|
rotate={collapseKeys.has(item.key) ? -90 : 0}
|
||||||
></CaretDownOutlined>
|
></CaretDownOutlined>
|
||||||
</span>
|
</span>
|
||||||
) : is_admin ? (
|
) : (
|
||||||
<span className={styles.line}></span>
|
<span className={styles.line}></span>
|
||||||
) : null}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className={cx(styles.menuItemGroup, {
|
className={cx(styles.menuItemGroup, {
|
||||||
|
|||||||
@@ -23,6 +23,13 @@ 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.parameterFormat.default': 'Backend Default',
|
||||||
|
'backend.form.parameterFormat.space': 'Space (--key value)',
|
||||||
|
'backend.form.parameterFormat.equal': 'Equal (--key=value)',
|
||||||
|
'backend.form.commonParameters': 'Common Parameters',
|
||||||
|
'backend.form.commonParameters.tips':
|
||||||
|
'Shown as suggestions in the backend parameters input during deployment.',
|
||||||
'backend.form.versionConfig': 'Versions Config',
|
'backend.form.versionConfig': 'Versions Config',
|
||||||
'backend.form.addParameter': 'Add Parameter',
|
'backend.form.addParameter': 'Add Parameter',
|
||||||
'backend.form.noVersion': 'No versions added',
|
'backend.form.noVersion': 'No versions added',
|
||||||
|
|||||||
@@ -42,6 +42,8 @@ export default {
|
|||||||
'Please make sure the <a href={link} target="_blank">prerequisites</a> for {label} are met before executing the following command.',
|
'Please make sure the <a href={link} target="_blank">prerequisites</a> for {label} are met before executing the following command.',
|
||||||
'clusters.create.addCommand.tips':
|
'clusters.create.addCommand.tips':
|
||||||
'On the Worker that needs to be added, run the following command to join it to the cluster.',
|
'On the Worker that needs to be added, run the following command to join it to the cluster.',
|
||||||
|
'clusters.create.addCommand.k8s.tips':
|
||||||
|
'On the Kubernetes cluster that needs to be registered, run the following command to create the Kubernetes resources and register the cluster.',
|
||||||
'clusters.create.register.tips':
|
'clusters.create.register.tips':
|
||||||
'On the Kubernetes cluster that needs to be added, run the following command to join its nodes to the cluster.',
|
'On the Kubernetes cluster that needs to be added, run the following command to join its nodes to the cluster.',
|
||||||
'cluster.create.checkEnv.tips':
|
'cluster.create.checkEnv.tips':
|
||||||
@@ -67,6 +69,9 @@ export default {
|
|||||||
'clusters.addworker.selectCluster.tips':
|
'clusters.addworker.selectCluster.tips':
|
||||||
'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.singleOnly':
|
||||||
|
'The selected vendor is not in this cluster’s GPU vendor overrides — single-select only. Pick a vendor configured in the overrides to enable multi-select.',
|
||||||
'clusters.addworker.checkEnv': 'Check Environment',
|
'clusters.addworker.checkEnv': 'Check Environment',
|
||||||
'clusters.addworker.specifyArgs': 'Specify Arguments',
|
'clusters.addworker.specifyArgs': 'Specify Arguments',
|
||||||
'clusters.addworker.runCommand': 'Run Command',
|
'clusters.addworker.runCommand': 'Run Command',
|
||||||
@@ -158,5 +163,25 @@ export default {
|
|||||||
'clusters.volume.pvc.readOnly': 'Read Only',
|
'clusters.volume.pvc.readOnly': 'Read Only',
|
||||||
'clusters.volume.configMap.name': 'ConfigMap Name',
|
'clusters.volume.configMap.name': 'ConfigMap Name',
|
||||||
'clusters.volume.configMap.optional': 'Optional',
|
'clusters.volume.configMap.optional': 'Optional',
|
||||||
'clusters.volume.add': 'Add Volume Mount'
|
'clusters.volume.add': 'Add Volume Mount',
|
||||||
|
'clusters.imageCredentials.title': 'Image Credentials',
|
||||||
|
'clusters.imageCredentials.add': 'Add Credential',
|
||||||
|
'clusters.imageCredentials.registry': 'Registry',
|
||||||
|
'clusters.imageCredentials.username': 'Username',
|
||||||
|
'clusters.imageCredentials.password': 'Password',
|
||||||
|
'clusters.nodeSelector.title': 'Node Selector',
|
||||||
|
'clusters.nodeSelector.tip':
|
||||||
|
'Pod nodeSelector applied to every worker DaemonSet — only nodes whose labels match are eligible to run the worker.',
|
||||||
|
'clusters.gpuVendorOverrides.title': 'GPU Vendor Overrides',
|
||||||
|
'clusters.gpuVendorOverrides.validate.emptySelector':
|
||||||
|
'The override for {vendor} needs at least one nodeSelector entry — an empty override would leave the multi-vendor manifest unable to target nodes for this runtime.',
|
||||||
|
'clusters.gpuVendorOverrides.validate.duplicate':
|
||||||
|
'Overrides for {v1} and {v2} use the same nodeSelector. Each vendor must scope to a distinct node set, otherwise their worker DaemonSets would compete for the same nodes.',
|
||||||
|
'clusters.gpuVendorOverrides.validate.keyConflict':
|
||||||
|
'Override for {vendor} reuses key(s) {keys} from the base Node Selector — the CPU worker would simultaneously require and forbid the key and never schedule.',
|
||||||
|
'clusters.gpuVendorOverrides.tip':
|
||||||
|
'Required when running multiple GPU vendors in one cluster. Each entry pins that vendor’s worker DaemonSet to nodes matching its nodeSelector, and the CPU worker is steered away from those nodes via a DoesNotExist node-affinity.',
|
||||||
|
'clusters.gpuVendorOverrides.add': 'Add Override',
|
||||||
|
'clusters.gpuVendorOverrides.vendor': 'GPU Vendor',
|
||||||
|
'clusters.gpuVendorOverrides.nodeSelector': 'Node Selector'
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -52,10 +52,13 @@ export default {
|
|||||||
'common.button.authorize': 'Role Authorization',
|
'common.button.authorize': 'Role Authorization',
|
||||||
'common.button.confirm': 'Confirm',
|
'common.button.confirm': 'Confirm',
|
||||||
'common.button.viewlog': 'View Logs',
|
'common.button.viewlog': 'View Logs',
|
||||||
|
'common.button.viewevent': 'View Events',
|
||||||
|
'common.button.recreate': 'Recreate',
|
||||||
'common.table.operation': 'Operations',
|
'common.table.operation': 'Operations',
|
||||||
'common.table.createTime': 'Created',
|
'common.table.createTime': 'Created',
|
||||||
'common.table.updateTime': 'Updated',
|
'common.table.updateTime': 'Updated',
|
||||||
'common.table.description': 'Description',
|
'common.table.description': 'Description',
|
||||||
|
'common.table.displayName': 'Display Name',
|
||||||
'common.table.name': 'Name',
|
'common.table.name': 'Name',
|
||||||
'common.table.status': 'Status',
|
'common.table.status': 'Status',
|
||||||
'common.table.name.list': '{type} Name',
|
'common.table.name.list': '{type} Name',
|
||||||
@@ -223,7 +226,6 @@ export default {
|
|||||||
'common.text.latest': 'Latest',
|
'common.text.latest': 'Latest',
|
||||||
'common.text.new': 'New',
|
'common.text.new': 'New',
|
||||||
'common.text.changelog': 'Release Notes',
|
'common.text.changelog': 'Release Notes',
|
||||||
'common.button.recreate': 'Recreate',
|
|
||||||
'common.button.delrecreate': 'Delete (Recreate)',
|
'common.button.delrecreate': 'Delete (Recreate)',
|
||||||
'common.options.all': 'All',
|
'common.options.all': 'All',
|
||||||
'common.options.none': 'None',
|
'common.options.none': 'None',
|
||||||
@@ -281,5 +283,7 @@ export default {
|
|||||||
'common.file.format.limit': 'Invalid file format. Allowed: {formats}.',
|
'common.file.format.limit': 'Invalid file format. Allowed: {formats}.',
|
||||||
'common.image.limit.width': 'Image width must be {width}.',
|
'common.image.limit.width': 'Image width must be {width}.',
|
||||||
'common.image.limit.height': 'Image height must be {height}.',
|
'common.image.limit.height': 'Image height must be {height}.',
|
||||||
'common.max': 'Max {count}'
|
'common.remaining': 'Remaining {count}',
|
||||||
|
'common.max': 'Max {count}',
|
||||||
|
'common.validate.group': 'Please complete the {group} configuration'
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,32 +1,23 @@
|
|||||||
export default {
|
export default {
|
||||||
'dashboard.title': 'Dashboard',
|
|
||||||
'dashboard.workers': 'Workers',
|
'dashboard.workers': 'Workers',
|
||||||
'dashboard.models': 'Models',
|
'dashboard.deployments': 'Deployments',
|
||||||
'dashboard.clusters': 'Clusters',
|
'dashboard.clusters': 'Clusters',
|
||||||
'dashboard.totalgpus': 'GPUs',
|
'dashboard.totalgpus': 'GPUs',
|
||||||
'dashboard.allocategpus': 'Allocated GPUs',
|
|
||||||
'dashboard.instances': 'Instances',
|
|
||||||
'dashboard.systemload': 'System Load',
|
'dashboard.systemload': 'System Load',
|
||||||
'dashboard.memory': 'RAM',
|
'dashboard.memory': 'RAM',
|
||||||
'dashboard.disk': 'Storage',
|
|
||||||
'dashboard.vram': 'VRAM',
|
'dashboard.vram': 'VRAM',
|
||||||
'dashboard.cpuutilization': 'Average CPU Utilization',
|
'dashboard.cpuutilization': 'Average CPU Utilization',
|
||||||
'dashboard.memoryutilization': 'Average RAM Utilization',
|
'dashboard.memoryutilization': 'Average RAM Utilization',
|
||||||
'dashboard.diskutilization': 'Storage Utilization',
|
|
||||||
'dashboard.vramutilization': 'Average VRAM Utilization',
|
'dashboard.vramutilization': 'Average VRAM Utilization',
|
||||||
'dashboard.gpuutilization': 'Average GPU Utilization',
|
'dashboard.gpuutilization': 'Average GPU Utilization',
|
||||||
'dashboard.usage': 'Usage',
|
'dashboard.usage': 'Usage',
|
||||||
'dashboard.apirequest': 'API Requests',
|
'dashboard.usage.title': 'Last {days} days usage',
|
||||||
|
'dashboard.usage.others': 'Others',
|
||||||
'dashboard.tokens': 'Token Usage',
|
'dashboard.tokens': 'Token Usage',
|
||||||
'dashboard.topusers': 'Top Users',
|
'dashboard.topusers': 'Top Users',
|
||||||
'dashboard.activeModels': 'Active Models',
|
'dashboard.activeDeployments': 'Active Deployments',
|
||||||
'dashboard.activeUsers': 'Active Users',
|
'dashboard.usageByModel': 'Usage by Model',
|
||||||
'dashboard.tokenUsageByModel': 'Token Usage by Model',
|
|
||||||
'dashboard.apiRequestsByModel': 'API Requests by Model',
|
|
||||||
'dashboard.topTokenUsageByUser': 'Top 10 Token Usage by User',
|
'dashboard.topTokenUsageByUser': 'Top 10 Token Usage by User',
|
||||||
'dashboard.topTokenUsageByApiKey': 'Top 10 Token Usage by API Key',
|
|
||||||
'dashboard.runninginstances': 'Running Instances',
|
|
||||||
'dashboard.activeModels.name': 'Model Name',
|
|
||||||
'dashboard.allocatevram': 'Allocated VRAM / RAM',
|
'dashboard.allocatevram': 'Allocated VRAM / RAM',
|
||||||
'dashboard.usage.selectuser': 'Select users',
|
'dashboard.usage.selectuser': 'Select users',
|
||||||
'dashboard.usage.selectmodel': 'Select models',
|
'dashboard.usage.selectmodel': 'Select models',
|
||||||
|
|||||||
@@ -15,10 +15,20 @@ export default {
|
|||||||
'gpuservice.template.mountPath': 'Mount Path',
|
'gpuservice.template.mountPath': 'Mount Path',
|
||||||
'gpuservice.template.containerDisk': 'Container Disk (GB)',
|
'gpuservice.template.containerDisk': 'Container Disk (GB)',
|
||||||
'gpuservice.template.memory': 'Memory (GB)',
|
'gpuservice.template.memory': 'Memory (GB)',
|
||||||
|
'gpuservice.instance.containerDisk.remaining':
|
||||||
|
'Container Disk (Max {count} GB)',
|
||||||
|
'gpuservice.instance.memory.remaining': 'Memory (Max {count} GB)',
|
||||||
|
'gpuservice.template.displayName': 'Display Name',
|
||||||
|
'gpuservice.template.displayName.max':
|
||||||
|
'Display name cannot exceed 63 characters.',
|
||||||
'gpuservice.template.ports': 'Ports',
|
'gpuservice.template.ports': 'Ports',
|
||||||
'gpuservice.template.ports.add': 'Add Port',
|
'gpuservice.template.ports.add': 'Add Port',
|
||||||
'gpuservice.template.ports.invalid':
|
'gpuservice.template.ports.invalid':
|
||||||
'Please complete the port configuration.',
|
'Please complete the port configuration.',
|
||||||
|
'gpuservice.template.ports.name': 'Name',
|
||||||
|
'gpuservice.template.ports.name.max':
|
||||||
|
'Port name cannot exceed 16 characters.',
|
||||||
|
'gpuservice.template.ports.name.duplicate': 'Port names must be unique.',
|
||||||
'gpuservice.template.env': 'Environment Variables',
|
'gpuservice.template.env': 'Environment Variables',
|
||||||
'gpuservice.template.env.add': 'Add Environment Variable',
|
'gpuservice.template.env.add': 'Add Environment Variable',
|
||||||
'gpuservice.template.env.invalid':
|
'gpuservice.template.env.invalid':
|
||||||
@@ -29,7 +39,49 @@ export default {
|
|||||||
'gpuservice.template.card.mount': 'Mount',
|
'gpuservice.template.card.mount': 'Mount',
|
||||||
'gpuservice.template.card.resources': 'Resources',
|
'gpuservice.template.card.resources': 'Resources',
|
||||||
'gpuservice.template.card.ports': 'Ports',
|
'gpuservice.template.card.ports': 'Ports',
|
||||||
|
'gpuservice.storageType': 'Storage Type',
|
||||||
|
'gpuservice.storageType.add': 'Add Storage Type',
|
||||||
|
'gpuservice.storageType.edit': 'Edit Storage Type',
|
||||||
|
'gpuservice.storageType.filter.name': 'Search by name',
|
||||||
|
'gpuservice.storageType.kind': 'Type',
|
||||||
|
'gpuservice.storageType.mountOptions': 'Mount Options',
|
||||||
|
'gpuservice.storageType.nfs.server': 'NFS Server',
|
||||||
|
'gpuservice.storageType.nfs.server.tips':
|
||||||
|
'Ensure the NFS server address is reachable from all Kubernetes clusters.',
|
||||||
|
'gpuservice.storageType.nfs.share': 'Share Path',
|
||||||
|
'gpuservice.storageType.nfs.share.tips':
|
||||||
|
'A directory based on the organization and storage names will be automatically created within this share path. If a subdirectory is specified, the generated directory will be created under that subdirectory.',
|
||||||
|
'gpuservice.storageType.nfs.subDirectory': 'Sub Directory',
|
||||||
|
'gpuservice.storageType.nfs.subDirectory.tips':
|
||||||
|
'If empty, a subdirectory named after the persistent volume will be created. If set, a directory with the persistent volume name will be created beneath this subdirectory.',
|
||||||
|
'gpuservice.storageType.nfs.mountPermissions': 'Mount Permissions',
|
||||||
|
'gpuservice.storageType.nfs.mountPermissions.tips':
|
||||||
|
'Inherit the file permissions from the NFS server.',
|
||||||
|
'gpuservice.storageType.s3.endpoint': 'Endpoint',
|
||||||
|
'gpuservice.storageType.s3.endpoint.tips':
|
||||||
|
'Ensure the S3 endpoint is reachable from all Kubernetes clusters.',
|
||||||
|
'gpuservice.storageType.s3.endpoint.rule': 'Must start with http or https',
|
||||||
|
'gpuservice.storageType.s3.region': 'Region',
|
||||||
|
'gpuservice.storageType.s3.bucket': 'Bucket',
|
||||||
|
'gpuservice.storageType.s3.bucket.tips':
|
||||||
|
'If empty, a new bucket named after the persistent volume will be created. If set, a subdirectory with the persistent volume name will be created inside this bucket.',
|
||||||
|
'gpuservice.storageType.s3.bucket.tips1':
|
||||||
|
'A prefix based on the organization and storage names will be automatically created within this bucket.',
|
||||||
|
'gpuservice.storageType.s3.bucket.tips2':
|
||||||
|
'For example, if the organization is named <span class="desc-block">awesome-group</span> and the storage is named <span class="desc-block">storage-1</span>, the resulting prefix will be <span class="desc-block">awesome-group/storage-1</span>.',
|
||||||
|
'gpuservice.storageType.s3.accessKey': 'Access Key',
|
||||||
|
'gpuservice.storageType.s3.secretKey': 'Secret Key',
|
||||||
|
'gpuservice.storageType.s3.insecure': 'Skip TLS/SSL certificate verification',
|
||||||
|
'gpuservice.storageType.s3.insecure.tips':
|
||||||
|
'When enabled, the S3 server certificate is not validated. Use this for internal testing or self-signed certificates; enable with caution in production.',
|
||||||
|
'gpuservice.publicKey': 'SSH Public Key',
|
||||||
|
'gpuservice.publicKey.add': 'Add SSH Public Key',
|
||||||
|
'gpuservice.publicKey.edit': 'Edit SSH Public Key',
|
||||||
|
'gpuservice.publicKey.filter.name': 'Search by name',
|
||||||
'gpuservice.publicKey.label': 'SSH Public Key',
|
'gpuservice.publicKey.label': 'SSH Public Key',
|
||||||
|
'gpuservice.instance.ssh.enable': 'Enable SSH Access',
|
||||||
|
'gpuservice.instance.ssh.assignKey': 'Assign SSH Public Key',
|
||||||
|
'gpuservice.instance.ssh.addKey': 'Add SSH Public Key',
|
||||||
'gpuservice.publicKey.placeholder':
|
'gpuservice.publicKey.placeholder':
|
||||||
'Begin with ssh-rsa or ssh-ed25519. One Public Key per line.\n\nView Public Key:\n- RSA\ncat ~/.ssh/id_rsa.pub\n- Ed25519\ncat ~/.ssh/id_ed25519.pub',
|
'Begin with ssh-rsa or ssh-ed25519. One Public Key per line.\n\nView Public Key:\n- RSA\ncat ~/.ssh/id_rsa.pub\n- Ed25519\ncat ~/.ssh/id_ed25519.pub',
|
||||||
'gpuservice.instance': 'GPU Instance',
|
'gpuservice.instance': 'GPU Instance',
|
||||||
@@ -48,19 +100,37 @@ export default {
|
|||||||
'gpuservice.instance.gpuCount': 'GPU Count',
|
'gpuservice.instance.gpuCount': 'GPU Count',
|
||||||
'gpuservice.instance.gpuCount.required': 'Please enter the GPU count',
|
'gpuservice.instance.gpuCount.required': 'Please enter the GPU count',
|
||||||
'gpuservice.instance.gpuCount.max':
|
'gpuservice.instance.gpuCount.max':
|
||||||
'The current instance type supports at most {count} GPU(s)',
|
'Please select at most {count} GPU card(s)',
|
||||||
|
'gpuservice.instance.gpuCount.min':
|
||||||
|
'Please select at least {count} GPU card(s)',
|
||||||
|
'gpuservice.instance.gpuCount.noAvailable':
|
||||||
|
'No available GPU resources, please choose another instance type.',
|
||||||
|
'gpuservice.instance.gpuCount.zero':
|
||||||
|
'CPU-only setup for environment preparation.',
|
||||||
'gpuservice.instance.stock': 'Stock',
|
'gpuservice.instance.stock': 'Stock',
|
||||||
'gpuservice.instance.sliced': 'Sliced',
|
'gpuservice.instance.sliced': 'Sliced',
|
||||||
'gpuservice.instance.memory': 'Memory',
|
'gpuservice.instance.memory': 'Memory',
|
||||||
'gpuservice.instance.ram': 'RAM',
|
'gpuservice.instance.ram': 'RAM',
|
||||||
'gpuservice.instance.search.type.placeholder':
|
'gpuservice.instance.disk': 'Disk',
|
||||||
'Search by name, VRAM, memory or vCPU',
|
'gpuservice.instance.search.type.placeholder': 'Search by name',
|
||||||
'gpuservice.instance.search.template.placeholder':
|
'gpuservice.instance.search.template.placeholder':
|
||||||
'Search by template name, image or mount path',
|
'Search by template name, image or mount path',
|
||||||
'gpuservice.instance.template.image': 'Image',
|
'gpuservice.instance.template.image': 'Image',
|
||||||
'gpuservice.instance.template.mount': 'Mount',
|
'gpuservice.instance.template.mount': 'Mount',
|
||||||
'gpuservice.instance.connect': 'Connect',
|
'gpuservice.instance.connect': 'Connect',
|
||||||
'gpuservice.instance.connect.copySshCommand': 'Copy SSH Command',
|
'gpuservice.instance.connect.copySshCommand': 'Copy SSH Command',
|
||||||
|
'gpuservice.instance.event.reason': 'Reason',
|
||||||
|
'gpuservice.instance.event.message': 'Message',
|
||||||
|
'gpuservice.instance.event.source': 'Source',
|
||||||
|
'gpuservice.instance.event.count': 'Count',
|
||||||
|
'gpuservice.instance.event.lastSeen': 'Last Seen',
|
||||||
|
'gpuservice.instance.event.recentHourTip':
|
||||||
|
'Only events from the last hour are shown',
|
||||||
|
'gpuservice.instance.event.tab.instance': 'Instance Events',
|
||||||
|
'gpuservice.instance.event.tab.volume': 'Volume Events',
|
||||||
|
'gpuservice.instance.recreate.confirm.title': 'Confirm recreation',
|
||||||
|
'gpuservice.instance.recreate.confirm.content':
|
||||||
|
'The current instance will be deleted first, then recreated with the current configuration.\n <span style="font-size: 13px;font-weight: 700">{name}</span>',
|
||||||
'gpuservice.storage': 'Storage',
|
'gpuservice.storage': 'Storage',
|
||||||
'gpuservice.storage.add': 'Add Storage',
|
'gpuservice.storage.add': 'Add Storage',
|
||||||
'gpuservice.storage.edit': 'Edit Storage',
|
'gpuservice.storage.edit': 'Edit Storage',
|
||||||
@@ -74,9 +144,20 @@ export default {
|
|||||||
'gpuservice.storage.persistent': 'Persistent',
|
'gpuservice.storage.persistent': 'Persistent',
|
||||||
'gpuservice.storage.temporary': 'Temporary',
|
'gpuservice.storage.temporary': 'Temporary',
|
||||||
'gpuservice.storage.persistentVolume': 'Persistent Volume',
|
'gpuservice.storage.persistentVolume': 'Persistent Volume',
|
||||||
|
'gpuservice.storage.temporary.tips':
|
||||||
|
'Data is cleared when the instance stops.',
|
||||||
|
'gpuservice.storage.persistentVolume.tips':
|
||||||
|
'Data persists across restarts and is deleted only when the instance is terminated. Cannot be shared with other instances.',
|
||||||
'gpuservice.storage.persistentVolume.required':
|
'gpuservice.storage.persistentVolume.required':
|
||||||
'Please select a persistent volume',
|
'Please select a persistent volume',
|
||||||
'gpuservice.storage.tempCapacity': 'Storage Capacity (GB)',
|
'gpuservice.storage.persistentVolume.capacity': 'Capacity (GB)',
|
||||||
|
'gpuservice.storage.persistentVolume.capacity.required':
|
||||||
|
'Please enter capacity',
|
||||||
|
'gpuservice.storage.persistentVolume.releaseWithInstance':
|
||||||
|
'Release with instance',
|
||||||
|
'gpuservice.storage.tempCapacity': 'Capacity (GB)',
|
||||||
'gpuservice.storage.tempCapacity.required':
|
'gpuservice.storage.tempCapacity.required':
|
||||||
'Please enter the local temporary storage capacity'
|
'Please enter the temporary storage capacity',
|
||||||
|
'gpuservice.form.rule.name':
|
||||||
|
"Lowercase letters, numbers, and '-'. Start and end with a letter or number, no consecutive '-', max 63 characters."
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -45,5 +45,6 @@ export default {
|
|||||||
'menu.gpuService.instances': 'GPU Instances',
|
'menu.gpuService.instances': 'GPU Instances',
|
||||||
'menu.gpuService.templates': 'Instance Templates',
|
'menu.gpuService.templates': 'Instance Templates',
|
||||||
'menu.gpuService.storage': 'Storage',
|
'menu.gpuService.storage': 'Storage',
|
||||||
|
'menu.gpuService.storageTypes': 'Storage Types',
|
||||||
'menu.gpuService.publicKeys': 'SSH Public Keys'
|
'menu.gpuService.publicKeys': 'SSH Public Keys'
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ export default {
|
|||||||
'models.form.backend': 'Backend',
|
'models.form.backend': 'Backend',
|
||||||
'models.form.backend_parameters': 'Backend Parameters',
|
'models.form.backend_parameters': 'Backend Parameters',
|
||||||
'models.instance.params.configured': 'User Configured',
|
'models.instance.params.configured': 'User Configured',
|
||||||
'models.instance.params.autoInjected': 'Auto-injected',
|
'models.instance.params.autoInjected': 'Auto-injected Parameters',
|
||||||
'models.search.gguf.tips':
|
'models.search.gguf.tips':
|
||||||
'GGUF models use llama-box(supports Linux, macOS and Windows).',
|
'GGUF models use llama-box(supports Linux, macOS and Windows).',
|
||||||
'models.search.vllm.tips':
|
'models.search.vllm.tips':
|
||||||
@@ -290,5 +290,11 @@ export default {
|
|||||||
'models.instance.previousRun': 'Previous Run',
|
'models.instance.previousRun': 'Previous Run',
|
||||||
'models.instance.startHistory': 'Run History',
|
'models.instance.startHistory': 'Run History',
|
||||||
'models.instance.startHistory.tips':
|
'models.instance.startHistory.tips':
|
||||||
'Shows logs from the run before the last error-triggered restart.'
|
'Shows logs from the run before the last error-triggered restart.',
|
||||||
|
'models.form.lora.label': 'LoRA Adapter',
|
||||||
|
'models.form.lora.add': 'Add LoRA Adapter',
|
||||||
|
'models.form.lora.select': 'Select LoRA',
|
||||||
|
'models.form.lora.name': 'LoRA name',
|
||||||
|
'models.form.lora.rule.empty': 'Input cannot be empty',
|
||||||
|
'models.form.lora.rule.duplicate': 'LoRA name cannot be duplicated'
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -64,5 +64,13 @@ export default {
|
|||||||
'noresult.gpuservice.instance.nofound': 'No matching GPU instances found.',
|
'noresult.gpuservice.instance.nofound': 'No matching GPU instances found.',
|
||||||
'noresult.gpuservice.storage.title': 'No Storage',
|
'noresult.gpuservice.storage.title': 'No Storage',
|
||||||
'noresult.gpuservice.storage.subTitle': 'No storage has been added yet.',
|
'noresult.gpuservice.storage.subTitle': 'No storage has been added yet.',
|
||||||
'noresult.gpuservice.storage.nofound': 'No matching storage found.'
|
'noresult.gpuservice.storage.nofound': 'No matching storage found.',
|
||||||
|
'noresult.gpuservice.storageType.title': 'No Storage Types',
|
||||||
|
'noresult.gpuservice.storageType.subTitle':
|
||||||
|
'No storage types have been added yet.',
|
||||||
|
'noresult.gpuservice.storageType.nofound': 'No matching storage types found.',
|
||||||
|
'noresult.gpuservice.sshkey.title': 'No SSH Public Keys',
|
||||||
|
'noresult.gpuservice.sshkey.subTitle':
|
||||||
|
'No SSH public keys have been added yet.',
|
||||||
|
'noresult.gpuservice.sshkey.nofound': 'No matching SSH public keys found.'
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -98,6 +98,7 @@ export default {
|
|||||||
'resources.worker.download.privatekey': 'Download Private Key',
|
'resources.worker.download.privatekey': 'Download Private Key',
|
||||||
'resources.modelfiles.form.exsting': 'Downloaded',
|
'resources.modelfiles.form.exsting': 'Downloaded',
|
||||||
'resources.modelfiles.form.added': 'Added',
|
'resources.modelfiles.form.added': 'Added',
|
||||||
|
'resources.modelfiles.form.isLora': 'Is LoRA',
|
||||||
'resources.worker.maintenance.title': 'System Maintenance',
|
'resources.worker.maintenance.title': 'System Maintenance',
|
||||||
'resources.worker.maintenance.enable': 'Enter Maintenance Mode',
|
'resources.worker.maintenance.enable': 'Enter Maintenance Mode',
|
||||||
'resources.worker.maintenance.disable': 'Exit Maintenance Mode',
|
'resources.worker.maintenance.disable': 'Exit Maintenance Mode',
|
||||||
|
|||||||
@@ -23,6 +23,13 @@ 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.parameterFormat.default': 'バックエンドのデフォルト',
|
||||||
|
'backend.form.parameterFormat.space': 'スペース区切り (--key value)',
|
||||||
|
'backend.form.parameterFormat.equal': 'イコール連結 (--key=value)',
|
||||||
|
'backend.form.commonParameters': 'よく使うパラメータ',
|
||||||
|
'backend.form.commonParameters.tips':
|
||||||
|
'モデルのデプロイ時にバックエンドパラメータ入力欄の候補として表示されます。',
|
||||||
'backend.form.versionConfig': 'Versions Config',
|
'backend.form.versionConfig': 'Versions Config',
|
||||||
'backend.form.addParameter': 'Add Parameter',
|
'backend.form.addParameter': 'Add Parameter',
|
||||||
'backend.form.noVersion': 'No versions added',
|
'backend.form.noVersion': 'No versions added',
|
||||||
|
|||||||
@@ -42,6 +42,8 @@ export default {
|
|||||||
'Please make sure the <a href={link} target="_blank">prerequisites</a> for {label} are met before executing the following command.',
|
'Please make sure the <a href={link} target="_blank">prerequisites</a> for {label} are met before executing the following command.',
|
||||||
'clusters.create.addCommand.tips':
|
'clusters.create.addCommand.tips':
|
||||||
'On the Worker that needs to be added, run the following command to join it to the cluster.',
|
'On the Worker that needs to be added, run the following command to join it to the cluster.',
|
||||||
|
'clusters.create.addCommand.k8s.tips':
|
||||||
|
'登録する Kubernetes クラスターで以下のコマンドを実行し、Kubernetes リソースを作成してクラスターを登録します。',
|
||||||
'cluster.create.checkEnv.tips':
|
'cluster.create.checkEnv.tips':
|
||||||
'Use the following command to check if the environment is ready.',
|
'Use the following command to check if the environment is ready.',
|
||||||
'clusters.create.register.tips':
|
'clusters.create.register.tips':
|
||||||
@@ -67,6 +69,9 @@ export default {
|
|||||||
'clusters.addworker.selectCluster.tips':
|
'clusters.addworker.selectCluster.tips':
|
||||||
'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.singleOnly':
|
||||||
|
'The selected vendor is not in this cluster’s GPU vendor overrides — single-select only. Pick a vendor configured in the overrides to enable multi-select.',
|
||||||
'clusters.addworker.checkEnv': 'Check Environment',
|
'clusters.addworker.checkEnv': 'Check Environment',
|
||||||
'clusters.addworker.specifyArgs': 'Specify Arguments',
|
'clusters.addworker.specifyArgs': 'Specify Arguments',
|
||||||
'clusters.addworker.runCommand': 'Run Command',
|
'clusters.addworker.runCommand': 'Run Command',
|
||||||
@@ -158,7 +163,27 @@ export default {
|
|||||||
'clusters.volume.pvc.readOnly': 'Read Only',
|
'clusters.volume.pvc.readOnly': 'Read Only',
|
||||||
'clusters.volume.configMap.name': 'ConfigMap Name',
|
'clusters.volume.configMap.name': 'ConfigMap Name',
|
||||||
'clusters.volume.configMap.optional': 'Optional',
|
'clusters.volume.configMap.optional': 'Optional',
|
||||||
'clusters.volume.add': 'Add Volume Mount'
|
'clusters.volume.add': 'Add Volume Mount',
|
||||||
|
'clusters.imageCredentials.title': 'Image Credentials',
|
||||||
|
'clusters.imageCredentials.add': 'Add Credential',
|
||||||
|
'clusters.imageCredentials.registry': 'Registry',
|
||||||
|
'clusters.imageCredentials.username': 'Username',
|
||||||
|
'clusters.imageCredentials.password': 'Password',
|
||||||
|
'clusters.nodeSelector.title': 'Node Selector',
|
||||||
|
'clusters.nodeSelector.tip':
|
||||||
|
'Pod nodeSelector applied to every worker DaemonSet — only nodes whose labels match are eligible to run the worker.',
|
||||||
|
'clusters.gpuVendorOverrides.title': 'GPU Vendor Overrides',
|
||||||
|
'clusters.gpuVendorOverrides.validate.emptySelector':
|
||||||
|
'The override for {vendor} needs at least one nodeSelector entry — an empty override would leave the multi-vendor manifest unable to target nodes for this runtime.',
|
||||||
|
'clusters.gpuVendorOverrides.validate.duplicate':
|
||||||
|
'Overrides for {v1} and {v2} use the same nodeSelector. Each vendor must scope to a distinct node set, otherwise their worker DaemonSets would compete for the same nodes.',
|
||||||
|
'clusters.gpuVendorOverrides.validate.keyConflict':
|
||||||
|
'Override for {vendor} reuses key(s) {keys} from the base Node Selector — the CPU worker would simultaneously require and forbid the key and never schedule.',
|
||||||
|
'clusters.gpuVendorOverrides.tip':
|
||||||
|
'Required when running multiple GPU vendors in one cluster. Each entry pins that vendor’s worker DaemonSet to nodes matching its nodeSelector, and the CPU worker is steered away from those nodes via a DoesNotExist node-affinity.',
|
||||||
|
'clusters.gpuVendorOverrides.add': 'Add Override',
|
||||||
|
'clusters.gpuVendorOverrides.vendor': 'GPU Vendor',
|
||||||
|
'clusters.gpuVendorOverrides.nodeSelector': 'Node Selector'
|
||||||
};
|
};
|
||||||
|
|
||||||
// ========== To-Do: Translate Keys (Remove After Translation) ==========
|
// ========== To-Do: Translate Keys (Remove After Translation) ==========
|
||||||
|
|||||||
@@ -52,10 +52,13 @@ export default {
|
|||||||
'common.button.authorize': 'ロール認可',
|
'common.button.authorize': 'ロール認可',
|
||||||
'common.button.confirm': '確認',
|
'common.button.confirm': '確認',
|
||||||
'common.button.viewlog': 'ログを表示',
|
'common.button.viewlog': 'ログを表示',
|
||||||
|
'common.button.viewevent': 'イベントを表示',
|
||||||
|
'common.button.recreate': '再作成',
|
||||||
'common.table.operation': '操作',
|
'common.table.operation': '操作',
|
||||||
'common.table.createTime': '作成日時',
|
'common.table.createTime': '作成日時',
|
||||||
'common.table.updateTime': '更新日時',
|
'common.table.updateTime': '更新日時',
|
||||||
'common.table.description': '説明',
|
'common.table.description': '説明',
|
||||||
|
'common.table.displayName': '表示名',
|
||||||
'common.table.name': '名前',
|
'common.table.name': '名前',
|
||||||
'common.table.status': 'ステータス',
|
'common.table.status': 'ステータス',
|
||||||
'common.table.name.list': '{type} 名称',
|
'common.table.name.list': '{type} 名称',
|
||||||
@@ -223,7 +226,6 @@ export default {
|
|||||||
'common.text.latest': '最新',
|
'common.text.latest': '最新',
|
||||||
'common.text.new': '新規',
|
'common.text.new': '新規',
|
||||||
'common.text.changelog': 'リリースノート',
|
'common.text.changelog': 'リリースノート',
|
||||||
'common.button.recreate': '再作成',
|
|
||||||
'common.button.delrecreate': '削除(再作成)',
|
'common.button.delrecreate': '削除(再作成)',
|
||||||
'common.options.all': 'すべて',
|
'common.options.all': 'すべて',
|
||||||
'common.options.none': 'なし',
|
'common.options.none': 'なし',
|
||||||
@@ -281,7 +283,9 @@ export default {
|
|||||||
'common.file.format.limit': 'Invalid file format. Allowed: {formats}.',
|
'common.file.format.limit': 'Invalid file format. Allowed: {formats}.',
|
||||||
'common.image.limit.width': 'Image width must be {width}.',
|
'common.image.limit.width': 'Image width must be {width}.',
|
||||||
'common.image.limit.height': 'Image height must be {height}.',
|
'common.image.limit.height': 'Image height must be {height}.',
|
||||||
'common.max': '最大 {count}'
|
'common.remaining': '残り {count}',
|
||||||
|
'common.max': '最大 {count}',
|
||||||
|
'common.validate.group': 'Please complete the {group} configuration'
|
||||||
};
|
};
|
||||||
|
|
||||||
// ========== To-Do: Translate Keys (Remove After Translation) ==========
|
// ========== To-Do: Translate Keys (Remove After Translation) ==========
|
||||||
|
|||||||
@@ -1,31 +1,22 @@
|
|||||||
export default {
|
export default {
|
||||||
'dashboard.title': 'ダッシュボード',
|
|
||||||
'dashboard.workers': 'ワーカー',
|
'dashboard.workers': 'ワーカー',
|
||||||
'dashboard.models': 'モデル',
|
'dashboard.deployments': 'Deployments',
|
||||||
'dashboard.totalgpus': 'GPU数',
|
'dashboard.totalgpus': 'GPU数',
|
||||||
'dashboard.allocategpus': '割り当て済みGPU',
|
|
||||||
'dashboard.instances': 'インスタンス',
|
|
||||||
'dashboard.systemload': 'システム負荷',
|
'dashboard.systemload': 'システム負荷',
|
||||||
'dashboard.memory': 'メモリ',
|
'dashboard.memory': 'メモリ',
|
||||||
'dashboard.disk': 'ストレージ',
|
|
||||||
'dashboard.vram': 'VRAM',
|
'dashboard.vram': 'VRAM',
|
||||||
'dashboard.cpuutilization': '平均CPU使用率',
|
'dashboard.cpuutilization': '平均CPU使用率',
|
||||||
'dashboard.memoryutilization': '平均メモリ使用率',
|
'dashboard.memoryutilization': '平均メモリ使用率',
|
||||||
'dashboard.diskutilization': 'ストレージ使用率',
|
|
||||||
'dashboard.vramutilization': '平均VRAM使用率',
|
'dashboard.vramutilization': '平均VRAM使用率',
|
||||||
'dashboard.gpuutilization': '平均GPU使用率',
|
'dashboard.gpuutilization': '平均GPU使用率',
|
||||||
'dashboard.usage': '使用状況',
|
'dashboard.usage': '使用状況',
|
||||||
'dashboard.apirequest': 'APIリクエスト',
|
'dashboard.usage.title': '過去 {days} 日間の使用状況',
|
||||||
|
'dashboard.usage.others': 'その他',
|
||||||
'dashboard.tokens': 'トークン使用量',
|
'dashboard.tokens': 'トークン使用量',
|
||||||
'dashboard.topusers': 'トップユーザー',
|
'dashboard.topusers': 'トップユーザー',
|
||||||
'dashboard.activeModels': 'アクティブなモデル',
|
'dashboard.activeDeployments': 'Active Deployments',
|
||||||
'dashboard.activeUsers': 'アクティブユーザー',
|
'dashboard.usageByModel': 'モデル別使用量',
|
||||||
'dashboard.tokenUsageByModel': 'モデル別トークン使用量',
|
|
||||||
'dashboard.apiRequestsByModel': 'モデル別APIリクエスト',
|
|
||||||
'dashboard.topTokenUsageByUser': 'ユーザー別トークン使用量トップ10',
|
'dashboard.topTokenUsageByUser': 'ユーザー別トークン使用量トップ10',
|
||||||
'dashboard.topTokenUsageByApiKey': 'APIキー別トークン使用量トップ10',
|
|
||||||
'dashboard.runninginstances': '稼働中のインスタンス',
|
|
||||||
'dashboard.activeModels.name': 'モデル名',
|
|
||||||
'dashboard.allocatevram': '割り当て済みVRAM / メモリ',
|
'dashboard.allocatevram': '割り当て済みVRAM / メモリ',
|
||||||
'dashboard.usage.selectuser': 'Select users',
|
'dashboard.usage.selectuser': 'Select users',
|
||||||
'dashboard.usage.selectmodel': 'Select models',
|
'dashboard.usage.selectmodel': 'Select models',
|
||||||
|
|||||||
@@ -15,9 +15,19 @@ export default {
|
|||||||
'gpuservice.template.mountPath': 'マウントパス',
|
'gpuservice.template.mountPath': 'マウントパス',
|
||||||
'gpuservice.template.containerDisk': 'コンテナディスク (GB)',
|
'gpuservice.template.containerDisk': 'コンテナディスク (GB)',
|
||||||
'gpuservice.template.memory': 'メモリ (GB)',
|
'gpuservice.template.memory': 'メモリ (GB)',
|
||||||
|
'gpuservice.instance.containerDisk.remaining':
|
||||||
|
'コンテナディスク (最大 {count} GB)',
|
||||||
|
'gpuservice.instance.memory.remaining': 'メモリ (最大 {count} GB)',
|
||||||
|
'gpuservice.template.displayName': '表示名',
|
||||||
|
'gpuservice.template.displayName.max':
|
||||||
|
'表示名は 63 文字以内で入力してください。',
|
||||||
'gpuservice.template.ports': 'ポート',
|
'gpuservice.template.ports': 'ポート',
|
||||||
'gpuservice.template.ports.add': 'ポートを追加',
|
'gpuservice.template.ports.add': 'ポートを追加',
|
||||||
'gpuservice.template.ports.invalid': 'ポート設定を完成させてください。',
|
'gpuservice.template.ports.invalid': 'ポート設定を完成させてください。',
|
||||||
|
'gpuservice.template.ports.name': '名前',
|
||||||
|
'gpuservice.template.ports.name.max':
|
||||||
|
'ポート名は 16 文字以内で入力してください。',
|
||||||
|
'gpuservice.template.ports.name.duplicate': 'ポート名は重複できません。',
|
||||||
'gpuservice.template.env': '環境変数',
|
'gpuservice.template.env': '環境変数',
|
||||||
'gpuservice.template.env.add': '環境変数を追加',
|
'gpuservice.template.env.add': '環境変数を追加',
|
||||||
'gpuservice.template.env.invalid': '環境変数を完成させてください。',
|
'gpuservice.template.env.invalid': '環境変数を完成させてください。',
|
||||||
@@ -27,7 +37,50 @@ export default {
|
|||||||
'gpuservice.template.card.mount': 'マウント',
|
'gpuservice.template.card.mount': 'マウント',
|
||||||
'gpuservice.template.card.resources': 'リソース',
|
'gpuservice.template.card.resources': 'リソース',
|
||||||
'gpuservice.template.card.ports': 'ポート',
|
'gpuservice.template.card.ports': 'ポート',
|
||||||
|
'gpuservice.storageType': 'ストレージタイプ',
|
||||||
|
'gpuservice.storageType.add': 'ストレージタイプを追加',
|
||||||
|
'gpuservice.storageType.edit': 'ストレージタイプを編集',
|
||||||
|
'gpuservice.storageType.filter.name': '名前で検索',
|
||||||
|
'gpuservice.storageType.kind': '種別',
|
||||||
|
'gpuservice.storageType.mountOptions': 'マウントオプション',
|
||||||
|
'gpuservice.storageType.nfs.server': 'NFS サーバー',
|
||||||
|
'gpuservice.storageType.nfs.server.tips':
|
||||||
|
'すべての Kubernetes クラスターから NFS サーバーアドレスにアクセスできることを確認してください。',
|
||||||
|
'gpuservice.storageType.nfs.share': '共有パス',
|
||||||
|
'gpuservice.storageType.nfs.share.tips':
|
||||||
|
'この共有パス配下に、組織名とストレージ名に基づくディレクトリが自動的に作成されます。サブディレクトリが指定されている場合、生成されたディレクトリはそのサブディレクトリ配下に作成されます。',
|
||||||
|
'gpuservice.storageType.nfs.subDirectory': 'サブディレクトリ',
|
||||||
|
'gpuservice.storageType.nfs.subDirectory.tips':
|
||||||
|
'空の場合、永続ボリューム名のサブディレクトリが作成されます。設定されている場合、このサブディレクトリ配下に永続ボリューム名のディレクトリが作成されます。',
|
||||||
|
'gpuservice.storageType.nfs.mountPermissions': 'マウント権限',
|
||||||
|
'gpuservice.storageType.nfs.mountPermissions.tips':
|
||||||
|
'NFS サーバー上のファイル権限を継承します。',
|
||||||
|
'gpuservice.storageType.s3.endpoint': 'エンドポイント',
|
||||||
|
'gpuservice.storageType.s3.endpoint.tips':
|
||||||
|
'すべての Kubernetes クラスターから S3 エンドポイントにアクセスできることを確認してください。',
|
||||||
|
'gpuservice.storageType.s3.endpoint.rule':
|
||||||
|
'http または https で始まる必要があります',
|
||||||
|
'gpuservice.storageType.s3.region': 'リージョン',
|
||||||
|
'gpuservice.storageType.s3.bucket': 'バケット',
|
||||||
|
'gpuservice.storageType.s3.bucket.tips':
|
||||||
|
'空の場合、永続ボリューム名で新しいバケットが作成されます。設定されている場合、このバケット配下に永続ボリューム名のサブディレクトリが作成されます。',
|
||||||
|
'gpuservice.storageType.s3.bucket.tips1':
|
||||||
|
'このバケット内に、組織名とストレージ名に基づくプレフィックスディレクトリが自動的に作成されます。',
|
||||||
|
'gpuservice.storageType.s3.bucket.tips2':
|
||||||
|
'例えば、組織名が <span class="desc-block">awesome-group</span>、ストレージ名が <span class="desc-block">storage-1</span> の場合、生成されるプレフィックスは <span class="desc-block">awesome-group/storage-1</span> になります。',
|
||||||
|
'gpuservice.storageType.s3.accessKey': 'アクセスキー',
|
||||||
|
'gpuservice.storageType.s3.secretKey': 'シークレットキー',
|
||||||
|
'gpuservice.storageType.s3.insecure': 'TLS/SSL 証明書の検証をスキップ',
|
||||||
|
'gpuservice.storageType.s3.insecure.tips':
|
||||||
|
'有効にすると S3 サーバーの証明書検証を無視します。社内テストや自己署名証明書の利用時に適しており、本番環境では慎重に有効化してください。',
|
||||||
|
'gpuservice.publicKey': 'SSH 公開鍵',
|
||||||
|
'gpuservice.publicKey.add': 'SSH 公開鍵を追加',
|
||||||
|
'gpuservice.publicKey.edit': 'SSH 公開鍵を編集',
|
||||||
|
'gpuservice.publicKey.filter.name': '名前で検索',
|
||||||
'gpuservice.publicKey.label': 'SSH 公開鍵',
|
'gpuservice.publicKey.label': 'SSH 公開鍵',
|
||||||
|
'gpuservice.instance.ssh.enable': 'SSH アクセスを有効化',
|
||||||
|
'gpuservice.instance.ssh.assignKey': 'SSH 公開鍵を割り当て',
|
||||||
|
'gpuservice.instance.ssh.addKey': 'SSH 公開鍵を追加',
|
||||||
'gpuservice.publicKey.placeholder':
|
'gpuservice.publicKey.placeholder':
|
||||||
'ssh-rsa または ssh-ed25519 で始まり、各公開鍵は1行ずつ記述します\n\n公開鍵を確認:\n- RSA\ncat ~/.ssh/id_rsa.pub\n- Ed25519\ncat ~/.ssh/id_ed25519.pub',
|
'ssh-rsa または ssh-ed25519 で始まり、各公開鍵は1行ずつ記述します\n\n公開鍵を確認:\n- RSA\ncat ~/.ssh/id_rsa.pub\n- Ed25519\ncat ~/.ssh/id_ed25519.pub',
|
||||||
'gpuservice.instance': 'GPU インスタンス',
|
'gpuservice.instance': 'GPU インスタンス',
|
||||||
@@ -46,19 +99,36 @@ export default {
|
|||||||
'gpuservice.instance.gpuCount': 'GPU 数',
|
'gpuservice.instance.gpuCount': 'GPU 数',
|
||||||
'gpuservice.instance.gpuCount.required': 'GPU 数を入力してください',
|
'gpuservice.instance.gpuCount.required': 'GPU 数を入力してください',
|
||||||
'gpuservice.instance.gpuCount.max':
|
'gpuservice.instance.gpuCount.max':
|
||||||
'現在のインスタンスタイプは最大 {count} 個の GPU をサポートします',
|
'最大 {count} 枚の GPU カードを選択してください',
|
||||||
|
'gpuservice.instance.gpuCount.min':
|
||||||
|
'少なくとも {count} 枚の GPU カードを選択してください',
|
||||||
|
'gpuservice.instance.gpuCount.noAvailable':
|
||||||
|
'利用可能な GPU リソースがありません。別のインスタンスタイプを選択してください。',
|
||||||
|
'gpuservice.instance.gpuCount.zero': 'CPU のみを使用し、環境準備用です。',
|
||||||
'gpuservice.instance.stock': '在庫',
|
'gpuservice.instance.stock': '在庫',
|
||||||
'gpuservice.instance.sliced': '分割',
|
'gpuservice.instance.sliced': '分割',
|
||||||
'gpuservice.instance.memory': 'Memory',
|
'gpuservice.instance.memory': 'Memory',
|
||||||
'gpuservice.instance.ram': 'RAM',
|
'gpuservice.instance.ram': 'RAM',
|
||||||
'gpuservice.instance.search.type.placeholder':
|
'gpuservice.instance.disk': 'ディスク',
|
||||||
'名前、VRAM、メモリまたは vCPU で検索',
|
'gpuservice.instance.search.type.placeholder': '名前で検索',
|
||||||
'gpuservice.instance.search.template.placeholder':
|
'gpuservice.instance.search.template.placeholder':
|
||||||
'テンプレート名、イメージまたはマウントパスで検索',
|
'テンプレート名、イメージまたはマウントパスで検索',
|
||||||
'gpuservice.instance.template.image': 'イメージ',
|
'gpuservice.instance.template.image': 'イメージ',
|
||||||
'gpuservice.instance.template.mount': 'マウント',
|
'gpuservice.instance.template.mount': 'マウント',
|
||||||
'gpuservice.instance.connect': '接続',
|
'gpuservice.instance.connect': '接続',
|
||||||
'gpuservice.instance.connect.copySshCommand': 'SSH コマンドをコピー',
|
'gpuservice.instance.connect.copySshCommand': 'SSH コマンドをコピー',
|
||||||
|
'gpuservice.instance.event.reason': '理由',
|
||||||
|
'gpuservice.instance.event.message': 'メッセージ',
|
||||||
|
'gpuservice.instance.event.source': 'ソース',
|
||||||
|
'gpuservice.instance.event.count': '回数',
|
||||||
|
'gpuservice.instance.event.lastSeen': '最終発生',
|
||||||
|
'gpuservice.instance.event.recentHourTip':
|
||||||
|
'直近 1 時間のイベントのみ表示されます',
|
||||||
|
'gpuservice.instance.event.tab.instance': 'インスタンスイベント',
|
||||||
|
'gpuservice.instance.event.tab.volume': 'ボリュームイベント',
|
||||||
|
'gpuservice.instance.recreate.confirm.title': '再作成を確認しますか',
|
||||||
|
'gpuservice.instance.recreate.confirm.content':
|
||||||
|
'現在のインスタンスを削除した後、現在の構成で再作成します。\n <span style="font-size: 13px;font-weight: 700">{name}</span>',
|
||||||
'gpuservice.storage': 'ストレージ',
|
'gpuservice.storage': 'ストレージ',
|
||||||
'gpuservice.storage.add': 'ストレージを追加',
|
'gpuservice.storage.add': 'ストレージを追加',
|
||||||
'gpuservice.storage.edit': 'ストレージを編集',
|
'gpuservice.storage.edit': 'ストレージを編集',
|
||||||
@@ -74,7 +144,18 @@ export default {
|
|||||||
'gpuservice.storage.persistentVolume': '永続ボリューム',
|
'gpuservice.storage.persistentVolume': '永続ボリューム',
|
||||||
'gpuservice.storage.persistentVolume.required':
|
'gpuservice.storage.persistentVolume.required':
|
||||||
'永続ボリュームを選択してください',
|
'永続ボリュームを選択してください',
|
||||||
'gpuservice.storage.tempCapacity': 'ストレージ容量 (GB)',
|
'gpuservice.storage.persistentVolume.capacity': '容量 (GB)',
|
||||||
|
'gpuservice.storage.persistentVolume.capacity.required':
|
||||||
|
'容量を入力してください',
|
||||||
|
'gpuservice.storage.persistentVolume.releaseWithInstance':
|
||||||
|
'インスタンスと共に解放',
|
||||||
|
'gpuservice.storage.tempCapacity': '容量 (GB)',
|
||||||
'gpuservice.storage.tempCapacity.required':
|
'gpuservice.storage.tempCapacity.required':
|
||||||
'ローカルの一時ストレージ容量を入力してください'
|
'一時ストレージ容量を入力してください',
|
||||||
|
'gpuservice.form.rule.name':
|
||||||
|
"小文字、数字、'-' のみ使用可能。文字または数字で始まり、文字または数字で終わる必要があり、連続する '-' は不可、最大 63 文字。",
|
||||||
|
'gpuservice.storage.temporary.tips':
|
||||||
|
'Data is cleared when the instance stops.',
|
||||||
|
'gpuservice.storage.persistentVolume.tips':
|
||||||
|
'Data persists across restarts and is deleted only when the instance is terminated. Cannot be shared with other instances.'
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ export default {
|
|||||||
'menu.gpuService.instances': 'GPU Instances',
|
'menu.gpuService.instances': 'GPU Instances',
|
||||||
'menu.gpuService.templates': 'Instance Templates',
|
'menu.gpuService.templates': 'Instance Templates',
|
||||||
'menu.gpuService.storage': 'Storage',
|
'menu.gpuService.storage': 'Storage',
|
||||||
|
'menu.gpuService.storageTypes': 'ストレージタイプ',
|
||||||
'menu.gpuService.publicKeys': 'SSH Public Keys'
|
'menu.gpuService.publicKeys': 'SSH Public Keys'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ export default {
|
|||||||
'models.form.backend': 'バックエンド',
|
'models.form.backend': 'バックエンド',
|
||||||
'models.form.backend_parameters': 'バックエンドパラメータ',
|
'models.form.backend_parameters': 'バックエンドパラメータ',
|
||||||
'models.instance.params.configured': 'User Configured',
|
'models.instance.params.configured': 'User Configured',
|
||||||
'models.instance.params.autoInjected': '自動注入',
|
'models.instance.params.autoInjected': '自動注入パラメータ',
|
||||||
'models.search.gguf.tips':
|
'models.search.gguf.tips':
|
||||||
'GGUFモデルはllama-boxを使用します(Linux、macOS、Windowsをサポート)。',
|
'GGUFモデルはllama-boxを使用します(Linux、macOS、Windowsをサポート)。',
|
||||||
'models.search.vllm.tips':
|
'models.search.vllm.tips':
|
||||||
@@ -290,7 +290,13 @@ export default {
|
|||||||
'models.instance.previousRun': 'Previous Run',
|
'models.instance.previousRun': 'Previous Run',
|
||||||
'models.instance.startHistory': 'Run History',
|
'models.instance.startHistory': 'Run History',
|
||||||
'models.instance.startHistory.tips':
|
'models.instance.startHistory.tips':
|
||||||
'Shows logs from the run before the last error-triggered restart.'
|
'Shows logs from the run before the last error-triggered restart.',
|
||||||
|
'models.form.lora.label': 'LoRA Adapter',
|
||||||
|
'models.form.lora.add': 'Add LoRA Adapter',
|
||||||
|
'models.form.lora.select': 'Select LoRA',
|
||||||
|
'models.form.lora.name': 'LoRA name',
|
||||||
|
'models.form.lora.rule.empty': 'Input cannot be empty',
|
||||||
|
'models.form.lora.rule.duplicate': 'LoRA name cannot be duplicated'
|
||||||
};
|
};
|
||||||
|
|
||||||
// ========== To-Do: Translate Keys (Remove After Translation) ==========
|
// ========== To-Do: Translate Keys (Remove After Translation) ==========
|
||||||
@@ -393,5 +399,11 @@ export default {
|
|||||||
// 78. 'models.form.enableModelRoute.tips': 'Enable Model Route',
|
// 78. 'models.form.enableModelRoute.tips': 'Enable Model Route',
|
||||||
// 79. 'models.table.modelView': 'Model View',
|
// 79. 'models.table.modelView': 'Model View',
|
||||||
// 80. 'models.table.instanceView': 'Instance View',
|
// 80. 'models.table.instanceView': 'Instance View',
|
||||||
// 81. 'models.table.category': 'Category'
|
// 81. 'models.table.category': 'Category',
|
||||||
|
// 82. 'models.form.lora.label': 'LoRA Adapter',
|
||||||
|
// 83. 'models.form.lora.add': 'Add LoRA Adapter',
|
||||||
|
// 84. 'models.form.lora.select': 'Select LoRA',
|
||||||
|
// 85. 'models.form.lora.name': 'LoRA name',
|
||||||
|
// 86. 'models.form.lora.rule.empty': 'Input cannot be empty',
|
||||||
|
// 87. 'models.form.lora.rule.duplicate': 'LoRA name cannot be duplicated'
|
||||||
// ========== End of To-Do List ==========
|
// ========== End of To-Do List ==========
|
||||||
|
|||||||
@@ -66,5 +66,13 @@ export default {
|
|||||||
'noresult.gpuservice.storage.title': 'ストレージなし',
|
'noresult.gpuservice.storage.title': 'ストレージなし',
|
||||||
'noresult.gpuservice.storage.subTitle':
|
'noresult.gpuservice.storage.subTitle':
|
||||||
'ストレージはまだ追加されていません。',
|
'ストレージはまだ追加されていません。',
|
||||||
'noresult.gpuservice.storage.nofound': '一致するストレージが見つかりません。'
|
'noresult.gpuservice.storage.nofound': '一致するストレージが見つかりません。',
|
||||||
|
'noresult.gpuservice.storageType.title': 'ストレージタイプなし',
|
||||||
|
'noresult.gpuservice.storageType.subTitle':
|
||||||
|
'ストレージタイプはまだ追加されていません。',
|
||||||
|
'noresult.gpuservice.storageType.nofound':
|
||||||
|
'一致するストレージタイプが見つかりません。',
|
||||||
|
'noresult.gpuservice.sshkey.title': 'SSH 公開鍵なし',
|
||||||
|
'noresult.gpuservice.sshkey.subTitle': 'SSH 公開鍵はまだ追加されていません。',
|
||||||
|
'noresult.gpuservice.sshkey.nofound': '一致する SSH 公開鍵が見つかりません。'
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -99,6 +99,7 @@ export default {
|
|||||||
'resources.worker': 'Worker',
|
'resources.worker': 'Worker',
|
||||||
'resources.modelfiles.form.exsting': 'Downloaded',
|
'resources.modelfiles.form.exsting': 'Downloaded',
|
||||||
'resources.modelfiles.form.added': 'Added',
|
'resources.modelfiles.form.added': 'Added',
|
||||||
|
'resources.modelfiles.form.isLora': 'Is LoRA',
|
||||||
'resources.worker.maintenance.title': 'System Maintenance',
|
'resources.worker.maintenance.title': 'System Maintenance',
|
||||||
'resources.worker.maintenance.enable': 'Enter Maintenance Mode',
|
'resources.worker.maintenance.enable': 'Enter Maintenance Mode',
|
||||||
'resources.worker.maintenance.disable': 'Exit Maintenance Mode',
|
'resources.worker.maintenance.disable': 'Exit Maintenance Mode',
|
||||||
|
|||||||
@@ -23,6 +23,13 @@ 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.parameterFormat.default': 'По умолчанию бэкенда',
|
||||||
|
'backend.form.parameterFormat.space': 'Пробел (--key value)',
|
||||||
|
'backend.form.parameterFormat.equal': 'Знак равенства (--key=value)',
|
||||||
|
'backend.form.commonParameters': 'Часто используемые параметры',
|
||||||
|
'backend.form.commonParameters.tips':
|
||||||
|
'Отображаются как подсказки в поле параметров бэкенда при развёртывании.',
|
||||||
'backend.form.versionConfig': 'Конфигурация версий',
|
'backend.form.versionConfig': 'Конфигурация версий',
|
||||||
'backend.form.addParameter': 'Добавить параметр',
|
'backend.form.addParameter': 'Добавить параметр',
|
||||||
'backend.form.noVersion': 'Версии не добавлены',
|
'backend.form.noVersion': 'Версии не добавлены',
|
||||||
|
|||||||
@@ -42,6 +42,8 @@ export default {
|
|||||||
'Пожалуйста, убедитесь, что выполнены <a href={link} target="_blank">предварительные условия</a> для {label} перед выполнением следующей команды.',
|
'Пожалуйста, убедитесь, что выполнены <a href={link} target="_blank">предварительные условия</a> для {label} перед выполнением следующей команды.',
|
||||||
'clusters.create.addCommand.tips':
|
'clusters.create.addCommand.tips':
|
||||||
'На воркере, который необходимо добавить, выполните следующую команду, чтобы присоединить его к кластеру.',
|
'На воркере, который необходимо добавить, выполните следующую команду, чтобы присоединить его к кластеру.',
|
||||||
|
'clusters.create.addCommand.k8s.tips':
|
||||||
|
'На Kubernetes-кластере, который необходимо зарегистрировать, выполните следующую команду, чтобы создать ресурсы Kubernetes и зарегистрировать этот кластер.',
|
||||||
'cluster.create.checkEnv.tips':
|
'cluster.create.checkEnv.tips':
|
||||||
'Используйте следующую команду для проверки готовности окружения',
|
'Используйте следующую команду для проверки готовности окружения',
|
||||||
'clusters.create.register.tips':
|
'clusters.create.register.tips':
|
||||||
@@ -67,6 +69,9 @@ export default {
|
|||||||
'clusters.addworker.selectCluster.tips':
|
'clusters.addworker.selectCluster.tips':
|
||||||
'Для <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.singleOnly':
|
||||||
|
'The selected vendor is not in this cluster’s GPU vendor overrides — single-select only. Pick a vendor configured in the overrides to enable multi-select.',
|
||||||
'clusters.addworker.checkEnv': 'Проверить окружение',
|
'clusters.addworker.checkEnv': 'Проверить окружение',
|
||||||
'clusters.addworker.specifyArgs': 'Указать аргументы',
|
'clusters.addworker.specifyArgs': 'Указать аргументы',
|
||||||
'clusters.addworker.runCommand': 'Выполнить команду',
|
'clusters.addworker.runCommand': 'Выполнить команду',
|
||||||
@@ -159,7 +164,27 @@ export default {
|
|||||||
'clusters.volume.pvc.readOnly': 'Read Only',
|
'clusters.volume.pvc.readOnly': 'Read Only',
|
||||||
'clusters.volume.configMap.name': 'ConfigMap Name',
|
'clusters.volume.configMap.name': 'ConfigMap Name',
|
||||||
'clusters.volume.configMap.optional': 'Optional',
|
'clusters.volume.configMap.optional': 'Optional',
|
||||||
'clusters.volume.add': 'Add Volume Mount'
|
'clusters.volume.add': 'Add Volume Mount',
|
||||||
|
'clusters.imageCredentials.title': 'Image Credentials',
|
||||||
|
'clusters.imageCredentials.add': 'Add Credential',
|
||||||
|
'clusters.imageCredentials.registry': 'Registry',
|
||||||
|
'clusters.imageCredentials.username': 'Username',
|
||||||
|
'clusters.imageCredentials.password': 'Password',
|
||||||
|
'clusters.nodeSelector.title': 'Node Selector',
|
||||||
|
'clusters.nodeSelector.tip':
|
||||||
|
'Pod nodeSelector applied to every worker DaemonSet — only nodes whose labels match are eligible to run the worker.',
|
||||||
|
'clusters.gpuVendorOverrides.title': 'GPU Vendor Overrides',
|
||||||
|
'clusters.gpuVendorOverrides.validate.emptySelector':
|
||||||
|
'The override for {vendor} needs at least one nodeSelector entry — an empty override would leave the multi-vendor manifest unable to target nodes for this runtime.',
|
||||||
|
'clusters.gpuVendorOverrides.validate.duplicate':
|
||||||
|
'Overrides for {v1} and {v2} use the same nodeSelector. Each vendor must scope to a distinct node set, otherwise their worker DaemonSets would compete for the same nodes.',
|
||||||
|
'clusters.gpuVendorOverrides.validate.keyConflict':
|
||||||
|
'Override for {vendor} reuses key(s) {keys} from the base Node Selector — the CPU worker would simultaneously require and forbid the key and never schedule.',
|
||||||
|
'clusters.gpuVendorOverrides.tip':
|
||||||
|
'Required when running multiple GPU vendors in one cluster. Each entry pins that vendor’s worker DaemonSet to nodes matching its nodeSelector, and the CPU worker is steered away from those nodes via a DoesNotExist node-affinity.',
|
||||||
|
'clusters.gpuVendorOverrides.add': 'Add Override',
|
||||||
|
'clusters.gpuVendorOverrides.vendor': 'GPU Vendor',
|
||||||
|
'clusters.gpuVendorOverrides.nodeSelector': 'Node Selector'
|
||||||
};
|
};
|
||||||
|
|
||||||
// ========== To-Do: Translate Keys (Remove After Translation) ==========
|
// ========== To-Do: Translate Keys (Remove After Translation) ==========
|
||||||
|
|||||||
@@ -52,10 +52,13 @@ export default {
|
|||||||
'common.button.authorize': 'Настройка прав',
|
'common.button.authorize': 'Настройка прав',
|
||||||
'common.button.confirm': 'Подтвердить',
|
'common.button.confirm': 'Подтвердить',
|
||||||
'common.button.viewlog': 'Просмотр логов',
|
'common.button.viewlog': 'Просмотр логов',
|
||||||
|
'common.button.viewevent': 'Просмотр событий',
|
||||||
|
'common.button.recreate': 'Пересоздать',
|
||||||
'common.table.operation': 'Действия',
|
'common.table.operation': 'Действия',
|
||||||
'common.table.createTime': 'Создано',
|
'common.table.createTime': 'Создано',
|
||||||
'common.table.updateTime': 'Обновлено',
|
'common.table.updateTime': 'Обновлено',
|
||||||
'common.table.description': 'Описание',
|
'common.table.description': 'Описание',
|
||||||
|
'common.table.displayName': 'Отображаемое имя',
|
||||||
'common.table.name': 'Название',
|
'common.table.name': 'Название',
|
||||||
'common.table.status': 'Статус',
|
'common.table.status': 'Статус',
|
||||||
'common.table.name.list': 'Название {type}',
|
'common.table.name.list': 'Название {type}',
|
||||||
@@ -221,7 +224,6 @@ export default {
|
|||||||
'common.text.latest': 'Последняя',
|
'common.text.latest': 'Последняя',
|
||||||
'common.text.new': 'Новая',
|
'common.text.new': 'Новая',
|
||||||
'common.text.changelog': 'История изменений',
|
'common.text.changelog': 'История изменений',
|
||||||
'common.button.recreate': 'Пересоздать',
|
|
||||||
'common.button.delrecreate': 'Удалить (Пересоздать)',
|
'common.button.delrecreate': 'Удалить (Пересоздать)',
|
||||||
'common.options.all': 'Все',
|
'common.options.all': 'Все',
|
||||||
'common.options.none': 'Нет',
|
'common.options.none': 'Нет',
|
||||||
@@ -280,7 +282,9 @@ export default {
|
|||||||
'common.file.format.limit': 'Invalid file format. Allowed: {formats}.',
|
'common.file.format.limit': 'Invalid file format. Allowed: {formats}.',
|
||||||
'common.image.limit.width': 'Image width must be {width}.',
|
'common.image.limit.width': 'Image width must be {width}.',
|
||||||
'common.image.limit.height': 'Image height must be {height}.',
|
'common.image.limit.height': 'Image height must be {height}.',
|
||||||
'common.max': 'Макс. {count}'
|
'common.remaining': 'Остаток {count}',
|
||||||
|
'common.max': 'Макс. {count}',
|
||||||
|
'common.validate.group': 'Please complete the {group} configuration'
|
||||||
};
|
};
|
||||||
|
|
||||||
// ========== To-Do: Translate Keys (Remove After Translation) ==========
|
// ========== To-Do: Translate Keys (Remove After Translation) ==========
|
||||||
|
|||||||
@@ -1,31 +1,22 @@
|
|||||||
export default {
|
export default {
|
||||||
'dashboard.title': 'Панель управления',
|
|
||||||
'dashboard.workers': 'Рабочие узлы',
|
'dashboard.workers': 'Рабочие узлы',
|
||||||
'dashboard.models': 'Модели',
|
'dashboard.deployments': 'Deployments',
|
||||||
'dashboard.totalgpus': 'Всего GPU',
|
'dashboard.totalgpus': 'Всего GPU',
|
||||||
'dashboard.allocategpus': 'Выделенные GPU',
|
|
||||||
'dashboard.instances': 'Инстансы',
|
|
||||||
'dashboard.systemload': 'Нагрузка системы',
|
'dashboard.systemload': 'Нагрузка системы',
|
||||||
'dashboard.memory': 'ОЗУ',
|
'dashboard.memory': 'ОЗУ',
|
||||||
'dashboard.disk': 'Хранилище',
|
|
||||||
'dashboard.vram': 'VRAM',
|
'dashboard.vram': 'VRAM',
|
||||||
'dashboard.cpuutilization': 'Средняя загрузка CPU',
|
'dashboard.cpuutilization': 'Средняя загрузка CPU',
|
||||||
'dashboard.memoryutilization': 'Средняя загрузка ОЗУ',
|
'dashboard.memoryutilization': 'Средняя загрузка ОЗУ',
|
||||||
'dashboard.diskutilization': 'Использование хранилища',
|
|
||||||
'dashboard.vramutilization': 'Средняя загрузка видеопамяти',
|
'dashboard.vramutilization': 'Средняя загрузка видеопамяти',
|
||||||
'dashboard.gpuutilization': 'Средняя загрузка GPU',
|
'dashboard.gpuutilization': 'Средняя загрузка GPU',
|
||||||
'dashboard.usage': 'Использование',
|
'dashboard.usage': 'Использование',
|
||||||
'dashboard.apirequest': 'API-запросы',
|
'dashboard.usage.title': 'Использование за последние {days} дн.',
|
||||||
|
'dashboard.usage.others': 'Прочее',
|
||||||
'dashboard.tokens': 'Использование токенов',
|
'dashboard.tokens': 'Использование токенов',
|
||||||
'dashboard.topusers': 'Топ пользователей',
|
'dashboard.topusers': 'Топ пользователей',
|
||||||
'dashboard.activeModels': 'Активные модели',
|
'dashboard.activeDeployments': 'Active Deployments',
|
||||||
'dashboard.activeUsers': 'Активные пользователи',
|
'dashboard.usageByModel': 'Использование по моделям',
|
||||||
'dashboard.tokenUsageByModel': 'Использование токенов по моделям',
|
|
||||||
'dashboard.apiRequestsByModel': 'API-запросы по моделям',
|
|
||||||
'dashboard.topTokenUsageByUser': 'Топ-10 пользователей по токенам',
|
'dashboard.topTokenUsageByUser': 'Топ-10 пользователей по токенам',
|
||||||
'dashboard.topTokenUsageByApiKey': 'Топ-10 API-ключей по токенам',
|
|
||||||
'dashboard.runninginstances': 'Запущенные инстансы',
|
|
||||||
'dashboard.activeModels.name': 'Название модели',
|
|
||||||
'dashboard.allocatevram': 'Выделено VRAM / ОЗУ',
|
'dashboard.allocatevram': 'Выделено VRAM / ОЗУ',
|
||||||
'dashboard.usage.selectuser': 'Выбрать пользователей',
|
'dashboard.usage.selectuser': 'Выбрать пользователей',
|
||||||
'dashboard.usage.selectmodel': 'Выбрать модели',
|
'dashboard.usage.selectmodel': 'Выбрать модели',
|
||||||
|
|||||||
@@ -16,9 +16,20 @@ export default {
|
|||||||
'gpuservice.template.mountPath': 'Путь монтирования',
|
'gpuservice.template.mountPath': 'Путь монтирования',
|
||||||
'gpuservice.template.containerDisk': 'Диск контейнера (GB)',
|
'gpuservice.template.containerDisk': 'Диск контейнера (GB)',
|
||||||
'gpuservice.template.memory': 'Память (GB)',
|
'gpuservice.template.memory': 'Память (GB)',
|
||||||
|
'gpuservice.instance.containerDisk.remaining':
|
||||||
|
'Диск контейнера (Макс. {count} GB)',
|
||||||
|
'gpuservice.instance.memory.remaining': 'Память (Макс. {count} GB)',
|
||||||
|
'gpuservice.template.displayName': 'Отображаемое имя',
|
||||||
|
'gpuservice.template.displayName.max':
|
||||||
|
'Отображаемое имя не должно превышать 63 символа.',
|
||||||
'gpuservice.template.ports': 'Порты',
|
'gpuservice.template.ports': 'Порты',
|
||||||
'gpuservice.template.ports.add': 'Добавить порт',
|
'gpuservice.template.ports.add': 'Добавить порт',
|
||||||
'gpuservice.template.ports.invalid': 'Заполните настройки портов полностью.',
|
'gpuservice.template.ports.invalid': 'Заполните настройки портов полностью.',
|
||||||
|
'gpuservice.template.ports.name': 'Имя',
|
||||||
|
'gpuservice.template.ports.name.max':
|
||||||
|
'Имя порта не должно превышать 16 символов.',
|
||||||
|
'gpuservice.template.ports.name.duplicate':
|
||||||
|
'Имена портов должны быть уникальными.',
|
||||||
'gpuservice.template.env': 'Переменные окружения',
|
'gpuservice.template.env': 'Переменные окружения',
|
||||||
'gpuservice.template.env.add': 'Добавить переменную окружения',
|
'gpuservice.template.env.add': 'Добавить переменную окружения',
|
||||||
'gpuservice.template.env.invalid':
|
'gpuservice.template.env.invalid':
|
||||||
@@ -29,7 +40,51 @@ export default {
|
|||||||
'gpuservice.template.card.mount': 'Монтирование',
|
'gpuservice.template.card.mount': 'Монтирование',
|
||||||
'gpuservice.template.card.resources': 'Ресурсы',
|
'gpuservice.template.card.resources': 'Ресурсы',
|
||||||
'gpuservice.template.card.ports': 'Порты',
|
'gpuservice.template.card.ports': 'Порты',
|
||||||
|
'gpuservice.storageType': 'Тип хранилища',
|
||||||
|
'gpuservice.storageType.add': 'Добавить тип хранилища',
|
||||||
|
'gpuservice.storageType.edit': 'Изменить тип хранилища',
|
||||||
|
'gpuservice.storageType.filter.name': 'Поиск по имени',
|
||||||
|
'gpuservice.storageType.kind': 'Тип',
|
||||||
|
'gpuservice.storageType.mountOptions': 'Параметры монтирования',
|
||||||
|
'gpuservice.storageType.nfs.server': 'Сервер NFS',
|
||||||
|
'gpuservice.storageType.nfs.server.tips':
|
||||||
|
'Убедитесь, что адрес NFS-сервера доступен из всех кластеров Kubernetes.',
|
||||||
|
'gpuservice.storageType.nfs.share': 'Путь общего ресурса',
|
||||||
|
'gpuservice.storageType.nfs.share.tips':
|
||||||
|
'В этом общем пути будет автоматически создан каталог на основе названия организации и названия хранилища. Если указан подкаталог, итоговый каталог будет создан внутри него.',
|
||||||
|
'gpuservice.storageType.nfs.subDirectory': 'Подкаталог',
|
||||||
|
'gpuservice.storageType.nfs.subDirectory.tips':
|
||||||
|
'Если поле пустое, будет создан подкаталог с именем постоянного тома. Если задано, под этим подкаталогом будет создан каталог с именем постоянного тома.',
|
||||||
|
'gpuservice.storageType.nfs.mountPermissions': 'Права монтирования',
|
||||||
|
'gpuservice.storageType.nfs.mountPermissions.tips':
|
||||||
|
'Наследует права файлов с NFS-сервера.',
|
||||||
|
'gpuservice.storageType.s3.endpoint': 'Endpoint',
|
||||||
|
'gpuservice.storageType.s3.endpoint.tips':
|
||||||
|
'Убедитесь, что S3 endpoint доступен из всех кластеров Kubernetes.',
|
||||||
|
'gpuservice.storageType.s3.endpoint.rule':
|
||||||
|
'Должен начинаться с http или https',
|
||||||
|
'gpuservice.storageType.s3.region': 'Регион',
|
||||||
|
'gpuservice.storageType.s3.bucket': 'Бакет',
|
||||||
|
'gpuservice.storageType.s3.bucket.tips':
|
||||||
|
'Если поле пустое, будет создан новый бакет с именем постоянного тома. Если задано, в этом бакете будет создан подкаталог с именем постоянного тома.',
|
||||||
|
'gpuservice.storageType.s3.bucket.tips1':
|
||||||
|
'В этом бакете будет автоматически создан префикс на основе названия организации и названия хранилища.',
|
||||||
|
'gpuservice.storageType.s3.bucket.tips2':
|
||||||
|
'Например, если организация называется <span class="desc-block">awesome-group</span>, а хранилище — <span class="desc-block">storage-1</span>, итоговый префикс будет: <span class="desc-block">awesome-group/storage-1</span>.',
|
||||||
|
'gpuservice.storageType.s3.accessKey': 'Access Key',
|
||||||
|
'gpuservice.storageType.s3.secretKey': 'Secret Key',
|
||||||
|
'gpuservice.storageType.s3.insecure':
|
||||||
|
'Пропустить проверку сертификата TLS/SSL',
|
||||||
|
'gpuservice.storageType.s3.insecure.tips':
|
||||||
|
'Если включено, сертификат сервера S3 не проверяется. Подходит для внутреннего тестирования или самоподписанных сертификатов; в производственной среде включайте с осторожностью.',
|
||||||
|
'gpuservice.publicKey': 'Открытый ключ SSH',
|
||||||
|
'gpuservice.publicKey.add': 'Добавить открытый ключ SSH',
|
||||||
|
'gpuservice.publicKey.edit': 'Изменить открытый ключ SSH',
|
||||||
|
'gpuservice.publicKey.filter.name': 'Поиск по имени',
|
||||||
'gpuservice.publicKey.label': 'Открытый ключ SSH',
|
'gpuservice.publicKey.label': 'Открытый ключ SSH',
|
||||||
|
'gpuservice.instance.ssh.enable': 'Включить SSH-доступ',
|
||||||
|
'gpuservice.instance.ssh.assignKey': 'Назначить открытый ключ SSH',
|
||||||
|
'gpuservice.instance.ssh.addKey': 'Добавить открытый ключ SSH',
|
||||||
'gpuservice.publicKey.placeholder':
|
'gpuservice.publicKey.placeholder':
|
||||||
'Начинается с ssh-rsa или ssh-ed25519, по одному открытому ключу на строку\n\nПросмотр открытого ключа:\n- RSA\ncat ~/.ssh/id_rsa.pub\n- Ed25519\ncat ~/.ssh/id_ed25519.pub',
|
'Начинается с ssh-rsa или ssh-ed25519, по одному открытому ключу на строку\n\nПросмотр открытого ключа:\n- RSA\ncat ~/.ssh/id_rsa.pub\n- Ed25519\ncat ~/.ssh/id_ed25519.pub',
|
||||||
'gpuservice.instance': 'Экземпляр GPU',
|
'gpuservice.instance': 'Экземпляр GPU',
|
||||||
@@ -47,20 +102,35 @@ export default {
|
|||||||
'gpuservice.instance.type.required': 'Выберите тип экземпляра',
|
'gpuservice.instance.type.required': 'Выберите тип экземпляра',
|
||||||
'gpuservice.instance.gpuCount': 'Количество GPU',
|
'gpuservice.instance.gpuCount': 'Количество GPU',
|
||||||
'gpuservice.instance.gpuCount.required': 'Введите количество GPU',
|
'gpuservice.instance.gpuCount.required': 'Введите количество GPU',
|
||||||
'gpuservice.instance.gpuCount.max':
|
'gpuservice.instance.gpuCount.max': 'Выберите максимум {count} GPU-карт',
|
||||||
'Текущий тип экземпляра поддерживает максимум {count} GPU',
|
'gpuservice.instance.gpuCount.min': 'Выберите минимум {count} GPU-карт',
|
||||||
|
'gpuservice.instance.gpuCount.noAvailable':
|
||||||
|
'Нет доступных ресурсов GPU, выберите другой тип экземпляра.',
|
||||||
|
'gpuservice.instance.gpuCount.zero': 'Только CPU, для подготовки окружения.',
|
||||||
'gpuservice.instance.stock': 'Остаток',
|
'gpuservice.instance.stock': 'Остаток',
|
||||||
'gpuservice.instance.sliced': 'Разделено',
|
'gpuservice.instance.sliced': 'Разделено',
|
||||||
'gpuservice.instance.memory': 'Память',
|
'gpuservice.instance.memory': 'Память',
|
||||||
'gpuservice.instance.ram': 'RAM',
|
'gpuservice.instance.ram': 'RAM',
|
||||||
'gpuservice.instance.search.type.placeholder':
|
'gpuservice.instance.disk': 'Диск',
|
||||||
'Поиск по имени, VRAM, памяти или vCPU',
|
'gpuservice.instance.search.type.placeholder': 'Поиск по имени',
|
||||||
'gpuservice.instance.search.template.placeholder':
|
'gpuservice.instance.search.template.placeholder':
|
||||||
'Поиск по имени шаблона, образу или пути монтирования',
|
'Поиск по имени шаблона, образу или пути монтирования',
|
||||||
'gpuservice.instance.template.image': 'Образ',
|
'gpuservice.instance.template.image': 'Образ',
|
||||||
'gpuservice.instance.template.mount': 'Монтирование',
|
'gpuservice.instance.template.mount': 'Монтирование',
|
||||||
'gpuservice.instance.connect': 'Подключение',
|
'gpuservice.instance.connect': 'Подключение',
|
||||||
'gpuservice.instance.connect.copySshCommand': 'Скопировать команду SSH',
|
'gpuservice.instance.connect.copySshCommand': 'Скопировать команду SSH',
|
||||||
|
'gpuservice.instance.event.reason': 'Причина',
|
||||||
|
'gpuservice.instance.event.message': 'Сообщение',
|
||||||
|
'gpuservice.instance.event.source': 'Источник',
|
||||||
|
'gpuservice.instance.event.count': 'Кол-во',
|
||||||
|
'gpuservice.instance.event.lastSeen': 'Последнее событие',
|
||||||
|
'gpuservice.instance.event.recentHourTip':
|
||||||
|
'Отображаются только события за последний час',
|
||||||
|
'gpuservice.instance.event.tab.instance': 'События экземпляра',
|
||||||
|
'gpuservice.instance.event.tab.volume': 'События тома',
|
||||||
|
'gpuservice.instance.recreate.confirm.title': 'Подтвердить пересоздание',
|
||||||
|
'gpuservice.instance.recreate.confirm.content':
|
||||||
|
'Текущий экземпляр будет сначала удалён, а затем пересоздан с текущей конфигурацией.\n <span style="font-size: 13px;font-weight: 700">{name}</span>',
|
||||||
'gpuservice.storage': 'Хранилище',
|
'gpuservice.storage': 'Хранилище',
|
||||||
'gpuservice.storage.add': 'Добавить хранилище',
|
'gpuservice.storage.add': 'Добавить хранилище',
|
||||||
'gpuservice.storage.edit': 'Редактировать хранилище',
|
'gpuservice.storage.edit': 'Редактировать хранилище',
|
||||||
@@ -75,7 +145,17 @@ export default {
|
|||||||
'gpuservice.storage.temporary': 'Временное',
|
'gpuservice.storage.temporary': 'Временное',
|
||||||
'gpuservice.storage.persistentVolume': 'Постоянный том',
|
'gpuservice.storage.persistentVolume': 'Постоянный том',
|
||||||
'gpuservice.storage.persistentVolume.required': 'Выберите постоянный том',
|
'gpuservice.storage.persistentVolume.required': 'Выберите постоянный том',
|
||||||
'gpuservice.storage.tempCapacity': 'Объём хранилища (ГБ)',
|
'gpuservice.storage.persistentVolume.capacity': 'Ёмкость (ГБ)',
|
||||||
|
'gpuservice.storage.persistentVolume.capacity.required': 'Введите ёмкость',
|
||||||
|
'gpuservice.storage.persistentVolume.releaseWithInstance':
|
||||||
|
'Освобождать вместе с экземпляром',
|
||||||
|
'gpuservice.storage.tempCapacity': 'Объём (ГБ)',
|
||||||
'gpuservice.storage.tempCapacity.required':
|
'gpuservice.storage.tempCapacity.required':
|
||||||
'Введите объём локального временного хранилища'
|
'Введите объём временного хранилища',
|
||||||
|
'gpuservice.form.rule.name':
|
||||||
|
"Строчные буквы, цифры и '-'. Должно начинаться и заканчиваться буквой или цифрой, без подряд идущих '-', максимум 63 символа.",
|
||||||
|
'gpuservice.storage.temporary.tips':
|
||||||
|
'Data is cleared when the instance stops.',
|
||||||
|
'gpuservice.storage.persistentVolume.tips':
|
||||||
|
'Data persists across restarts and is deleted only when the instance is terminated. Cannot be shared with other instances.'
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ export default {
|
|||||||
'menu.gpuService.instances': 'GPU Instances',
|
'menu.gpuService.instances': 'GPU Instances',
|
||||||
'menu.gpuService.templates': 'Instance Templates',
|
'menu.gpuService.templates': 'Instance Templates',
|
||||||
'menu.gpuService.storage': 'Storage',
|
'menu.gpuService.storage': 'Storage',
|
||||||
|
'menu.gpuService.storageTypes': 'Типы хранилищ',
|
||||||
'menu.gpuService.publicKeys': 'SSH Public Keys'
|
'menu.gpuService.publicKeys': 'SSH Public Keys'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ export default {
|
|||||||
'models.form.backend': 'Бэкенд',
|
'models.form.backend': 'Бэкенд',
|
||||||
'models.form.backend_parameters': 'Параметры бэкенда',
|
'models.form.backend_parameters': 'Параметры бэкенда',
|
||||||
'models.instance.params.configured': 'User Configured',
|
'models.instance.params.configured': 'User Configured',
|
||||||
'models.instance.params.autoInjected': 'Автовнедрённые',
|
'models.instance.params.autoInjected': 'Автовнедрённые параметры',
|
||||||
'models.search.gguf.tips':
|
'models.search.gguf.tips':
|
||||||
'GGUF-модели используют llama-box (поддерживает Linux, macOS и Windows).',
|
'GGUF-модели используют llama-box (поддерживает Linux, macOS и Windows).',
|
||||||
'models.search.vllm.tips':
|
'models.search.vllm.tips':
|
||||||
@@ -294,7 +294,13 @@ export default {
|
|||||||
'models.instance.previousRun': 'Previous Run',
|
'models.instance.previousRun': 'Previous Run',
|
||||||
'models.instance.startHistory': 'Run History',
|
'models.instance.startHistory': 'Run History',
|
||||||
'models.instance.startHistory.tips':
|
'models.instance.startHistory.tips':
|
||||||
'Shows logs from the run before the last error-triggered restart.'
|
'Shows logs from the run before the last error-triggered restart.',
|
||||||
|
'models.form.lora.label': 'LoRA Adapter',
|
||||||
|
'models.form.lora.add': 'Add LoRA Adapter',
|
||||||
|
'models.form.lora.select': 'Select LoRA',
|
||||||
|
'models.form.lora.name': 'LoRA name',
|
||||||
|
'models.form.lora.rule.empty': 'Input cannot be empty',
|
||||||
|
'models.form.lora.rule.duplicate': 'LoRA name cannot be duplicated'
|
||||||
};
|
};
|
||||||
|
|
||||||
// ========== To-Do: Translate Keys (Remove After Translation) ==========
|
// ========== To-Do: Translate Keys (Remove After Translation) ==========
|
||||||
@@ -307,5 +313,11 @@ export default {
|
|||||||
// 5. 'models.form.backend.sglang': 'Built-in support for NVIDIA, AMD, Ascend, Moore Threads, MetaX, T-Head PPU devices.',
|
// 5. 'models.form.backend.sglang': 'Built-in support for NVIDIA, AMD, Ascend, Moore Threads, MetaX, T-Head PPU devices.',
|
||||||
// 6. 'models.table.modelView': 'Model List',
|
// 6. 'models.table.modelView': 'Model List',
|
||||||
// 7. 'models.table.instanceView': 'Instance List',
|
// 7. 'models.table.instanceView': 'Instance List',
|
||||||
// 8. 'models.table.category': 'Category'
|
// 8. 'models.table.category': 'Category',
|
||||||
|
// 9. 'models.form.lora.label': 'LoRA Adapter',
|
||||||
|
// 10. 'models.form.lora.add': 'Add LoRA Adapter',
|
||||||
|
// 11. 'models.form.lora.select': 'Select LoRA',
|
||||||
|
// 12. 'models.form.lora.name': 'LoRA name',
|
||||||
|
// 13. 'models.form.lora.rule.empty': 'Input cannot be empty',
|
||||||
|
// 14. 'models.form.lora.rule.duplicate': 'LoRA name cannot be duplicated'
|
||||||
// ========== End of To-Do List ==========
|
// ========== End of To-Do List ==========
|
||||||
|
|||||||
@@ -65,7 +65,16 @@ export default {
|
|||||||
'Подходящие экземпляры GPU не найдены.',
|
'Подходящие экземпляры GPU не найдены.',
|
||||||
'noresult.gpuservice.storage.title': 'Нет хранилищ',
|
'noresult.gpuservice.storage.title': 'Нет хранилищ',
|
||||||
'noresult.gpuservice.storage.subTitle': 'Хранилища ещё не добавлены.',
|
'noresult.gpuservice.storage.subTitle': 'Хранилища ещё не добавлены.',
|
||||||
'noresult.gpuservice.storage.nofound': 'Подходящие хранилища не найдены.'
|
'noresult.gpuservice.storage.nofound': 'Подходящие хранилища не найдены.',
|
||||||
|
'noresult.gpuservice.storageType.title': 'Нет типов хранилищ',
|
||||||
|
'noresult.gpuservice.storageType.subTitle':
|
||||||
|
'Типы хранилищ ещё не добавлены.',
|
||||||
|
'noresult.gpuservice.storageType.nofound':
|
||||||
|
'Подходящие типы хранилищ не найдены.',
|
||||||
|
'noresult.gpuservice.sshkey.title': 'Нет открытых ключей SSH',
|
||||||
|
'noresult.gpuservice.sshkey.subTitle': 'Открытые ключи SSH ещё не добавлены.',
|
||||||
|
'noresult.gpuservice.sshkey.nofound':
|
||||||
|
'Подходящие открытые ключи SSH не найдены.'
|
||||||
};
|
};
|
||||||
|
|
||||||
// ========== To-Do: Translate Keys (Remove After Translation) ==========
|
// ========== To-Do: Translate Keys (Remove After Translation) ==========
|
||||||
|
|||||||
@@ -97,6 +97,7 @@ export default {
|
|||||||
'resources.worker': 'Рабочий узел',
|
'resources.worker': 'Рабочий узел',
|
||||||
'resources.modelfiles.form.exsting': 'Загружено',
|
'resources.modelfiles.form.exsting': 'Загружено',
|
||||||
'resources.modelfiles.form.added': 'Добавлено',
|
'resources.modelfiles.form.added': 'Добавлено',
|
||||||
|
'resources.modelfiles.form.isLora': 'Is LoRA',
|
||||||
'resources.worker.maintenance.title': 'System Maintenance',
|
'resources.worker.maintenance.title': 'System Maintenance',
|
||||||
'resources.worker.maintenance.enable': 'Enter Maintenance Mode',
|
'resources.worker.maintenance.enable': 'Enter Maintenance Mode',
|
||||||
'resources.worker.maintenance.disable': 'Exit Maintenance Mode',
|
'resources.worker.maintenance.disable': 'Exit Maintenance Mode',
|
||||||
|
|||||||
@@ -23,6 +23,13 @@ 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.parameterFormat.default': 'Varsayılan',
|
||||||
|
'backend.form.parameterFormat.space': 'Boşluk (--key value)',
|
||||||
|
'backend.form.parameterFormat.equal': 'Eşittir (--key=value)',
|
||||||
|
'backend.form.commonParameters': 'Yaygın Parametreler',
|
||||||
|
'backend.form.commonParameters.tips':
|
||||||
|
'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ı',
|
||||||
'backend.form.addParameter': 'Parametre Ekle',
|
'backend.form.addParameter': 'Parametre Ekle',
|
||||||
'backend.form.noVersion': 'Sürüm eklenmedi',
|
'backend.form.noVersion': 'Sürüm eklenmedi',
|
||||||
|
|||||||
@@ -42,6 +42,8 @@ export default {
|
|||||||
'Aşağıdaki komutu çalıştırmadan önce lütfen {label} için <a href={link} target="_blank">ön koşulların</a> karşılandığından emin olun.',
|
'Aşağıdaki komutu çalıştırmadan önce lütfen {label} için <a href={link} target="_blank">ön koşulların</a> karşılandığından emin olun.',
|
||||||
'clusters.create.addCommand.tips':
|
'clusters.create.addCommand.tips':
|
||||||
'Eklenmesi gereken İşçi Düğümde, kümeye katılması için aşağıdaki komutu çalıştırın.',
|
'Eklenmesi gereken İşçi Düğümde, kümeye katılması için aşağıdaki komutu çalıştırın.',
|
||||||
|
'clusters.create.addCommand.k8s.tips':
|
||||||
|
'Kaydedilmesi gereken Kubernetes kümesinde, Kubernetes kaynaklarını oluşturmak ve kümeyi kaydetmek için aşağıdaki komutu çalıştırın.',
|
||||||
'clusters.create.register.tips':
|
'clusters.create.register.tips':
|
||||||
'Eklenmesi gereken Kubernetes kümesinde, düğümlerini kümeye katılması için aşağıdaki komutu çalıştırın.',
|
'Eklenmesi gereken Kubernetes kümesinde, düğümlerini kümeye katılması için aşağıdaki komutu çalıştırın.',
|
||||||
'cluster.create.checkEnv.tips':
|
'cluster.create.checkEnv.tips':
|
||||||
@@ -67,6 +69,9 @@ export default {
|
|||||||
'clusters.addworker.selectCluster.tips':
|
'clusters.addworker.selectCluster.tips':
|
||||||
'<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.singleOnly':
|
||||||
|
'The selected vendor is not in this cluster’s GPU vendor overrides — single-select only. Pick a vendor configured in the overrides to enable multi-select.',
|
||||||
'clusters.addworker.checkEnv': 'Ortamı Kontrol Et',
|
'clusters.addworker.checkEnv': 'Ortamı Kontrol Et',
|
||||||
'clusters.addworker.specifyArgs': 'Argümanları Belirle',
|
'clusters.addworker.specifyArgs': 'Argümanları Belirle',
|
||||||
'clusters.addworker.runCommand': 'Komutu Çalıştır',
|
'clusters.addworker.runCommand': 'Komutu Çalıştır',
|
||||||
@@ -159,5 +164,25 @@ export default {
|
|||||||
'clusters.volume.pvc.readOnly': 'Read Only',
|
'clusters.volume.pvc.readOnly': 'Read Only',
|
||||||
'clusters.volume.configMap.name': 'ConfigMap Name',
|
'clusters.volume.configMap.name': 'ConfigMap Name',
|
||||||
'clusters.volume.configMap.optional': 'Optional',
|
'clusters.volume.configMap.optional': 'Optional',
|
||||||
'clusters.volume.add': 'Add Volume Mount'
|
'clusters.volume.add': 'Add Volume Mount',
|
||||||
|
'clusters.imageCredentials.title': 'Image Credentials',
|
||||||
|
'clusters.imageCredentials.add': 'Add Credential',
|
||||||
|
'clusters.imageCredentials.registry': 'Registry',
|
||||||
|
'clusters.imageCredentials.username': 'Username',
|
||||||
|
'clusters.imageCredentials.password': 'Password',
|
||||||
|
'clusters.nodeSelector.title': 'Node Selector',
|
||||||
|
'clusters.nodeSelector.tip':
|
||||||
|
'Pod nodeSelector applied to every worker DaemonSet — only nodes whose labels match are eligible to run the worker.',
|
||||||
|
'clusters.gpuVendorOverrides.title': 'GPU Vendor Overrides',
|
||||||
|
'clusters.gpuVendorOverrides.validate.emptySelector':
|
||||||
|
'The override for {vendor} needs at least one nodeSelector entry — an empty override would leave the multi-vendor manifest unable to target nodes for this runtime.',
|
||||||
|
'clusters.gpuVendorOverrides.validate.duplicate':
|
||||||
|
'Overrides for {v1} and {v2} use the same nodeSelector. Each vendor must scope to a distinct node set, otherwise their worker DaemonSets would compete for the same nodes.',
|
||||||
|
'clusters.gpuVendorOverrides.validate.keyConflict':
|
||||||
|
'Override for {vendor} reuses key(s) {keys} from the base Node Selector — the CPU worker would simultaneously require and forbid the key and never schedule.',
|
||||||
|
'clusters.gpuVendorOverrides.tip':
|
||||||
|
'Required when running multiple GPU vendors in one cluster. Each entry pins that vendor’s worker DaemonSet to nodes matching its nodeSelector, and the CPU worker is steered away from those nodes via a DoesNotExist node-affinity.',
|
||||||
|
'clusters.gpuVendorOverrides.add': 'Add Override',
|
||||||
|
'clusters.gpuVendorOverrides.vendor': 'GPU Vendor',
|
||||||
|
'clusters.gpuVendorOverrides.nodeSelector': 'Node Selector'
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -52,10 +52,13 @@ export default {
|
|||||||
'common.button.authorize': 'Rol Yetkilendirme',
|
'common.button.authorize': 'Rol Yetkilendirme',
|
||||||
'common.button.confirm': 'Onayla',
|
'common.button.confirm': 'Onayla',
|
||||||
'common.button.viewlog': 'Günlükleri Görüntüle',
|
'common.button.viewlog': 'Günlükleri Görüntüle',
|
||||||
|
'common.button.viewevent': 'Olayları Görüntüle',
|
||||||
|
'common.button.recreate': 'Yeniden Oluştur',
|
||||||
'common.table.operation': 'İşlemler',
|
'common.table.operation': 'İşlemler',
|
||||||
'common.table.createTime': 'Oluşturulma',
|
'common.table.createTime': 'Oluşturulma',
|
||||||
'common.table.updateTime': 'Güncellenme',
|
'common.table.updateTime': 'Güncellenme',
|
||||||
'common.table.description': 'Açıklama',
|
'common.table.description': 'Açıklama',
|
||||||
|
'common.table.displayName': 'Görünen Ad',
|
||||||
'common.table.name': 'Ad',
|
'common.table.name': 'Ad',
|
||||||
'common.table.status': 'Durum',
|
'common.table.status': 'Durum',
|
||||||
'common.table.name.list': '{type} Adı',
|
'common.table.name.list': '{type} Adı',
|
||||||
@@ -226,7 +229,6 @@ export default {
|
|||||||
'common.text.latest': 'En Son',
|
'common.text.latest': 'En Son',
|
||||||
'common.text.new': 'Yeni',
|
'common.text.new': 'Yeni',
|
||||||
'common.text.changelog': 'Sürüm Notları',
|
'common.text.changelog': 'Sürüm Notları',
|
||||||
'common.button.recreate': 'Yeniden oluştur',
|
|
||||||
'common.button.delrecreate': 'Sil (Yeniden oluştur)',
|
'common.button.delrecreate': 'Sil (Yeniden oluştur)',
|
||||||
'common.options.all': 'Tümü',
|
'common.options.all': 'Tümü',
|
||||||
'common.options.none': 'Hiçbiri',
|
'common.options.none': 'Hiçbiri',
|
||||||
@@ -284,5 +286,7 @@ export default {
|
|||||||
'common.file.format.limit': 'Invalid file format. Allowed: {formats}.',
|
'common.file.format.limit': 'Invalid file format. Allowed: {formats}.',
|
||||||
'common.image.limit.width': 'Image width must be {width}.',
|
'common.image.limit.width': 'Image width must be {width}.',
|
||||||
'common.image.limit.height': 'Image height must be {height}.',
|
'common.image.limit.height': 'Image height must be {height}.',
|
||||||
'common.max': 'Maks. {count}'
|
'common.remaining': 'Kalan {count}',
|
||||||
|
'common.max': 'Maks. {count}',
|
||||||
|
'common.validate.group': 'Please complete the {group} configuration'
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,33 +1,23 @@
|
|||||||
export default {
|
export default {
|
||||||
'dashboard.title': 'Gösterge Paneli',
|
|
||||||
'dashboard.workers': 'İşçi Düğümler',
|
'dashboard.workers': 'İşçi Düğümler',
|
||||||
'dashboard.models': 'Modeller',
|
'dashboard.deployments': 'Deployments',
|
||||||
'dashboard.clusters': 'Kümeler',
|
'dashboard.clusters': 'Kümeler',
|
||||||
'dashboard.totalgpus': "GPU'lar",
|
'dashboard.totalgpus': "GPU'lar",
|
||||||
'dashboard.allocategpus': "Ayrılan GPU'lar",
|
|
||||||
'dashboard.instances': 'Örnekler',
|
|
||||||
'dashboard.systemload': 'Sistem Yükü',
|
'dashboard.systemload': 'Sistem Yükü',
|
||||||
'dashboard.memory': 'RAM',
|
'dashboard.memory': 'RAM',
|
||||||
'dashboard.disk': 'Depolama',
|
|
||||||
'dashboard.vram': 'VRAM',
|
'dashboard.vram': 'VRAM',
|
||||||
'dashboard.cpuutilization': 'Ortalama CPU Kullanımı',
|
'dashboard.cpuutilization': 'Ortalama CPU Kullanımı',
|
||||||
'dashboard.memoryutilization': 'Ortalama RAM Kullanımı',
|
'dashboard.memoryutilization': 'Ortalama RAM Kullanımı',
|
||||||
'dashboard.diskutilization': 'Depolama Kullanımı',
|
|
||||||
'dashboard.vramutilization': 'Ortalama VRAM Kullanımı',
|
'dashboard.vramutilization': 'Ortalama VRAM Kullanımı',
|
||||||
'dashboard.gpuutilization': 'Ortalama GPU Kullanımı',
|
'dashboard.gpuutilization': 'Ortalama GPU Kullanımı',
|
||||||
'dashboard.usage': 'Kullanım',
|
'dashboard.usage': 'Kullanım',
|
||||||
'dashboard.apirequest': 'API İstekleri',
|
'dashboard.usage.title': 'Son {days} günün kullanımı',
|
||||||
|
'dashboard.usage.others': 'Diğer',
|
||||||
'dashboard.tokens': 'Token Kullanımı',
|
'dashboard.tokens': 'Token Kullanımı',
|
||||||
'dashboard.topusers': 'En Aktif Kullanıcılar',
|
'dashboard.topusers': 'En Aktif Kullanıcılar',
|
||||||
'dashboard.activeModels': 'Aktif Modeller',
|
'dashboard.activeDeployments': 'Active Deployments',
|
||||||
'dashboard.activeUsers': 'Aktif Kullanıcılar',
|
'dashboard.usageByModel': 'Modele Göre Kullanım',
|
||||||
'dashboard.tokenUsageByModel': 'Modele Göre Token Kullanımı',
|
|
||||||
'dashboard.apiRequestsByModel': 'Modele Göre API İstekleri',
|
|
||||||
'dashboard.topTokenUsageByUser': 'Kullanıcıya Göre İlk 10 Token Kullanımı',
|
'dashboard.topTokenUsageByUser': 'Kullanıcıya Göre İlk 10 Token Kullanımı',
|
||||||
'dashboard.topTokenUsageByApiKey':
|
|
||||||
'API Anahtarına Göre İlk 10 Token Kullanımı',
|
|
||||||
'dashboard.runninginstances': 'Çalışan Örnekler',
|
|
||||||
'dashboard.activeModels.name': 'Model Adı',
|
|
||||||
'dashboard.allocatevram': 'Ayrılan VRAM / RAM',
|
'dashboard.allocatevram': 'Ayrılan VRAM / RAM',
|
||||||
'dashboard.usage.selectuser': 'Kullanıcı seçin',
|
'dashboard.usage.selectuser': 'Kullanıcı seçin',
|
||||||
'dashboard.usage.selectmodel': 'Model seçin',
|
'dashboard.usage.selectmodel': 'Model seçin',
|
||||||
|
|||||||
@@ -15,10 +15,21 @@ export default {
|
|||||||
'gpuservice.template.mountPath': 'Bağlama Yolu',
|
'gpuservice.template.mountPath': 'Bağlama Yolu',
|
||||||
'gpuservice.template.containerDisk': 'Konteyner Diski (GB)',
|
'gpuservice.template.containerDisk': 'Konteyner Diski (GB)',
|
||||||
'gpuservice.template.memory': 'Bellek (GB)',
|
'gpuservice.template.memory': 'Bellek (GB)',
|
||||||
|
'gpuservice.instance.containerDisk.remaining':
|
||||||
|
'Konteyner Diski (Maks. {count} GB)',
|
||||||
|
'gpuservice.instance.memory.remaining': 'Bellek (Maks. {count} GB)',
|
||||||
|
'gpuservice.template.displayName': 'Görünen Ad',
|
||||||
|
'gpuservice.template.displayName.max':
|
||||||
|
'Görünen ad 63 karakterden uzun olamaz.',
|
||||||
'gpuservice.template.ports': 'Bağlantı Noktaları',
|
'gpuservice.template.ports': 'Bağlantı Noktaları',
|
||||||
'gpuservice.template.ports.add': 'Bağlantı Noktası Ekle',
|
'gpuservice.template.ports.add': 'Bağlantı Noktası Ekle',
|
||||||
'gpuservice.template.ports.invalid':
|
'gpuservice.template.ports.invalid':
|
||||||
'Bağlantı noktası yapılandırmasını eksiksiz doldurun.',
|
'Bağlantı noktası yapılandırmasını eksiksiz doldurun.',
|
||||||
|
'gpuservice.template.ports.name': 'Ad',
|
||||||
|
'gpuservice.template.ports.name.max':
|
||||||
|
'Bağlantı noktası adı 16 karakterden uzun olamaz.',
|
||||||
|
'gpuservice.template.ports.name.duplicate':
|
||||||
|
'Bağlantı noktası adları benzersiz olmalıdır.',
|
||||||
'gpuservice.template.env': 'Ortam Değişkenleri',
|
'gpuservice.template.env': 'Ortam Değişkenleri',
|
||||||
'gpuservice.template.env.add': 'Ortam Değişkeni Ekle',
|
'gpuservice.template.env.add': 'Ortam Değişkeni Ekle',
|
||||||
'gpuservice.template.env.invalid': 'Ortam değişkenlerini eksiksiz doldurun.',
|
'gpuservice.template.env.invalid': 'Ortam değişkenlerini eksiksiz doldurun.',
|
||||||
@@ -28,7 +39,49 @@ export default {
|
|||||||
'gpuservice.template.card.mount': 'Bağlama',
|
'gpuservice.template.card.mount': 'Bağlama',
|
||||||
'gpuservice.template.card.resources': 'Kaynaklar',
|
'gpuservice.template.card.resources': 'Kaynaklar',
|
||||||
'gpuservice.template.card.ports': 'Bağlantı Noktaları',
|
'gpuservice.template.card.ports': 'Bağlantı Noktaları',
|
||||||
|
'gpuservice.storageType': 'Depolama Türü',
|
||||||
|
'gpuservice.storageType.add': 'Depolama Türü Ekle',
|
||||||
|
'gpuservice.storageType.edit': 'Depolama Türünü Düzenle',
|
||||||
|
'gpuservice.storageType.filter.name': 'Ada göre ara',
|
||||||
|
'gpuservice.storageType.kind': 'Tür',
|
||||||
|
'gpuservice.storageType.mountOptions': 'Bağlama Seçenekleri',
|
||||||
|
'gpuservice.storageType.nfs.server': 'NFS Sunucusu',
|
||||||
|
'gpuservice.storageType.nfs.server.tips':
|
||||||
|
'NFS sunucu adresinin tüm Kubernetes kümelerinden erişilebilir olduğundan emin olun.',
|
||||||
|
'gpuservice.storageType.nfs.share': 'Paylaşım Yolu',
|
||||||
|
'gpuservice.storageType.nfs.share.tips':
|
||||||
|
'Bu paylaşım yolu altında organizasyon ve depolama adlarına dayalı bir dizin otomatik olarak oluşturulur. Bir alt dizin belirtilmişse, oluşturulan dizin o alt dizin altında yer alır.',
|
||||||
|
'gpuservice.storageType.nfs.subDirectory': 'Alt Dizin',
|
||||||
|
'gpuservice.storageType.nfs.subDirectory.tips':
|
||||||
|
'Boş bırakılırsa kalıcı hacim adıyla bir alt dizin oluşturulur. Doldurulursa bu alt dizinin altında kalıcı hacim adıyla bir dizin oluşturulur.',
|
||||||
|
'gpuservice.storageType.nfs.mountPermissions': 'Bağlama İzinleri',
|
||||||
|
'gpuservice.storageType.nfs.mountPermissions.tips':
|
||||||
|
'NFS sunucusundaki dosya izinleri devralınır.',
|
||||||
|
'gpuservice.storageType.s3.endpoint': 'Endpoint',
|
||||||
|
'gpuservice.storageType.s3.endpoint.tips':
|
||||||
|
'S3 endpoint adresinin tüm Kubernetes kümelerinden erişilebilir olduğundan emin olun.',
|
||||||
|
'gpuservice.storageType.s3.endpoint.rule': 'http veya https ile başlamalıdır',
|
||||||
|
'gpuservice.storageType.s3.region': 'Bölge',
|
||||||
|
'gpuservice.storageType.s3.bucket': 'Kova',
|
||||||
|
'gpuservice.storageType.s3.bucket.tips':
|
||||||
|
'Boş bırakılırsa kalıcı hacim adıyla yeni bir kova oluşturulur. Doldurulursa bu kova içinde kalıcı hacim adıyla bir alt dizin oluşturulur.',
|
||||||
|
'gpuservice.storageType.s3.bucket.tips1':
|
||||||
|
'Bu kova içinde organizasyon ve depolama adlarına dayalı bir önek dizini otomatik olarak oluşturulur.',
|
||||||
|
'gpuservice.storageType.s3.bucket.tips2':
|
||||||
|
'Örneğin, organizasyon adı <span class="desc-block">awesome-group</span> ve depolama adı <span class="desc-block">storage-1</span> ise, oluşacak önek: <span class="desc-block">awesome-group/storage-1</span>.',
|
||||||
|
'gpuservice.storageType.s3.accessKey': 'Access Key',
|
||||||
|
'gpuservice.storageType.s3.secretKey': 'Secret Key',
|
||||||
|
'gpuservice.storageType.s3.insecure': 'TLS/SSL sertifika doğrulamasını atla',
|
||||||
|
'gpuservice.storageType.s3.insecure.tips':
|
||||||
|
'Etkinleştirildiğinde S3 sunucu sertifikası doğrulanmaz. İç ağ testleri veya kendinden imzalı sertifikalar için uygundur; üretim ortamında dikkatli kullanın.',
|
||||||
|
'gpuservice.publicKey': 'SSH Açık Anahtarı',
|
||||||
|
'gpuservice.publicKey.add': 'SSH Açık Anahtarı Ekle',
|
||||||
|
'gpuservice.publicKey.edit': 'SSH Açık Anahtarını Düzenle',
|
||||||
|
'gpuservice.publicKey.filter.name': 'Ada göre ara',
|
||||||
'gpuservice.publicKey.label': 'SSH Açık Anahtarı',
|
'gpuservice.publicKey.label': 'SSH Açık Anahtarı',
|
||||||
|
'gpuservice.instance.ssh.enable': 'SSH Erişimini Etkinleştir',
|
||||||
|
'gpuservice.instance.ssh.assignKey': 'SSH Açık Anahtarı Ata',
|
||||||
|
'gpuservice.instance.ssh.addKey': 'SSH Açık Anahtarı Ekle',
|
||||||
'gpuservice.publicKey.placeholder':
|
'gpuservice.publicKey.placeholder':
|
||||||
'ssh-rsa veya ssh-ed25519 ile başlar, her açık anahtar ayrı bir satırda\n\nAçık anahtarı görüntüle:\n- RSA\ncat ~/.ssh/id_rsa.pub\n- Ed25519\ncat ~/.ssh/id_ed25519.pub',
|
'ssh-rsa veya ssh-ed25519 ile başlar, her açık anahtar ayrı bir satırda\n\nAçık anahtarı görüntüle:\n- RSA\ncat ~/.ssh/id_rsa.pub\n- Ed25519\ncat ~/.ssh/id_ed25519.pub',
|
||||||
'gpuservice.instance': 'GPU Örneği',
|
'gpuservice.instance': 'GPU Örneği',
|
||||||
@@ -46,20 +99,36 @@ export default {
|
|||||||
'gpuservice.instance.type.required': 'Lütfen bir örnek türü seçin',
|
'gpuservice.instance.type.required': 'Lütfen bir örnek türü seçin',
|
||||||
'gpuservice.instance.gpuCount': 'GPU Sayısı',
|
'gpuservice.instance.gpuCount': 'GPU Sayısı',
|
||||||
'gpuservice.instance.gpuCount.required': 'Lütfen GPU sayısını girin',
|
'gpuservice.instance.gpuCount.required': 'Lütfen GPU sayısını girin',
|
||||||
'gpuservice.instance.gpuCount.max':
|
'gpuservice.instance.gpuCount.max': 'En fazla {count} GPU kartı seçin',
|
||||||
'Mevcut örnek türü en fazla {count} GPU destekler',
|
'gpuservice.instance.gpuCount.min': 'En az {count} GPU kartı seçin',
|
||||||
|
'gpuservice.instance.gpuCount.noAvailable':
|
||||||
|
'Kullanılabilir GPU kaynağı yok, lütfen başka bir örnek türü seçin.',
|
||||||
|
'gpuservice.instance.gpuCount.zero': 'Yalnızca CPU, ortam hazırlığı için.',
|
||||||
'gpuservice.instance.stock': 'Stok',
|
'gpuservice.instance.stock': 'Stok',
|
||||||
'gpuservice.instance.sliced': 'Bölünmüş',
|
'gpuservice.instance.sliced': 'Bölünmüş',
|
||||||
'gpuservice.instance.memory': 'Bellek',
|
'gpuservice.instance.memory': 'Bellek',
|
||||||
'gpuservice.instance.ram': 'RAM',
|
'gpuservice.instance.ram': 'RAM',
|
||||||
'gpuservice.instance.search.type.placeholder':
|
'gpuservice.instance.disk': 'Disk',
|
||||||
"Ada, VRAM, belleğe veya vCPU'ya göre ara",
|
'gpuservice.instance.search.type.placeholder': 'Ada göre ara',
|
||||||
'gpuservice.instance.search.template.placeholder':
|
'gpuservice.instance.search.template.placeholder':
|
||||||
'Şablon adına, imaja veya bağlama yoluna göre ara',
|
'Şablon adına, imaja veya bağlama yoluna göre ara',
|
||||||
'gpuservice.instance.template.image': 'İmaj',
|
'gpuservice.instance.template.image': 'İmaj',
|
||||||
'gpuservice.instance.template.mount': 'Bağlama',
|
'gpuservice.instance.template.mount': 'Bağlama',
|
||||||
'gpuservice.instance.connect': 'Bağlan',
|
'gpuservice.instance.connect': 'Bağlan',
|
||||||
'gpuservice.instance.connect.copySshCommand': 'SSH Komutunu Kopyala',
|
'gpuservice.instance.connect.copySshCommand': 'SSH Komutunu Kopyala',
|
||||||
|
'gpuservice.instance.event.reason': 'Neden',
|
||||||
|
'gpuservice.instance.event.message': 'Mesaj',
|
||||||
|
'gpuservice.instance.event.source': 'Kaynak',
|
||||||
|
'gpuservice.instance.event.count': 'Sayı',
|
||||||
|
'gpuservice.instance.event.lastSeen': 'Son Görülen',
|
||||||
|
'gpuservice.instance.event.recentHourTip':
|
||||||
|
'Yalnızca son bir saatteki olaylar gösterilir',
|
||||||
|
'gpuservice.instance.event.tab.instance': 'Örnek Olayları',
|
||||||
|
'gpuservice.instance.event.tab.volume': 'Birim Olayları',
|
||||||
|
'gpuservice.instance.recreate.confirm.title':
|
||||||
|
'Yeniden oluşturma onaylansın mı',
|
||||||
|
'gpuservice.instance.recreate.confirm.content':
|
||||||
|
'Mevcut örnek önce silinecek, ardından mevcut yapılandırmayla yeniden oluşturulacaktır.\n <span style="font-size: 13px;font-weight: 700">{name}</span>',
|
||||||
'gpuservice.storage': 'Depolama',
|
'gpuservice.storage': 'Depolama',
|
||||||
'gpuservice.storage.add': 'Depolama Ekle',
|
'gpuservice.storage.add': 'Depolama Ekle',
|
||||||
'gpuservice.storage.edit': 'Depolamayı Düzenle',
|
'gpuservice.storage.edit': 'Depolamayı Düzenle',
|
||||||
@@ -75,7 +144,18 @@ export default {
|
|||||||
'gpuservice.storage.persistentVolume': 'Kalıcı Hacim',
|
'gpuservice.storage.persistentVolume': 'Kalıcı Hacim',
|
||||||
'gpuservice.storage.persistentVolume.required':
|
'gpuservice.storage.persistentVolume.required':
|
||||||
'Lütfen bir kalıcı hacim seçin',
|
'Lütfen bir kalıcı hacim seçin',
|
||||||
'gpuservice.storage.tempCapacity': 'Depolama Kapasitesi (GB)',
|
'gpuservice.storage.persistentVolume.capacity': 'Kapasite (GB)',
|
||||||
|
'gpuservice.storage.persistentVolume.capacity.required':
|
||||||
|
'Lütfen kapasiteyi girin',
|
||||||
|
'gpuservice.storage.persistentVolume.releaseWithInstance':
|
||||||
|
'Örnekle birlikte serbest bırak',
|
||||||
|
'gpuservice.storage.tempCapacity': 'Kapasite (GB)',
|
||||||
'gpuservice.storage.tempCapacity.required':
|
'gpuservice.storage.tempCapacity.required':
|
||||||
'Lütfen yerel geçici depolama kapasitesini girin'
|
'Lütfen geçici depolama kapasitesini girin',
|
||||||
|
'gpuservice.form.rule.name':
|
||||||
|
"Küçük harfler, rakamlar ve '-'. Harf veya rakamla başlamalı ve bitmeli, ardışık '-' içermemeli, en fazla 63 karakter.",
|
||||||
|
'gpuservice.storage.temporary.tips':
|
||||||
|
'Data is cleared when the instance stops.',
|
||||||
|
'gpuservice.storage.persistentVolume.tips':
|
||||||
|
'Data persists across restarts and is deleted only when the instance is terminated. Cannot be shared with other instances.'
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ export default {
|
|||||||
'menu.gpuService.instances': 'GPU Instances',
|
'menu.gpuService.instances': 'GPU Instances',
|
||||||
'menu.gpuService.templates': 'Instance Templates',
|
'menu.gpuService.templates': 'Instance Templates',
|
||||||
'menu.gpuService.storage': 'Storage',
|
'menu.gpuService.storage': 'Storage',
|
||||||
|
'menu.gpuService.storageTypes': 'Depolama Türleri',
|
||||||
'menu.gpuService.publicKeys': 'SSH Public Keys'
|
'menu.gpuService.publicKeys': 'SSH Public Keys'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ export default {
|
|||||||
'models.form.backend': 'Altyapı',
|
'models.form.backend': 'Altyapı',
|
||||||
'models.form.backend_parameters': 'Altyapı Parametreleri',
|
'models.form.backend_parameters': 'Altyapı Parametreleri',
|
||||||
'models.instance.params.configured': 'User Configured',
|
'models.instance.params.configured': 'User Configured',
|
||||||
'models.instance.params.autoInjected': 'Otomatik Enjekte',
|
'models.instance.params.autoInjected': 'Otomatik Enjekte Edilen Parametreler',
|
||||||
'models.search.gguf.tips':
|
'models.search.gguf.tips':
|
||||||
'GGUF modelleri llama-box kullanır (Linux, macOS ve Windows destekler).',
|
'GGUF modelleri llama-box kullanır (Linux, macOS ve Windows destekler).',
|
||||||
'models.search.vllm.tips':
|
'models.search.vllm.tips':
|
||||||
@@ -290,11 +290,23 @@ export default {
|
|||||||
'models.instance.previousRun': 'Previous Run',
|
'models.instance.previousRun': 'Previous Run',
|
||||||
'models.instance.startHistory': 'Run History',
|
'models.instance.startHistory': 'Run History',
|
||||||
'models.instance.startHistory.tips':
|
'models.instance.startHistory.tips':
|
||||||
'Shows logs from the run before the last error-triggered restart.'
|
'Shows logs from the run before the last error-triggered restart.',
|
||||||
|
'models.form.lora.label': 'LoRA Adapter',
|
||||||
|
'models.form.lora.add': 'Add LoRA Adapter',
|
||||||
|
'models.form.lora.select': 'Select LoRA',
|
||||||
|
'models.form.lora.name': 'LoRA name',
|
||||||
|
'models.form.lora.rule.empty': 'Input cannot be empty',
|
||||||
|
'models.form.lora.rule.duplicate': 'LoRA name cannot be duplicated'
|
||||||
};
|
};
|
||||||
|
|
||||||
// ========== To-Do: Translate Keys (Remove After Translation) ==========
|
// ========== To-Do: Translate Keys (Remove After Translation) ==========
|
||||||
// 1. 'models.table.modelView': 'Model List',
|
// 1. 'models.table.modelView': 'Model List',
|
||||||
// 2. 'models.table.instanceView': 'Instance List',
|
// 2. 'models.table.instanceView': 'Instance List',
|
||||||
// 3. 'models.table.category': 'Category'
|
// 3. 'models.table.category': 'Category',
|
||||||
|
// 4. 'models.form.lora.label': 'LoRA Adapter',
|
||||||
|
// 5. 'models.form.lora.add': 'Add LoRA Adapter',
|
||||||
|
// 6. 'models.form.lora.select': 'Select LoRA',
|
||||||
|
// 7. 'models.form.lora.name': 'LoRA name',
|
||||||
|
// 8. 'models.form.lora.rule.empty': 'Input cannot be empty',
|
||||||
|
// 9. 'models.form.lora.rule.duplicate': 'LoRA name cannot be duplicated'
|
||||||
// ========== End of To-Do List ==========
|
// ========== End of To-Do List ==========
|
||||||
|
|||||||
@@ -63,7 +63,14 @@ export default {
|
|||||||
'noresult.gpuservice.instance.nofound': 'Eşleşen GPU örneği bulunamadı.',
|
'noresult.gpuservice.instance.nofound': 'Eşleşen GPU örneği bulunamadı.',
|
||||||
'noresult.gpuservice.storage.title': 'Depolama Yok',
|
'noresult.gpuservice.storage.title': 'Depolama Yok',
|
||||||
'noresult.gpuservice.storage.subTitle': 'Henüz depolama eklenmedi.',
|
'noresult.gpuservice.storage.subTitle': 'Henüz depolama eklenmedi.',
|
||||||
'noresult.gpuservice.storage.nofound': 'Eşleşen depolama bulunamadı.'
|
'noresult.gpuservice.storage.nofound': 'Eşleşen depolama bulunamadı.',
|
||||||
|
'noresult.gpuservice.storageType.title': 'Depolama Türü Yok',
|
||||||
|
'noresult.gpuservice.storageType.subTitle': 'Henüz depolama türü eklenmedi.',
|
||||||
|
'noresult.gpuservice.storageType.nofound':
|
||||||
|
'Eşleşen depolama türü bulunamadı.',
|
||||||
|
'noresult.gpuservice.sshkey.title': 'SSH Açık Anahtarı Yok',
|
||||||
|
'noresult.gpuservice.sshkey.subTitle': 'Henüz SSH açık anahtarı eklenmedi.',
|
||||||
|
'noresult.gpuservice.sshkey.nofound': 'Eşleşen SSH açık anahtarı bulunamadı.'
|
||||||
};
|
};
|
||||||
|
|
||||||
// ========== To-Do: Translate Keys (Remove After Translation) ==========
|
// ========== To-Do: Translate Keys (Remove After Translation) ==========
|
||||||
|
|||||||
@@ -98,6 +98,7 @@ export default {
|
|||||||
'resources.worker.download.privatekey': 'Özel Anahtarı İndir',
|
'resources.worker.download.privatekey': 'Özel Anahtarı İndir',
|
||||||
'resources.modelfiles.form.exsting': 'İndirilmiş',
|
'resources.modelfiles.form.exsting': 'İndirilmiş',
|
||||||
'resources.modelfiles.form.added': 'Eklenmiş',
|
'resources.modelfiles.form.added': 'Eklenmiş',
|
||||||
|
'resources.modelfiles.form.isLora': 'Is LoRA',
|
||||||
'resources.worker.maintenance.title': 'Sistem Bakımı',
|
'resources.worker.maintenance.title': 'Sistem Bakımı',
|
||||||
'resources.worker.maintenance.enable': 'Bakım Moduna Gir',
|
'resources.worker.maintenance.enable': 'Bakım Moduna Gir',
|
||||||
'resources.worker.maintenance.disable': 'Bakım Modundan Çık',
|
'resources.worker.maintenance.disable': 'Bakım Modundan Çık',
|
||||||
|
|||||||
@@ -23,6 +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.parameterFormat.default': '后端默认',
|
||||||
|
'backend.form.parameterFormat.space': '空格分隔 (--key value)',
|
||||||
|
'backend.form.parameterFormat.equal': '等号连接 (--key=value)',
|
||||||
|
'backend.form.commonParameters': '常用参数',
|
||||||
|
'backend.form.commonParameters.tips': '部署模型时作为后端参数候选项展示。',
|
||||||
'backend.form.versionConfig': '版本配置',
|
'backend.form.versionConfig': '版本配置',
|
||||||
'backend.form.addParameter': '添加参数',
|
'backend.form.addParameter': '添加参数',
|
||||||
'backend.form.noVersion': '未添加版本',
|
'backend.form.noVersion': '未添加版本',
|
||||||
|
|||||||
@@ -41,6 +41,8 @@ export default {
|
|||||||
'在执行以下命令之前,请确保已满足 {label} 的<a href={link} target="_blank">先决条件</a>。',
|
'在执行以下命令之前,请确保已满足 {label} 的<a href={link} target="_blank">先决条件</a>。',
|
||||||
'clusters.create.addCommand.tips':
|
'clusters.create.addCommand.tips':
|
||||||
'在需要添加的节点上运行以下命令,将其加入到集群中。',
|
'在需要添加的节点上运行以下命令,将其加入到集群中。',
|
||||||
|
'clusters.create.addCommand.k8s.tips':
|
||||||
|
'在需要注册的 Kubernetes 集群中运行以下命令,创建 Kubernetes 资源,注册该集群。',
|
||||||
'clusters.create.register.tips':
|
'clusters.create.register.tips':
|
||||||
'在需要添加的 Kubernetes 集群上运行以下命令,将其中的节点加入到集群中。',
|
'在需要添加的 Kubernetes 集群上运行以下命令,将其中的节点加入到集群中。',
|
||||||
'cluster.create.checkEnv.tips': '使用以下命令检查环境是否准备妥当。',
|
'cluster.create.checkEnv.tips': '使用以下命令检查环境是否准备妥当。',
|
||||||
@@ -65,6 +67,9 @@ export default {
|
|||||||
'clusters.addworker.selectCluster.tips':
|
'clusters.addworker.selectCluster.tips':
|
||||||
'<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.singleOnly':
|
||||||
|
'当前所选厂商未在集群的 GPU 厂商覆盖(override node selector)中配置,仅支持单选。如需多选,请先选择 override 中已配置的厂商。',
|
||||||
'clusters.addworker.checkEnv': '检查环境',
|
'clusters.addworker.checkEnv': '检查环境',
|
||||||
'clusters.addworker.specifyArgs': '指定参数',
|
'clusters.addworker.specifyArgs': '指定参数',
|
||||||
'clusters.addworker.runCommand': '运行指令',
|
'clusters.addworker.runCommand': '运行指令',
|
||||||
@@ -151,5 +156,25 @@ export default {
|
|||||||
'clusters.volume.pvc.readOnly': '只读',
|
'clusters.volume.pvc.readOnly': '只读',
|
||||||
'clusters.volume.configMap.name': '配置名称',
|
'clusters.volume.configMap.name': '配置名称',
|
||||||
'clusters.volume.configMap.optional': '可选',
|
'clusters.volume.configMap.optional': '可选',
|
||||||
'clusters.volume.add': '添加卷挂载'
|
'clusters.volume.add': '添加卷挂载',
|
||||||
|
'clusters.imageCredentials.title': '镜像仓库凭证',
|
||||||
|
'clusters.imageCredentials.add': '添加凭证',
|
||||||
|
'clusters.imageCredentials.registry': '镜像仓库地址',
|
||||||
|
'clusters.imageCredentials.username': '用户名',
|
||||||
|
'clusters.imageCredentials.password': '密码',
|
||||||
|
'clusters.nodeSelector.title': '节点选择器',
|
||||||
|
'clusters.nodeSelector.tip':
|
||||||
|
'应用到每个 worker DaemonSet 的 Pod nodeSelector,只有标签匹配的节点才会被调度运行 worker。',
|
||||||
|
'clusters.gpuVendorOverrides.title': 'GPU 厂商覆盖配置',
|
||||||
|
'clusters.gpuVendorOverrides.validate.emptySelector':
|
||||||
|
'{vendor} 的覆盖配置至少需要一项 nodeSelector —— 空覆盖会让多 vendor manifest 无法定位该 runtime 的节点。',
|
||||||
|
'clusters.gpuVendorOverrides.validate.duplicate':
|
||||||
|
'{v1} 与 {v2} 的覆盖配置使用了完全相同的 nodeSelector。每个 vendor 必须对应不同的节点集,否则它们的 worker DaemonSet 会相互争抢同一批节点。',
|
||||||
|
'clusters.gpuVendorOverrides.validate.keyConflict':
|
||||||
|
'{vendor} 的覆盖配置重用了基础节点选择器中已有的 key({keys})—— 这会让 CPU worker 同时"要求"和"禁止"这些 key,永远无法被调度。',
|
||||||
|
'clusters.gpuVendorOverrides.tip':
|
||||||
|
'集群中存在多个 GPU 厂商时必填。每项的 nodeSelector 将该厂商的 worker DaemonSet 固定到匹配的节点,同时 CPU worker 通过 DoesNotExist 节点亲和性避开这些节点。',
|
||||||
|
'clusters.gpuVendorOverrides.add': '添加覆盖配置',
|
||||||
|
'clusters.gpuVendorOverrides.vendor': 'GPU 厂商',
|
||||||
|
'clusters.gpuVendorOverrides.nodeSelector': '节点选择器'
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -50,10 +50,13 @@ export default {
|
|||||||
'common.button.authorize': '角色授权',
|
'common.button.authorize': '角色授权',
|
||||||
'common.button.confirm': '确定',
|
'common.button.confirm': '确定',
|
||||||
'common.button.viewlog': '查看日志',
|
'common.button.viewlog': '查看日志',
|
||||||
|
'common.button.viewevent': '查看事件',
|
||||||
|
'common.button.recreate': '重新创建',
|
||||||
'common.table.operation': '操作',
|
'common.table.operation': '操作',
|
||||||
'common.table.createTime': '创建时间',
|
'common.table.createTime': '创建时间',
|
||||||
'common.table.updateTime': '更新时间',
|
'common.table.updateTime': '更新时间',
|
||||||
'common.table.description': '描述',
|
'common.table.description': '描述',
|
||||||
|
'common.table.displayName': '显示名称',
|
||||||
'common.table.name': '名称',
|
'common.table.name': '名称',
|
||||||
'common.table.status': '状态',
|
'common.table.status': '状态',
|
||||||
'common.table.name.list': '{type}名称',
|
'common.table.name.list': '{type}名称',
|
||||||
@@ -273,5 +276,7 @@ export default {
|
|||||||
'common.file.format.limit': '文件格式不正确,仅支持{formats}。',
|
'common.file.format.limit': '文件格式不正确,仅支持{formats}。',
|
||||||
'common.image.limit.width': '图片宽度须为{width}。',
|
'common.image.limit.width': '图片宽度须为{width}。',
|
||||||
'common.image.limit.height': '图片高度须为{height}。',
|
'common.image.limit.height': '图片高度须为{height}。',
|
||||||
'common.max': '最大 {count}'
|
'common.remaining': '剩余 {count}',
|
||||||
|
'common.max': '最大 {count}',
|
||||||
|
'common.validate.group': '请填写完整的{group}配置'
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,32 +1,23 @@
|
|||||||
export default {
|
export default {
|
||||||
'dashboard.title': '概览',
|
|
||||||
'dashboard.workers': '节点',
|
'dashboard.workers': '节点',
|
||||||
'dashboard.models': '模型',
|
'dashboard.deployments': '部署',
|
||||||
'dashboard.clusters': '集群',
|
'dashboard.clusters': '集群',
|
||||||
'dashboard.totalgpus': 'GPUs',
|
'dashboard.totalgpus': 'GPUs',
|
||||||
'dashboard.allocategpus': '已分配 GPU 数量',
|
|
||||||
'dashboard.instances': '实例',
|
|
||||||
'dashboard.systemload': '系统负载',
|
'dashboard.systemload': '系统负载',
|
||||||
'dashboard.memory': '内存',
|
'dashboard.memory': '内存',
|
||||||
'dashboard.disk': '磁盘',
|
|
||||||
'dashboard.vram': '显存',
|
'dashboard.vram': '显存',
|
||||||
'dashboard.cpuutilization': '平均 CPU 利用率',
|
'dashboard.cpuutilization': '平均 CPU 利用率',
|
||||||
'dashboard.memoryutilization': '平均内存利用率',
|
'dashboard.memoryutilization': '平均内存利用率',
|
||||||
'dashboard.diskutilization': '磁盘利用率',
|
|
||||||
'dashboard.vramutilization': '平均显存利用率',
|
'dashboard.vramutilization': '平均显存利用率',
|
||||||
'dashboard.gpuutilization': '平均 GPU 利用率',
|
'dashboard.gpuutilization': '平均 GPU 利用率',
|
||||||
'dashboard.usage': '使用量',
|
'dashboard.usage': '使用量',
|
||||||
'dashboard.apirequest': 'API 请求',
|
'dashboard.usage.title': '最近 {days} 天使用量',
|
||||||
|
'dashboard.usage.others': '其他',
|
||||||
'dashboard.tokens': 'Token 使用量',
|
'dashboard.tokens': 'Token 使用量',
|
||||||
'dashboard.topusers': '用户排行',
|
'dashboard.topusers': '用户排行',
|
||||||
'dashboard.activeModels': '活跃模型',
|
'dashboard.activeDeployments': '活跃部署',
|
||||||
'dashboard.activeUsers': '活跃用户',
|
'dashboard.usageByModel': '按模型统计使用量',
|
||||||
'dashboard.tokenUsageByModel': '按模型统计 Token 使用量',
|
|
||||||
'dashboard.apiRequestsByModel': '按模型统计 API 请求数',
|
|
||||||
'dashboard.topTokenUsageByUser': '用户 Token 使用量 Top 10',
|
'dashboard.topTokenUsageByUser': '用户 Token 使用量 Top 10',
|
||||||
'dashboard.topTokenUsageByApiKey': 'API 密钥 Token 使用量 Top 10',
|
|
||||||
'dashboard.activeModels.name': '模型名称',
|
|
||||||
'dashboard.runninginstances': '运行实例',
|
|
||||||
'dashboard.allocatevram': '已分配显存 / 内存',
|
'dashboard.allocatevram': '已分配显存 / 内存',
|
||||||
'dashboard.usage.selectuser': '选择用户',
|
'dashboard.usage.selectuser': '选择用户',
|
||||||
'dashboard.usage.selectmodel': '选择模型',
|
'dashboard.usage.selectmodel': '选择模型',
|
||||||
|
|||||||
@@ -15,9 +15,16 @@ export default {
|
|||||||
'gpuservice.template.mountPath': '挂载路径',
|
'gpuservice.template.mountPath': '挂载路径',
|
||||||
'gpuservice.template.containerDisk': '容器磁盘(GB)',
|
'gpuservice.template.containerDisk': '容器磁盘(GB)',
|
||||||
'gpuservice.template.memory': '内存(GB)',
|
'gpuservice.template.memory': '内存(GB)',
|
||||||
|
'gpuservice.instance.containerDisk.remaining': '容器磁盘(最大{count}GB)',
|
||||||
|
'gpuservice.instance.memory.remaining': '内存(最大{count}GB)',
|
||||||
|
'gpuservice.template.displayName': '显示名称',
|
||||||
|
'gpuservice.template.displayName.max': '显示名称不能超过 63 个字符',
|
||||||
'gpuservice.template.ports': '端口',
|
'gpuservice.template.ports': '端口',
|
||||||
'gpuservice.template.ports.add': '添加端口',
|
'gpuservice.template.ports.add': '添加端口',
|
||||||
'gpuservice.template.ports.invalid': '请填写完整的端口配置',
|
'gpuservice.template.ports.invalid': '请填写完整的端口配置',
|
||||||
|
'gpuservice.template.ports.name': '名称',
|
||||||
|
'gpuservice.template.ports.name.max': '端口名称不能超过 16 个字符',
|
||||||
|
'gpuservice.template.ports.name.duplicate': '端口名称不能重复',
|
||||||
'gpuservice.template.env': '环境变量',
|
'gpuservice.template.env': '环境变量',
|
||||||
'gpuservice.template.env.add': '添加环境变量',
|
'gpuservice.template.env.add': '添加环境变量',
|
||||||
'gpuservice.template.env.invalid': '请填写完整的环境变量',
|
'gpuservice.template.env.invalid': '请填写完整的环境变量',
|
||||||
@@ -27,7 +34,49 @@ export default {
|
|||||||
'gpuservice.template.card.mount': '挂载',
|
'gpuservice.template.card.mount': '挂载',
|
||||||
'gpuservice.template.card.resources': '资源',
|
'gpuservice.template.card.resources': '资源',
|
||||||
'gpuservice.template.card.ports': '端口',
|
'gpuservice.template.card.ports': '端口',
|
||||||
|
'gpuservice.storageType': '存储类型',
|
||||||
|
'gpuservice.storageType.add': '添加存储类型',
|
||||||
|
'gpuservice.storageType.edit': '编辑存储类型',
|
||||||
|
'gpuservice.storageType.filter.name': '按名称搜索',
|
||||||
|
'gpuservice.storageType.kind': '类型',
|
||||||
|
'gpuservice.storageType.mountOptions': '挂载参数',
|
||||||
|
'gpuservice.storageType.nfs.server': 'NFS 服务器',
|
||||||
|
'gpuservice.storageType.nfs.server.tips':
|
||||||
|
'确保所有 Kubernetes 集群都能访问该 NFS 服务地址。',
|
||||||
|
'gpuservice.storageType.nfs.share': '共享路径',
|
||||||
|
'gpuservice.storageType.nfs.share.tips':
|
||||||
|
'系统会在该共享路径下自动创建一个基于组织名称和存储名称的目录。如果指定了子目录,则生成的目录会创建在该子目录下。',
|
||||||
|
'gpuservice.storageType.nfs.subDirectory': '子目录',
|
||||||
|
'gpuservice.storageType.nfs.subDirectory.tips':
|
||||||
|
'如果为空,会以持久卷的卷名创建一个子目录;如果不为空,会在同名子目录下,以持久卷的卷名创建一个孙目录。',
|
||||||
|
'gpuservice.storageType.nfs.mountPermissions': '挂载权限',
|
||||||
|
'gpuservice.storageType.nfs.mountPermissions.tips':
|
||||||
|
'沿用 NFS 服务器文件的权限。',
|
||||||
|
'gpuservice.storageType.s3.endpoint': 'Endpoint',
|
||||||
|
'gpuservice.storageType.s3.endpoint.tips':
|
||||||
|
'确保所有 Kubernetes 集群都能访问该 S3 Endpoint。',
|
||||||
|
'gpuservice.storageType.s3.endpoint.rule': '必须以 http 或 https 开头',
|
||||||
|
'gpuservice.storageType.s3.region': '区域',
|
||||||
|
'gpuservice.storageType.s3.bucket': '存储桶',
|
||||||
|
'gpuservice.storageType.s3.bucket.tips':
|
||||||
|
'如果为空,会以持久卷的卷名创建一个新桶;如果不为空,会在同名桶下,以持久卷的卷名创建一个子目录。',
|
||||||
|
'gpuservice.storageType.s3.bucket.tips1':
|
||||||
|
'系统会在该 Bucket 中自动创建一个基于组织名称和存储名称的前缀目录。',
|
||||||
|
'gpuservice.storageType.s3.bucket.tips2':
|
||||||
|
'例如,若组织名称为 <span class="desc-block">awesome-group</span>,存储名称为 <span class="desc-block">storage-1</span>,则生成的前缀为:<span class="desc-block">awesome-group/storage-1</span>。',
|
||||||
|
'gpuservice.storageType.s3.accessKey': 'Access Key',
|
||||||
|
'gpuservice.storageType.s3.secretKey': 'Secret Key',
|
||||||
|
'gpuservice.storageType.s3.insecure': '跳过 TLS/SSL 证书验证',
|
||||||
|
'gpuservice.storageType.s3.insecure.tips':
|
||||||
|
'开启后将忽略 S3 服务端的证书安全检查。适用于内网测试或使用自签名证书的场景,生产环境请谨慎勾选。',
|
||||||
|
'gpuservice.publicKey': 'SSH 公钥',
|
||||||
|
'gpuservice.publicKey.add': '添加 SSH 公钥',
|
||||||
|
'gpuservice.publicKey.edit': '编辑 SSH 公钥',
|
||||||
|
'gpuservice.publicKey.filter.name': '按名称搜索',
|
||||||
'gpuservice.publicKey.label': 'SSH 公钥',
|
'gpuservice.publicKey.label': 'SSH 公钥',
|
||||||
|
'gpuservice.instance.ssh.enable': '启用 SSH 访问',
|
||||||
|
'gpuservice.instance.ssh.assignKey': '分配 SSH 公钥',
|
||||||
|
'gpuservice.instance.ssh.addKey': '添加 SSH 公钥',
|
||||||
'gpuservice.publicKey.placeholder':
|
'gpuservice.publicKey.placeholder':
|
||||||
'以 ssh-rsa 或 ssh-ed25519 开头,每个公钥单独一行\n\n查看公钥:\n- RSA\ncat ~/.ssh/id_rsa.pub\n- Ed25519\ncat ~/.ssh/id_ed25519.pub',
|
'以 ssh-rsa 或 ssh-ed25519 开头,每个公钥单独一行\n\n查看公钥:\n- RSA\ncat ~/.ssh/id_rsa.pub\n- Ed25519\ncat ~/.ssh/id_ed25519.pub',
|
||||||
'gpuservice.instance': 'GPU 实例',
|
'gpuservice.instance': 'GPU 实例',
|
||||||
@@ -45,18 +94,34 @@ export default {
|
|||||||
'gpuservice.instance.type.required': '请选择实例类型',
|
'gpuservice.instance.type.required': '请选择实例类型',
|
||||||
'gpuservice.instance.gpuCount': 'GPU 数量',
|
'gpuservice.instance.gpuCount': 'GPU 数量',
|
||||||
'gpuservice.instance.gpuCount.required': '请输入 GPU 数量',
|
'gpuservice.instance.gpuCount.required': '请输入 GPU 数量',
|
||||||
'gpuservice.instance.gpuCount.max': '当前实例类型最多支持 {count} 个 GPU',
|
'gpuservice.instance.gpuCount.max': '最多选择 {count} 张卡',
|
||||||
|
'gpuservice.instance.gpuCount.min': '至少选择 {count} 张卡',
|
||||||
|
'gpuservice.instance.gpuCount.noAvailable':
|
||||||
|
'没有可用的 GPU 资源,请选择其他实例类型。',
|
||||||
|
'gpuservice.instance.gpuCount.zero': '仅使用 CPU,用于环境准备。',
|
||||||
'gpuservice.instance.stock': '库存',
|
'gpuservice.instance.stock': '库存',
|
||||||
'gpuservice.instance.sliced': '切分',
|
'gpuservice.instance.sliced': '切分',
|
||||||
'gpuservice.instance.memory': '显存',
|
'gpuservice.instance.memory': '显存',
|
||||||
'gpuservice.instance.ram': '内存',
|
'gpuservice.instance.ram': '内存',
|
||||||
'gpuservice.instance.search.type.placeholder': '搜索名称、显存、内存或 vCPU',
|
'gpuservice.instance.disk': '磁盘',
|
||||||
|
'gpuservice.instance.search.type.placeholder': '搜索名称',
|
||||||
'gpuservice.instance.search.template.placeholder':
|
'gpuservice.instance.search.template.placeholder':
|
||||||
'搜索模板名称、镜像或挂载路径',
|
'搜索模板名称、镜像或挂载路径',
|
||||||
'gpuservice.instance.template.image': '镜像',
|
'gpuservice.instance.template.image': '镜像',
|
||||||
'gpuservice.instance.template.mount': '挂载',
|
'gpuservice.instance.template.mount': '挂载',
|
||||||
'gpuservice.instance.connect': '连接',
|
'gpuservice.instance.connect': '连接',
|
||||||
'gpuservice.instance.connect.copySshCommand': '复制 SSH 命令',
|
'gpuservice.instance.connect.copySshCommand': '复制 SSH 命令',
|
||||||
|
'gpuservice.instance.event.reason': '原因',
|
||||||
|
'gpuservice.instance.event.message': '消息',
|
||||||
|
'gpuservice.instance.event.source': '来源',
|
||||||
|
'gpuservice.instance.event.count': '次数',
|
||||||
|
'gpuservice.instance.event.lastSeen': '最近发生',
|
||||||
|
'gpuservice.instance.event.recentHourTip': '仅显示最近一小时的事件。',
|
||||||
|
'gpuservice.instance.event.tab.instance': '实例事件',
|
||||||
|
'gpuservice.instance.event.tab.volume': '存储卷事件',
|
||||||
|
'gpuservice.instance.recreate.confirm.title': '确认重新创建',
|
||||||
|
'gpuservice.instance.recreate.confirm.content':
|
||||||
|
'系统将先删除当前实例,然后使用当前配置重新创建。\n <span style="font-size: 13px;font-weight: 700">{name}</span>',
|
||||||
'gpuservice.storage': '存储',
|
'gpuservice.storage': '存储',
|
||||||
'gpuservice.storage.add': '添加存储',
|
'gpuservice.storage.add': '添加存储',
|
||||||
'gpuservice.storage.edit': '编辑存储',
|
'gpuservice.storage.edit': '编辑存储',
|
||||||
@@ -70,7 +135,15 @@ export default {
|
|||||||
'gpuservice.storage.persistent': '持久',
|
'gpuservice.storage.persistent': '持久',
|
||||||
'gpuservice.storage.temporary': '临时',
|
'gpuservice.storage.temporary': '临时',
|
||||||
'gpuservice.storage.persistentVolume': '持久卷',
|
'gpuservice.storage.persistentVolume': '持久卷',
|
||||||
|
'gpuservice.storage.temporary.tips': '实例停止后,数据将被清除。',
|
||||||
|
'gpuservice.storage.persistentVolume.tips':
|
||||||
|
'数据在实例重启后仍会保留,仅在实例被终止时删除。无法被其它实例共享。',
|
||||||
'gpuservice.storage.persistentVolume.required': '请选择持久卷',
|
'gpuservice.storage.persistentVolume.required': '请选择持久卷',
|
||||||
'gpuservice.storage.tempCapacity': '存储容量 (GB)',
|
'gpuservice.storage.persistentVolume.capacity': '容量(GB)',
|
||||||
'gpuservice.storage.tempCapacity.required': '请输入本地临时存储容量'
|
'gpuservice.storage.persistentVolume.capacity.required': '请输入容量',
|
||||||
|
'gpuservice.storage.persistentVolume.releaseWithInstance': '随实例释放',
|
||||||
|
'gpuservice.storage.tempCapacity': '容量(GB)',
|
||||||
|
'gpuservice.storage.tempCapacity.required': '请输入临时存储容量',
|
||||||
|
'gpuservice.form.rule.name':
|
||||||
|
'由小写字母、数字和 "-" 组成,以字母或数字开头和结尾,不能包含连续的 "-",最多 63 个字符。'
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -45,5 +45,6 @@ export default {
|
|||||||
'menu.gpuService.instances': 'GPU 实例',
|
'menu.gpuService.instances': 'GPU 实例',
|
||||||
'menu.gpuService.templates': '实例模板',
|
'menu.gpuService.templates': '实例模板',
|
||||||
'menu.gpuService.storage': '存储',
|
'menu.gpuService.storage': '存储',
|
||||||
|
'menu.gpuService.storageTypes': '存储类型',
|
||||||
'menu.gpuService.publicKeys': 'SSH 公钥'
|
'menu.gpuService.publicKeys': 'SSH 公钥'
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ export default {
|
|||||||
'models.form.backend': '后端',
|
'models.form.backend': '后端',
|
||||||
'models.form.backend_parameters': '后端参数',
|
'models.form.backend_parameters': '后端参数',
|
||||||
'models.instance.params.configured': '用户配置',
|
'models.instance.params.configured': '用户配置',
|
||||||
'models.instance.params.autoInjected': '自动注入',
|
'models.instance.params.autoInjected': '自动注入参数',
|
||||||
'models.search.gguf.tips':
|
'models.search.gguf.tips':
|
||||||
'GGUF 模型用 llama-box(支持 Linux, macOS 和 Windows)。',
|
'GGUF 模型用 llama-box(支持 Linux, macOS 和 Windows)。',
|
||||||
'models.search.vllm.tips':
|
'models.search.vllm.tips':
|
||||||
@@ -274,5 +274,11 @@ export default {
|
|||||||
'models.instance.previousRun': '上一次运行',
|
'models.instance.previousRun': '上一次运行',
|
||||||
'models.instance.startHistory': '运行记录',
|
'models.instance.startHistory': '运行记录',
|
||||||
'models.instance.startHistory.tips':
|
'models.instance.startHistory.tips':
|
||||||
'显示上一次因错误自动重启之前的那次运行的日志。'
|
'显示上一次因错误自动重启之前的那次运行的日志。',
|
||||||
|
'models.form.lora.label': 'LoRA 适配器',
|
||||||
|
'models.form.lora.add': '添加 LoRA 适配器',
|
||||||
|
'models.form.lora.select': '选择 LoRA',
|
||||||
|
'models.form.lora.name': 'LoRA 名称',
|
||||||
|
'models.form.lora.rule.empty': '输入不能为空',
|
||||||
|
'models.form.lora.rule.duplicate': 'LoRA name 不能重复'
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -59,5 +59,11 @@ export default {
|
|||||||
'noresult.gpuservice.instance.nofound': '未找到匹配的 GPU 实例',
|
'noresult.gpuservice.instance.nofound': '未找到匹配的 GPU 实例',
|
||||||
'noresult.gpuservice.storage.title': '暂无存储',
|
'noresult.gpuservice.storage.title': '暂无存储',
|
||||||
'noresult.gpuservice.storage.subTitle': '尚未添加任何存储。',
|
'noresult.gpuservice.storage.subTitle': '尚未添加任何存储。',
|
||||||
'noresult.gpuservice.storage.nofound': '未找到匹配的存储'
|
'noresult.gpuservice.storage.nofound': '未找到匹配的存储',
|
||||||
|
'noresult.gpuservice.storageType.title': '暂无存储类型',
|
||||||
|
'noresult.gpuservice.storageType.subTitle': '尚未添加任何存储类型。',
|
||||||
|
'noresult.gpuservice.storageType.nofound': '未找到匹配的存储类型',
|
||||||
|
'noresult.gpuservice.sshkey.title': '暂无 SSH 公钥',
|
||||||
|
'noresult.gpuservice.sshkey.subTitle': '尚未添加任何 SSH 公钥。',
|
||||||
|
'noresult.gpuservice.sshkey.nofound': '未找到匹配的 SSH 公钥'
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -96,6 +96,7 @@ export default {
|
|||||||
'resources.worker.download.privatekey': '下载私钥',
|
'resources.worker.download.privatekey': '下载私钥',
|
||||||
'resources.modelfiles.form.exsting': '已下载',
|
'resources.modelfiles.form.exsting': '已下载',
|
||||||
'resources.modelfiles.form.added': '已添加',
|
'resources.modelfiles.form.added': '已添加',
|
||||||
|
'resources.modelfiles.form.isLora': '是否 LoRA',
|
||||||
'resources.worker.maintenance.title': '系统维护',
|
'resources.worker.maintenance.title': '系统维护',
|
||||||
'resources.worker.maintenance.enable': '进入维护模式',
|
'resources.worker.maintenance.enable': '进入维护模式',
|
||||||
'resources.worker.maintenance.disable': '退出维护模式',
|
'resources.worker.maintenance.disable': '退出维护模式',
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import useCoolColors from '@/hooks/use-cool-colors';
|
import useCoolColors from '@/hooks/use-cool-colors';
|
||||||
import { Chart } from '@gpustack/core-ui';
|
import { Chart } from '@gpustack/core-ui';
|
||||||
import { formatLargeNumber } from '@gpustack/core-ui/utils';
|
import { formatLargeNumber } from '@gpustack/core-ui/utils';
|
||||||
import { theme } from 'antd';
|
import { Empty, theme } from 'antd';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import React, { useEffect, useMemo, useRef } from 'react';
|
import React, { useEffect, useMemo, useRef } from 'react';
|
||||||
|
|
||||||
@@ -254,7 +254,19 @@ const BarChart: React.FC<BarChartProps> = (props) => {
|
|||||||
}, [legendIsolate, seriesData, legendData]);
|
}, [legendIsolate, seriesData, legendData]);
|
||||||
|
|
||||||
if (!seriesData.length) {
|
if (!seriesData.length) {
|
||||||
return <div style={{ width: width || '100%', height }} />;
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: width || '100%',
|
||||||
|
height,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -0,0 +1,192 @@
|
|||||||
|
import { QuestionCircleOutlined } from '@ant-design/icons';
|
||||||
|
import { LabelInfo } from '@gpustack/core-ui';
|
||||||
|
import { useIntl } from '@umijs/max';
|
||||||
|
import { Flex, InputNumber, Tooltip } from 'antd';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import styles from './styles.less';
|
||||||
|
|
||||||
|
interface NumberSelectionProps {
|
||||||
|
id?: string;
|
||||||
|
step?: number;
|
||||||
|
min?: number;
|
||||||
|
max?: number;
|
||||||
|
value?: number;
|
||||||
|
disabled?: boolean;
|
||||||
|
label?: React.ReactNode;
|
||||||
|
required?: boolean;
|
||||||
|
className?: string;
|
||||||
|
style?: React.CSSProperties;
|
||||||
|
styles?: {
|
||||||
|
input?: React.CSSProperties;
|
||||||
|
};
|
||||||
|
labelExtra?: React.ReactNode;
|
||||||
|
maxCount?: number;
|
||||||
|
tips?: string;
|
||||||
|
onChange?: (value: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const NumberSelection: React.FC<NumberSelectionProps> = ({
|
||||||
|
id,
|
||||||
|
step = 1,
|
||||||
|
min = 1,
|
||||||
|
max = 16,
|
||||||
|
value,
|
||||||
|
disabled,
|
||||||
|
label,
|
||||||
|
required,
|
||||||
|
labelExtra,
|
||||||
|
className,
|
||||||
|
maxCount = 8,
|
||||||
|
tips,
|
||||||
|
style,
|
||||||
|
onChange
|
||||||
|
}) => {
|
||||||
|
const intl = useIntl();
|
||||||
|
|
||||||
|
const showCustomInput = max > maxCount;
|
||||||
|
const presetItems = Array.from(
|
||||||
|
{ length: Math.max(0, maxCount) },
|
||||||
|
(_, i) => i + 1
|
||||||
|
);
|
||||||
|
if (min <= 0) {
|
||||||
|
presetItems.unshift(0);
|
||||||
|
}
|
||||||
|
const items = presetItems;
|
||||||
|
const isItemDisabled = (num: number) => !!disabled || num > max || num < min;
|
||||||
|
const [inputValue, setInputValue] = useState<number | null>(() =>
|
||||||
|
value !== undefined && value !== null && !presetItems.includes(value)
|
||||||
|
? value
|
||||||
|
: null
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (value === undefined || value === null) {
|
||||||
|
setInputValue(null);
|
||||||
|
} else if (presetItems.includes(value)) {
|
||||||
|
setInputValue(null);
|
||||||
|
} else {
|
||||||
|
setInputValue(value);
|
||||||
|
}
|
||||||
|
}, [value]);
|
||||||
|
|
||||||
|
const handleSelect = (num: number) => {
|
||||||
|
if (isItemDisabled(num) || num === value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setInputValue(null);
|
||||||
|
onChange?.(num);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleInputChange = (num: number | null) => {
|
||||||
|
setInputValue(num);
|
||||||
|
};
|
||||||
|
|
||||||
|
const commitInput = () => {
|
||||||
|
if (disabled || inputValue === null || inputValue === value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
onChange?.(inputValue);
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
id={id}
|
||||||
|
className={classNames(styles.wrapper, className, {
|
||||||
|
[styles.disabled]: disabled
|
||||||
|
})}
|
||||||
|
style={style}
|
||||||
|
role="radiogroup"
|
||||||
|
>
|
||||||
|
{label !== undefined && label !== null && (
|
||||||
|
<div className={styles.label}>
|
||||||
|
<LabelInfo
|
||||||
|
label={label}
|
||||||
|
required={required}
|
||||||
|
labelExtra={labelExtra}
|
||||||
|
></LabelInfo>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<Flex className={styles.contentWrapper} align="center">
|
||||||
|
<div className={styles.content}>
|
||||||
|
{items.map((num) => {
|
||||||
|
const itemDisabled = isItemDisabled(num);
|
||||||
|
return (
|
||||||
|
<div key={num} style={{ flex: 1 }}>
|
||||||
|
<Tooltip title={num === 0 ? tips : false}>
|
||||||
|
<div
|
||||||
|
key={num}
|
||||||
|
role="radio"
|
||||||
|
aria-checked={num === value}
|
||||||
|
aria-disabled={itemDisabled}
|
||||||
|
tabIndex={itemDisabled ? -1 : 0}
|
||||||
|
className={classNames(styles.numberItem, {
|
||||||
|
[styles.active]: num === value && value != null,
|
||||||
|
[styles.itemDisabled]: itemDisabled && !disabled
|
||||||
|
})}
|
||||||
|
onClick={() => handleSelect(num)}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter' || e.key === ' ') {
|
||||||
|
e.preventDefault();
|
||||||
|
handleSelect(num);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{num}
|
||||||
|
{num === 0 && (
|
||||||
|
<QuestionCircleOutlined
|
||||||
|
style={{
|
||||||
|
marginLeft: 4,
|
||||||
|
fontSize: 11,
|
||||||
|
color: 'var(--ant-color-text-tertiary)'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
{showCustomInput && (
|
||||||
|
<div className={styles.inputWrapper}>
|
||||||
|
<div className={styles.line}></div>
|
||||||
|
<div
|
||||||
|
className={classNames(styles.inputContainer, {
|
||||||
|
[styles.hasValue]: inputValue != null
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<InputNumber
|
||||||
|
controls={false}
|
||||||
|
variant="borderless"
|
||||||
|
min={min}
|
||||||
|
max={max}
|
||||||
|
step={step}
|
||||||
|
disabled={disabled}
|
||||||
|
value={inputValue ?? undefined}
|
||||||
|
style={{
|
||||||
|
fontSize: 14,
|
||||||
|
width: 60,
|
||||||
|
height: 32,
|
||||||
|
backgroundColor: 'var(--ant-color-bg-container)',
|
||||||
|
...style
|
||||||
|
}}
|
||||||
|
styles={{
|
||||||
|
input: {
|
||||||
|
fontWeight: inputValue != null ? 500 : 400,
|
||||||
|
...(styles?.input as React.CSSProperties)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
placeholder={intl.formatMessage({ id: 'common.option.other' })}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
onBlur={commitInput}
|
||||||
|
onPressEnter={commitInput}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NumberSelection;
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
.wrapper {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
border-radius: var(--ant-border-radius-lg);
|
||||||
|
border: 1px solid var(--ant-color-border);
|
||||||
|
overflow: hidden;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
padding: 4px;
|
||||||
|
|
||||||
|
.inputContainer {
|
||||||
|
&.hasValue {
|
||||||
|
background-color: var(--ant-color-bg-container);
|
||||||
|
:global {
|
||||||
|
.ant-input-number-action {
|
||||||
|
border-color: var(--ant-color-split) !important;
|
||||||
|
}
|
||||||
|
.ant-input-number-actions {
|
||||||
|
width: 32px !important;
|
||||||
|
opacity: 1 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
margin-inline: 4px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.inputWrapper {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
.line {
|
||||||
|
display: flex;
|
||||||
|
height: 22px;
|
||||||
|
border-left: 1px solid var(--ant-color-split);
|
||||||
|
margin-inline: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 12px;
|
||||||
|
color: var(--ant-color-text-tertiary);
|
||||||
|
white-space: nowrap;
|
||||||
|
padding-block: 6px;
|
||||||
|
}
|
||||||
|
.contentWrapper {
|
||||||
|
width: 100%;
|
||||||
|
background-color: var(--ant-color-fill-quaternary);
|
||||||
|
border-radius: 4px;
|
||||||
|
height: 36px;
|
||||||
|
padding-left: 4px;
|
||||||
|
}
|
||||||
|
.content {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex: 1;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 0px;
|
||||||
|
}
|
||||||
|
.numberItem {
|
||||||
|
position: relative;
|
||||||
|
color: var(--ant-color-text-secondary);
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
outline: none;
|
||||||
|
transition:
|
||||||
|
color 0.2s,
|
||||||
|
background-color 0.2s,
|
||||||
|
border-color 0.2s;
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--ant-color-fill-secondary);
|
||||||
|
}
|
||||||
|
&:focus-visible {
|
||||||
|
border-color: var(--ant-color-primary);
|
||||||
|
}
|
||||||
|
&.active {
|
||||||
|
background-color: var(--ant-color-bg-container);
|
||||||
|
color: var(--ant-color-text);
|
||||||
|
font-weight: 500;
|
||||||
|
border-color: var(--ant-color-split);
|
||||||
|
}
|
||||||
|
&.itemDisabled {
|
||||||
|
cursor: not-allowed;
|
||||||
|
color: var(--ant-color-text-disabled);
|
||||||
|
&:hover {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.disabled {
|
||||||
|
cursor: not-allowed;
|
||||||
|
background-color: var(--ant-color-bg-container-disabled);
|
||||||
|
.label {
|
||||||
|
color: var(--ant-color-text-disabled);
|
||||||
|
}
|
||||||
|
.numberItem {
|
||||||
|
cursor: not-allowed;
|
||||||
|
color: var(--ant-color-text-disabled);
|
||||||
|
&:hover:not(.active) {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import { currentClusterAtom } from '@/atoms/gpuservice';
|
||||||
|
import { Input as CInput } from '@gpustack/core-ui';
|
||||||
|
import { Form } from 'antd';
|
||||||
|
import type { NamePath } from 'antd/es/form/interface';
|
||||||
|
import { useAtomValue } from 'jotai';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
|
interface OwnerPrincipalIdFieldProps {
|
||||||
|
name?: NamePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
const OwnerPrincipalIdField: React.FC<OwnerPrincipalIdFieldProps> = ({
|
||||||
|
name = 'owner_principal_id'
|
||||||
|
}) => {
|
||||||
|
const currentCluster = useAtomValue(currentClusterAtom);
|
||||||
|
const ownerPrincipalId = currentCluster?.owner_principal_id;
|
||||||
|
const form = Form.useFormInstance();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (ownerPrincipalId != null) {
|
||||||
|
form.setFieldValue(name, ownerPrincipalId);
|
||||||
|
}
|
||||||
|
}, [ownerPrincipalId, name, form]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form.Item name={name} hidden>
|
||||||
|
<CInput.Input />
|
||||||
|
</Form.Item>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default OwnerPrincipalIdField;
|
||||||
@@ -1,4 +1,9 @@
|
|||||||
.containerWrapper {
|
.containerWrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
|
||||||
:global {
|
:global {
|
||||||
.ant-affix {
|
.ant-affix {
|
||||||
top: 0 !important;
|
top: 0 !important;
|
||||||
@@ -17,8 +22,24 @@
|
|||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ant-pro-page-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-pro-grid-content,
|
||||||
|
.ant-pro-grid-content-children {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.ant-pro-page-container-children-container {
|
.ant-pro-page-container-children-container {
|
||||||
height: calc(100vh - 16px);
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
height: auto;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
border: 1px solid var(--color-border-container);
|
border: 1px solid var(--color-border-container);
|
||||||
|
|||||||
@@ -131,14 +131,16 @@ const AllowModelsForm: React.FC<{
|
|||||||
id: 'common.filter.name'
|
id: 'common.filter.name'
|
||||||
})}
|
})}
|
||||||
options={modelList}
|
options={modelList}
|
||||||
selectedKeys={allowedModelNames || []}
|
value={allowedModelNames || []}
|
||||||
notFoundContent={intl.formatMessage({
|
notFoundContent={intl.formatMessage({
|
||||||
id: 'apikeys.models.noModelsFound'
|
id: 'apikeys.models.noModelsFound'
|
||||||
})}
|
})}
|
||||||
onSelectChange={(selectedKeys) => {
|
onChange={(selectedKeys) => {
|
||||||
form.setFieldsValue({ allowed_model_names: selectedKeys });
|
form.setFieldsValue({
|
||||||
|
allowed_model_names: selectedKeys || []
|
||||||
|
});
|
||||||
onValuesChange?.(
|
onValuesChange?.(
|
||||||
{ allowed_model_names: selectedKeys },
|
{ allowed_model_names: selectedKeys || [] },
|
||||||
form.getFieldsValue()
|
form.getFieldsValue()
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -279,7 +279,8 @@ const AddModal: React.FC<AddModalProps> = ({
|
|||||||
preserve={false}
|
preserve={false}
|
||||||
initialValues={{
|
initialValues={{
|
||||||
allowed_type: 'all',
|
allowed_type: 'all',
|
||||||
scope: ['inference']
|
scope: ['inference'],
|
||||||
|
allowed_model_names: []
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{!showKey && (
|
{!showKey && (
|
||||||
|
|||||||
@@ -1,65 +1,74 @@
|
|||||||
// columns.ts
|
// columns.ts
|
||||||
import { tableSorter } from '@/config/settings';
|
import { tableSorter } from '@/config/settings';
|
||||||
import { getGPUStackPlugin } from '@/plugins';
|
import { AutoTooltip, DropdownButtons, icons } from '@gpustack/core-ui';
|
||||||
import {
|
|
||||||
AutoTooltip,
|
|
||||||
DropdownButtons,
|
|
||||||
IconFont,
|
|
||||||
icons
|
|
||||||
} from '@gpustack/core-ui';
|
|
||||||
import { useIntl } from '@umijs/max';
|
import { useIntl } from '@umijs/max';
|
||||||
import { MenuProps, Tag } from 'antd';
|
import { MenuProps, Tag } from 'antd';
|
||||||
import { ColumnsType } from 'antd/lib/table';
|
import { ColumnsType } from 'antd/lib/table';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { ListItem } from '../config/types';
|
import { ListItem } from '../config/types';
|
||||||
|
import type { APIKeyConfigAction } from '../plugin';
|
||||||
|
|
||||||
type APIKeyAction = Global.ActionItem<ListItem> & {
|
type APIKeyAction = Global.ActionItem<ListItem> & {
|
||||||
onClick?: (record: ListItem) => void;
|
onClick?: (record: ListItem) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type RankedAction = APIKeyAction & { priority: number };
|
||||||
|
|
||||||
interface ColumnsHookProps {
|
interface ColumnsHookProps {
|
||||||
handleSelect: (val: string, record: ListItem, item?: APIKeyAction) => void;
|
handleSelect: (val: string, record: ListItem, item?: APIKeyAction) => void;
|
||||||
sortOrder: string[];
|
sortOrder: string[];
|
||||||
is_admin?: boolean;
|
is_admin?: boolean;
|
||||||
onIPConfig?: (record: ListItem) => void;
|
configActions?: APIKeyConfigAction[];
|
||||||
|
// Dispatches the click for a plugin-contributed dropdown entry to the
|
||||||
|
// controller `useCreate()` returned for that entry.
|
||||||
|
onConfigAction?: (actionKey: string, record: ListItem) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const useModelsColumns = ({
|
const useModelsColumns = ({
|
||||||
handleSelect,
|
handleSelect,
|
||||||
sortOrder,
|
sortOrder,
|
||||||
is_admin,
|
is_admin,
|
||||||
onIPConfig
|
configActions = [],
|
||||||
|
onConfigAction
|
||||||
}: ColumnsHookProps): ColumnsType<ListItem> => {
|
}: ColumnsHookProps): ColumnsType<ListItem> => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
|
||||||
const actionList = useMemo<APIKeyAction[]>(() => {
|
const actionList = useMemo<APIKeyAction[]>(() => {
|
||||||
const list: APIKeyAction[] = [
|
// Built-ins use a step-of-10 priority scale so plugins have room
|
||||||
|
// to insert at any position (e.g. 5 before Edit, 15 between Edit
|
||||||
|
// and Delete, 25 after Delete). The final list is sorted purely
|
||||||
|
// by priority — Delete sits last by virtue of its higher number,
|
||||||
|
// not by a special-case for `danger`.
|
||||||
|
const builtIns: RankedAction[] = [
|
||||||
{
|
{
|
||||||
label: 'common.button.edit',
|
label: 'common.button.edit',
|
||||||
key: 'edit',
|
key: 'edit',
|
||||||
icon: icons.EditOutlined
|
icon: icons.EditOutlined,
|
||||||
|
priority: 10
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'common.button.delete',
|
label: 'common.button.delete',
|
||||||
key: 'delete',
|
key: 'delete',
|
||||||
icon: icons.DeleteOutlined,
|
icon: icons.DeleteOutlined,
|
||||||
props: { danger: true }
|
props: { danger: true },
|
||||||
|
priority: 20
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
const ipConfigComponent = getGPUStackPlugin()?.APIKeyIPConfig?.form;
|
const fromPlugins: RankedAction[] = configActions.map((a) => ({
|
||||||
if (ipConfigComponent && onIPConfig) {
|
label: a.labelId,
|
||||||
list.splice(1, 0, {
|
key: a.key,
|
||||||
label: 'apikeys.button.ipConfig',
|
icon: a.icon,
|
||||||
key: 'ipConfig',
|
priority: a.priority ?? 100,
|
||||||
icon: <IconFont type="icon-safe-ip" />,
|
props: a.danger ? { danger: true } : undefined,
|
||||||
onClick: (record: ListItem) => onIPConfig(record)
|
onClick: (record: ListItem) => onConfigAction?.(a.key, record)
|
||||||
});
|
}));
|
||||||
}
|
|
||||||
|
|
||||||
return list;
|
return [...builtIns, ...fromPlugins].sort(
|
||||||
}, [onIPConfig]);
|
(a, b) => a.priority - b.priority
|
||||||
|
);
|
||||||
|
}, [configActions, onConfigAction]);
|
||||||
|
|
||||||
return useMemo(() => {
|
return useMemo(() => {
|
||||||
return [
|
return [
|
||||||
|
|||||||
@@ -3,19 +3,23 @@ import { PaginationKey } from '@/config/settings';
|
|||||||
import type { PageActionType } from '@/config/types';
|
import type { PageActionType } from '@/config/types';
|
||||||
import useTableFetch from '@/hooks/use-table-fetch';
|
import useTableFetch from '@/hooks/use-table-fetch';
|
||||||
import useQueryUserList from '@/pages/users/services/use-query-user-list';
|
import useQueryUserList from '@/pages/users/services/use-query-user-list';
|
||||||
import { getGPUStackPlugin } from '@/plugins';
|
|
||||||
import { useModel } from '@@/plugin-model';
|
import { useModel } from '@@/plugin-model';
|
||||||
import { DeleteModal, FilterBar, IconFont, NoResult } from '@gpustack/core-ui';
|
import { DeleteModal, FilterBar, IconFont, NoResult } from '@gpustack/core-ui';
|
||||||
import { useIntl } from '@umijs/max';
|
import { useIntl } from '@umijs/max';
|
||||||
import useMemoizedFn from 'ahooks/lib/useMemoizedFn';
|
import useMemoizedFn from 'ahooks/lib/useMemoizedFn';
|
||||||
import { ConfigProvider, Table } from 'antd';
|
import { ConfigProvider, Table } from 'antd';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { useEffect, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import PageBox from '../_components/page-box';
|
import PageBox from '../_components/page-box';
|
||||||
import { deleteApisKey, queryApisKeysList } from './apis';
|
import { deleteApisKey, queryApisKeysList } from './apis';
|
||||||
import AddAPIKeyModal from './components/add-apikey-modal';
|
import AddAPIKeyModal from './components/add-apikey-modal';
|
||||||
import { ListItem } from './config/types';
|
import { ListItem } from './config/types';
|
||||||
import useKeysColumns from './hooks/use-keys-columns';
|
import useKeysColumns from './hooks/use-keys-columns';
|
||||||
|
import {
|
||||||
|
APIKeyConfigActionMount,
|
||||||
|
getAPIKeyConfigActions,
|
||||||
|
type APIKeyConfigActionController
|
||||||
|
} from './plugin';
|
||||||
|
|
||||||
const APIKeys: React.FC = () => {
|
const APIKeys: React.FC = () => {
|
||||||
const { initialState } = useModel('@@initialState');
|
const { initialState } = useModel('@@initialState');
|
||||||
@@ -55,10 +59,27 @@ const APIKeys: React.FC = () => {
|
|||||||
|
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
|
||||||
const apiKeyIPConfig = getGPUStackPlugin()?.APIKeyIPConfig;
|
// Generic per-row plugin slot. Each enterprise plugin contributes a
|
||||||
const APIKeyIPConfigForm = apiKeyIPConfig?.form;
|
// `{ key, labelId, icon, priority, form, useCreate }` entry; the
|
||||||
const { openIPConfigModalStatus, openIPConfigModal, closeIPConfigModal } =
|
// host renders a button per entry in the dropdown and renders one
|
||||||
apiKeyIPConfig?.useCreateIPConfig?.() || {};
|
// `APIKeyConfigActionMount` per entry — those mounts own each
|
||||||
|
// entry's controller and register it back into `controllersRef` so
|
||||||
|
// dropdown clicks can route to the correct `openModal`. See
|
||||||
|
// `./plugin.tsx`.
|
||||||
|
//
|
||||||
|
// The action list is read once. Plugins are registered at boot and
|
||||||
|
// never recompute, so the reference is stable for the lifetime of
|
||||||
|
// the page and `useMemo([])` is safe.
|
||||||
|
const configActions = useMemo(() => getAPIKeyConfigActions(), []);
|
||||||
|
const controllersRef = useRef<Record<string, APIKeyConfigActionController>>(
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
const registerController = useCallback(
|
||||||
|
(key: string, controller: APIKeyConfigActionController) => {
|
||||||
|
controllersRef.current[key] = controller;
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
const [openAddModal, setOpenAddModal] = useState<{
|
const [openAddModal, setOpenAddModal] = useState<{
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@@ -140,6 +161,14 @@ const APIKeys: React.FC = () => {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Each plugin entry's button onClick routes here. The controller
|
||||||
|
// registry is populated by each `APIKeyConfigActionMount` on mount.
|
||||||
|
const handleConfigAction = useMemoizedFn(
|
||||||
|
(actionKey: string, record: ListItem) => {
|
||||||
|
controllersRef.current[actionKey]?.openModal(record);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const handleUserChange = (val: string) => {
|
const handleUserChange = (val: string) => {
|
||||||
handleQueryChange({
|
handleQueryChange({
|
||||||
user_id: val || '*'
|
user_id: val || '*'
|
||||||
@@ -170,7 +199,8 @@ const APIKeys: React.FC = () => {
|
|||||||
handleSelect: onSelect,
|
handleSelect: onSelect,
|
||||||
sortOrder,
|
sortOrder,
|
||||||
is_admin: currentUser?.is_admin,
|
is_admin: currentUser?.is_admin,
|
||||||
onIPConfig: openIPConfigModal
|
configActions,
|
||||||
|
onConfigAction: handleConfigAction
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -225,14 +255,20 @@ const APIKeys: React.FC = () => {
|
|||||||
onCancel={handleModalCancel}
|
onCancel={handleModalCancel}
|
||||||
onOk={handleModalOk}
|
onOk={handleModalOk}
|
||||||
></AddAPIKeyModal>
|
></AddAPIKeyModal>
|
||||||
{APIKeyIPConfigForm && (
|
|
||||||
<APIKeyIPConfigForm
|
|
||||||
open={openIPConfigModalStatus.open}
|
|
||||||
apiKey={openIPConfigModalStatus.currentData}
|
|
||||||
onClose={closeIPConfigModal}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<DeleteModal ref={modalRef}></DeleteModal>
|
<DeleteModal ref={modalRef}></DeleteModal>
|
||||||
|
{/* One mount per registered action. Each mount calls its
|
||||||
|
entry's `useCreate` (single hook per component, so iterating
|
||||||
|
the plugin list doesn't violate the Rules of Hooks),
|
||||||
|
renders the form, and registers its controller so dropdown
|
||||||
|
clicks can dispatch to it. */}
|
||||||
|
{configActions.map((action) => (
|
||||||
|
<APIKeyConfigActionMount
|
||||||
|
key={action.key}
|
||||||
|
action={action}
|
||||||
|
registerController={registerController}
|
||||||
|
onOk={fetchData}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,95 @@
|
|||||||
|
import { getGPUStackPlugin } from '@/plugins';
|
||||||
|
import type { ComponentType, ReactNode } from 'react';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
import type { ListItem } from './config/types';
|
||||||
|
|
||||||
|
// Generic per-row "configure this api-key" plugin slot.
|
||||||
|
//
|
||||||
|
// Each enterprise plugin contributes one entry: a dropdown button +
|
||||||
|
// its own controlled form component + a `useCreate` hook that owns the
|
||||||
|
// open/close state for that entry's drawer. The host renders all
|
||||||
|
// entries' buttons in the dropdown (ordered by `priority`) and renders
|
||||||
|
// one `APIKeyConfigActionMount` per entry — that mount is what calls
|
||||||
|
// the entry's `useCreate` (a single hook call per component, so the
|
||||||
|
// Rules-of-Hooks aren't violated by iteration order) and reports its
|
||||||
|
// controller back to the host via `registerController`. Replaces the
|
||||||
|
// older `APIKeyIPConfig` slot, the generic `apiKeys.rowActions`, and
|
||||||
|
// the `PluginExtraFields name="APIKeysPageGlobal"` mount point —
|
||||||
|
// adding a new per-key configuration feature now only requires
|
||||||
|
// registering a new entry from the plugin side.
|
||||||
|
export type APIKeyConfigActionRecord = Partial<ListItem> & {
|
||||||
|
id: number;
|
||||||
|
name?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type APIKeyConfigActionFormProps = {
|
||||||
|
open: boolean;
|
||||||
|
apiKey?: APIKeyConfigActionRecord | null;
|
||||||
|
onClose: () => void;
|
||||||
|
onOk?: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type APIKeyConfigActionState = {
|
||||||
|
open: boolean;
|
||||||
|
currentData?: APIKeyConfigActionRecord | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type APIKeyConfigActionController = {
|
||||||
|
openModalStatus: APIKeyConfigActionState;
|
||||||
|
openModal: (row: APIKeyConfigActionRecord) => void;
|
||||||
|
closeModal: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type APIKeyConfigAction = {
|
||||||
|
key: string;
|
||||||
|
labelId: string;
|
||||||
|
icon?: ReactNode;
|
||||||
|
// Lower comes first; default 100. Stable sort preserves declaration
|
||||||
|
// order on ties. Built-ins use `edit=0` and `delete=9999`, so a
|
||||||
|
// plugin priority of 10–9000 lands between Edit and Delete.
|
||||||
|
priority?: number;
|
||||||
|
// Destructive entries sink to the bottom of the dropdown regardless
|
||||||
|
// of priority — keeps Delete-style actions visually grouped.
|
||||||
|
danger?: boolean;
|
||||||
|
form: ComponentType<APIKeyConfigActionFormProps>;
|
||||||
|
useCreate: () => APIKeyConfigActionController;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Static registration read — plugins are wired once at boot, so the
|
||||||
|
// list reference is stable across renders. Exposed as a helper to keep
|
||||||
|
// the host's import surface tight.
|
||||||
|
export const getAPIKeyConfigActions = (): APIKeyConfigAction[] =>
|
||||||
|
getGPUStackPlugin()?.apiKeys?.configActions ?? [];
|
||||||
|
|
||||||
|
type APIKeyConfigActionMountProps = {
|
||||||
|
action: APIKeyConfigAction;
|
||||||
|
// Called once after mount (and on controller identity change) so the
|
||||||
|
// host can route a dropdown click to the correct entry's openModal.
|
||||||
|
registerController: (
|
||||||
|
key: string,
|
||||||
|
controller: APIKeyConfigActionController
|
||||||
|
) => void;
|
||||||
|
onOk?: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
// One component instance per registered action. Calls `useCreate` at
|
||||||
|
// the top level (single hook call per component) and renders the
|
||||||
|
// entry's form bound to its own controller. Hosts mount one of these
|
||||||
|
// per entry in `apiKeys.configActions`.
|
||||||
|
export const APIKeyConfigActionMount: React.FC<
|
||||||
|
APIKeyConfigActionMountProps
|
||||||
|
> = ({ action, registerController, onOk }) => {
|
||||||
|
const controller = action.useCreate();
|
||||||
|
useEffect(() => {
|
||||||
|
registerController(action.key, controller);
|
||||||
|
}, [action.key, controller, registerController]);
|
||||||
|
const Form = action.form;
|
||||||
|
return (
|
||||||
|
<Form
|
||||||
|
open={controller.openModalStatus.open}
|
||||||
|
apiKey={controller.openModalStatus.currentData}
|
||||||
|
onClose={controller.closeModal}
|
||||||
|
onOk={onOk}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -119,6 +119,10 @@ const AddModal: React.FC<AddModalProps> = (props) => {
|
|||||||
|
|
||||||
onSubmit({
|
onSubmit({
|
||||||
...values,
|
...values,
|
||||||
|
parameter_format:
|
||||||
|
values.parameter_format === 'auto' || !values.parameter_format
|
||||||
|
? null
|
||||||
|
: values.parameter_format,
|
||||||
default_version: defaultVersion?.version_no || '',
|
default_version: defaultVersion?.version_no || '',
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
version_configs: versionConfigs
|
version_configs: versionConfigs
|
||||||
@@ -155,6 +159,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',
|
||||||
version_configs: versionConfigs,
|
version_configs: versionConfigs,
|
||||||
built_in_version_configs: builtInVersions
|
built_in_version_configs: builtInVersions
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -198,6 +198,8 @@ export const customBackendFields = [
|
|||||||
'default_run_command',
|
'default_run_command',
|
||||||
'version_configs',
|
'version_configs',
|
||||||
'default_backend_param',
|
'default_backend_param',
|
||||||
|
'parameter_format',
|
||||||
|
'common_parameters',
|
||||||
'default_env'
|
'default_env'
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -208,6 +210,8 @@ export const builtInBackendFields = [
|
|||||||
'description',
|
'description',
|
||||||
'version_configs',
|
'version_configs',
|
||||||
'default_backend_param',
|
'default_backend_param',
|
||||||
|
'parameter_format',
|
||||||
|
'common_parameters',
|
||||||
'default_env'
|
'default_env'
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -293,6 +297,10 @@ default_version: v0.11.0
|
|||||||
health_check_path: /v1/models
|
health_check_path: /v1/models
|
||||||
default_backend_param:
|
default_backend_param:
|
||||||
- --host
|
- --host
|
||||||
|
parameter_format: space
|
||||||
|
common_parameters:
|
||||||
|
- --max-model-len
|
||||||
|
- --gpu-memory-utilization
|
||||||
default_run_command: "{{model_path}} --port {{port}} --host {{worker_ip}} --served-model-name {{model_name}}"
|
default_run_command: "{{model_path}} --port {{port}} --host {{worker_ip}} --served-model-name {{model_name}}"
|
||||||
default_env:
|
default_env:
|
||||||
version_configs:
|
version_configs:
|
||||||
|
|||||||
@@ -26,6 +26,18 @@
|
|||||||
},
|
},
|
||||||
"description": "default backend parameter"
|
"description": "default backend parameter"
|
||||||
},
|
},
|
||||||
|
"parameter_format": {
|
||||||
|
"type": ["string", "null"],
|
||||||
|
"enum": ["space", "equal", null],
|
||||||
|
"description": "parameter output format: 'space' -> --key value, 'equal' -> --key=value"
|
||||||
|
},
|
||||||
|
"common_parameters": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"description": "commonly used parameters shown as hints in deployment UI"
|
||||||
|
},
|
||||||
"default_run_command": {
|
"default_run_command": {
|
||||||
"type": ["string", "null"],
|
"type": ["string", "null"],
|
||||||
"description": "default start command"
|
"description": "default start command"
|
||||||
|
|||||||
@@ -12,6 +12,18 @@
|
|||||||
},
|
},
|
||||||
"description": "default backend parameter"
|
"description": "default backend parameter"
|
||||||
},
|
},
|
||||||
|
"parameter_format": {
|
||||||
|
"type": ["string", "null"],
|
||||||
|
"enum": ["space", "equal", null],
|
||||||
|
"description": "parameter output format: 'space' -> --key value, 'equal' -> --key=value"
|
||||||
|
},
|
||||||
|
"common_parameters": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"description": "commonly used parameters shown as hints in deployment UI"
|
||||||
|
},
|
||||||
"default_run_command": {
|
"default_run_command": {
|
||||||
"type": ["string", "null"],
|
"type": ["string", "null"],
|
||||||
"description": "default start command"
|
"description": "default start command"
|
||||||
|
|||||||
@@ -16,6 +16,18 @@
|
|||||||
},
|
},
|
||||||
"description": "default backend parameter"
|
"description": "default backend parameter"
|
||||||
},
|
},
|
||||||
|
"parameter_format": {
|
||||||
|
"type": ["string", "null"],
|
||||||
|
"enum": ["space", "equal", null],
|
||||||
|
"description": "parameter output format: 'space' -> --key value, 'equal' -> --key=value"
|
||||||
|
},
|
||||||
|
"common_parameters": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"description": "commonly used parameters shown as hints in deployment UI"
|
||||||
|
},
|
||||||
"default_run_command": {
|
"default_run_command": {
|
||||||
"type": ["string", "null"],
|
"type": ["string", "null"],
|
||||||
"description": "default start command"
|
"description": "default start command"
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ 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;
|
||||||
|
common_parameters?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ListItem extends FormData {
|
export interface ListItem extends FormData {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
Input as CInput,
|
Input as CInput,
|
||||||
LabelSelector,
|
LabelSelector,
|
||||||
ListInput,
|
ListInput,
|
||||||
|
Select as SealSelect,
|
||||||
Textarea as SealTextArea,
|
Textarea as SealTextArea,
|
||||||
useAppUtils
|
useAppUtils
|
||||||
} from '@gpustack/core-ui';
|
} from '@gpustack/core-ui';
|
||||||
@@ -148,6 +149,46 @@ const BasicForm = () => {
|
|||||||
})}
|
})}
|
||||||
></ListInput>
|
></ListInput>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
<Form.Item<FormData>
|
||||||
|
name="parameter_format"
|
||||||
|
initialValue="auto"
|
||||||
|
rules={[{ required: false }]}
|
||||||
|
>
|
||||||
|
<SealSelect
|
||||||
|
label={intl.formatMessage({ id: 'backend.form.parameterFormat' })}
|
||||||
|
options={[
|
||||||
|
{
|
||||||
|
label: intl.formatMessage({
|
||||||
|
id: 'backend.form.parameterFormat.default'
|
||||||
|
}),
|
||||||
|
value: 'auto'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: intl.formatMessage({
|
||||||
|
id: 'backend.form.parameterFormat.space'
|
||||||
|
}),
|
||||||
|
value: 'space'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: intl.formatMessage({
|
||||||
|
id: 'backend.form.parameterFormat.equal'
|
||||||
|
}),
|
||||||
|
value: 'equal'
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
></SealSelect>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item<FormData>
|
||||||
|
name="common_parameters"
|
||||||
|
rules={[{ required: false }]}
|
||||||
|
>
|
||||||
|
<ListInput
|
||||||
|
btnText={intl.formatMessage({ id: 'backend.form.addParameter' })}
|
||||||
|
label={intl.formatMessage({
|
||||||
|
id: 'backend.form.commonParameters'
|
||||||
|
})}
|
||||||
|
></ListInput>
|
||||||
|
</Form.Item>
|
||||||
<Form.Item<FormData> name="default_env">
|
<Form.Item<FormData> name="default_env">
|
||||||
<LabelSelector
|
<LabelSelector
|
||||||
label={intl.formatMessage({
|
label={intl.formatMessage({
|
||||||
|
|||||||
@@ -84,13 +84,13 @@ const Instance: React.FC = () => {
|
|||||||
}),
|
}),
|
||||||
children: renderParams(instanceData?.backend_parameters || [])
|
children: renderParams(instanceData?.backend_parameters || [])
|
||||||
},
|
},
|
||||||
// {
|
{
|
||||||
// key: '1-1',
|
key: '1-1',
|
||||||
// label: intl.formatMessage({
|
label: intl.formatMessage({
|
||||||
// id: 'models.instance.params.autoInjected'
|
id: 'models.instance.params.autoInjected'
|
||||||
// }),
|
}),
|
||||||
// children: renderParams(instanceData?.injected_backend_parameters || [])
|
children: renderParams(instanceData?.injected_backend_parameters || [])
|
||||||
// },
|
},
|
||||||
{
|
{
|
||||||
key: '3',
|
key: '3',
|
||||||
label: intl.formatMessage({ id: 'benchmark.detail.kvCache' }),
|
label: intl.formatMessage({ id: 'benchmark.detail.kvCache' }),
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ const FilterFormContent: React.FC<FilterFormContentProps> = forwardRef(
|
|||||||
<FilterForm
|
<FilterForm
|
||||||
ref={filterRef}
|
ref={filterRef}
|
||||||
width={232}
|
width={232}
|
||||||
contentHeight={'calc(100vh - 122px)'}
|
contentHeight={'calc(100vh - var(--app-banner-height, 0px) - 122px)'}
|
||||||
open={open}
|
open={open}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
onClear={onClear}
|
onClear={onClear}
|
||||||
|
|||||||
@@ -180,7 +180,7 @@ const Benchmark: React.FC = () => {
|
|||||||
loading={dataSource.loading}
|
loading={dataSource.loading}
|
||||||
loadend={dataSource.loadend}
|
loadend={dataSource.loadend}
|
||||||
dataSource={[]}
|
dataSource={[]}
|
||||||
image={<IconFont type="icon-credential-outline" />}
|
image={<IconFont type="icon-speed" />}
|
||||||
filters={_.omit(queryParams, ['sort_by'])}
|
filters={_.omit(queryParams, ['sort_by'])}
|
||||||
noFoundText={intl.formatMessage({
|
noFoundText={intl.formatMessage({
|
||||||
id: 'noresult.benchmark.nofound'
|
id: 'noresult.benchmark.nofound'
|
||||||
|
|||||||
@@ -331,7 +331,9 @@ const ClusterCreate: React.FC<{
|
|||||||
)}
|
)}
|
||||||
<ColumnWrapper
|
<ColumnWrapper
|
||||||
maxHeight={
|
maxHeight={
|
||||||
isAddWorkerStep ? 'calc(100vh - 200px)' : 'calc(100vh - 150px)'
|
isAddWorkerStep
|
||||||
|
? 'calc(100vh - var(--app-banner-height, 0px) - 200px)'
|
||||||
|
: 'calc(100vh - var(--app-banner-height, 0px) - 150px)'
|
||||||
}
|
}
|
||||||
styles={{
|
styles={{
|
||||||
container: { paddingTop: 16, paddingBottom: 16 }
|
container: { paddingTop: 16, paddingBottom: 16 }
|
||||||
|
|||||||
@@ -96,6 +96,28 @@ const AddWorkerSteps: React.FC<AddWorkerProps> = (props) => {
|
|||||||
);
|
);
|
||||||
}, [clusterList, stepList, StepNamesMap]);
|
}, [clusterList, stepList, StepNamesMap]);
|
||||||
|
|
||||||
|
// Downstream steps (check env, run command, ...) only make sense after a
|
||||||
|
// 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.
|
||||||
|
const selectedGPUs =
|
||||||
|
(summary.get('selectedGPUs') as string[] | undefined) || [];
|
||||||
|
const currentGPU = (summary.get('currentGPU') as string | undefined) || '';
|
||||||
|
const noVendorSelected = !currentGPU && selectedGPUs.length === 0;
|
||||||
|
const downstreamDisabled = disabled || noVendorSelected;
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
// If the user just deselected everything, collapse any downstream
|
||||||
|
// panel back to the GPU step so they aren't left looking at a stale
|
||||||
|
// disabled-but-open command. Functional updater so we don't have to
|
||||||
|
// depend on `collapseKey` and re-run the effect on every toggle.
|
||||||
|
if (!noVendorSelected) return;
|
||||||
|
setCollapseKey((prev) =>
|
||||||
|
prev.has(StepNamesMap.SelectGPU)
|
||||||
|
? prev
|
||||||
|
: new Set([StepNamesMap.SelectGPU])
|
||||||
|
);
|
||||||
|
}, [noVendorSelected]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AddWorkerContext.Provider
|
<AddWorkerContext.Provider
|
||||||
value={{
|
value={{
|
||||||
@@ -123,16 +145,20 @@ const AddWorkerSteps: React.FC<AddWorkerProps> = (props) => {
|
|||||||
!stepList.includes(StepNamesMap.SelectCluster)) && (
|
!stepList.includes(StepNamesMap.SelectCluster)) && (
|
||||||
<>
|
<>
|
||||||
<SelectVendor disabled={disabled}></SelectVendor>
|
<SelectVendor disabled={disabled}></SelectVendor>
|
||||||
<CheckEnvironment disabled={disabled}></CheckEnvironment>
|
<CheckEnvironment disabled={downstreamDisabled}></CheckEnvironment>
|
||||||
|
|
||||||
{provider === ProviderValueMap.Kubernetes && (
|
{provider === ProviderValueMap.Kubernetes && (
|
||||||
<K8sRunCommand disabled={disabled}></K8sRunCommand>
|
<K8sRunCommand disabled={downstreamDisabled}></K8sRunCommand>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{provider === ProviderValueMap.Docker && (
|
{provider === ProviderValueMap.Docker && (
|
||||||
<>
|
<>
|
||||||
<SpecifyArguments disabled={disabled}></SpecifyArguments>
|
<SpecifyArguments
|
||||||
<DockerRunCommand disabled={disabled}></DockerRunCommand>
|
disabled={downstreamDisabled}
|
||||||
|
></SpecifyArguments>
|
||||||
|
<DockerRunCommand
|
||||||
|
disabled={downstreamDisabled}
|
||||||
|
></DockerRunCommand>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ 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');
|
||||||
|
const currentGPUs: string[] = summary.get('selectedGPUs') || [];
|
||||||
const workerCommand = summary.get('workerCommand') || {
|
const workerCommand = summary.get('workerCommand') || {
|
||||||
label: '',
|
label: '',
|
||||||
link: '',
|
link: '',
|
||||||
@@ -51,7 +52,11 @@ const CheckEnvironment: React.FC<AddWorkerStepProps> = ({ disabled }) => {
|
|||||||
<Typography.Paragraph style={{ marginBottom: 8 }}>
|
<Typography.Paragraph style={{ marginBottom: 8 }}>
|
||||||
{intl.formatMessage({ id: 'cluster.create.checkEnv.tips' })}
|
{intl.formatMessage({ id: 'cluster.create.checkEnv.tips' })}
|
||||||
</Typography.Paragraph>
|
</Typography.Paragraph>
|
||||||
<CheckEnvCommand provider={provider} currentGPU={currentGPU} />
|
<CheckEnvCommand
|
||||||
|
provider={provider}
|
||||||
|
currentGPU={currentGPU}
|
||||||
|
currentGPUs={currentGPUs}
|
||||||
|
/>
|
||||||
</StepCollapse>
|
</StepCollapse>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -31,6 +31,9 @@ export const K8sStepsFromCluter = [
|
|||||||
|
|
||||||
export interface SummaryDataKeys {
|
export interface SummaryDataKeys {
|
||||||
currentGPU: string;
|
currentGPU: string;
|
||||||
|
// Multi-vendor selection for K8s register flow — array of GPU driver keys.
|
||||||
|
// Falls back to `[currentGPU]` for the single-select default path.
|
||||||
|
selectedGPUs: string[];
|
||||||
cluster_id: number;
|
cluster_id: number;
|
||||||
clusterName: string;
|
clusterName: string;
|
||||||
workerCommand: {
|
workerCommand: {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ const K8sRunCommand: React.FC<AddWorkerStepProps> = ({ disabled }) => {
|
|||||||
|
|
||||||
const stepIndex = stepList.indexOf(StepNamesMap.RunCommand) + 1;
|
const stepIndex = stepList.indexOf(StepNamesMap.RunCommand) + 1;
|
||||||
const currentGPU = summary.get('currentGPU') || '';
|
const currentGPU = summary.get('currentGPU') || '';
|
||||||
|
const currentGPUs: string[] = summary.get('selectedGPUs') || [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StepCollapse
|
<StepCollapse
|
||||||
@@ -30,12 +31,13 @@ const K8sRunCommand: React.FC<AddWorkerStepProps> = ({ disabled }) => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{intl.formatMessage({
|
{intl.formatMessage({
|
||||||
id: 'clusters.create.addCommand.tips'
|
id: 'clusters.create.addCommand.k8s.tips'
|
||||||
})}
|
})}
|
||||||
</Typography.Paragraph>
|
</Typography.Paragraph>
|
||||||
<RegisterClusterInner
|
<RegisterClusterInner
|
||||||
registrationInfo={registrationInfo}
|
registrationInfo={registrationInfo}
|
||||||
currentGPU={currentGPU}
|
currentGPU={currentGPU}
|
||||||
|
currentGPUs={currentGPUs}
|
||||||
/>
|
/>
|
||||||
</StepCollapse>
|
</StepCollapse>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,54 +1,161 @@
|
|||||||
import {
|
import {
|
||||||
AddWorkerDockerNotes,
|
AddWorkerDockerNotes,
|
||||||
GPUDriverMap
|
GPUDriverMap,
|
||||||
|
GPUsConfigs
|
||||||
} from '@/pages/resources/config/gpu-driver';
|
} from '@/pages/resources/config/gpu-driver';
|
||||||
|
import { BulbOutlined } from '@ant-design/icons';
|
||||||
import { useIntl } from '@umijs/max';
|
import { useIntl } from '@umijs/max';
|
||||||
import React, { useEffect } from 'react';
|
import { Alert, Tag } from 'antd';
|
||||||
|
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
import { queryClusterItem } from '../../apis';
|
||||||
|
import { ProviderValueMap } from '../../config';
|
||||||
|
import { ClusterListItem } from '../../config/types';
|
||||||
import SupportedGPUs from '../support-gpus';
|
import SupportedGPUs from '../support-gpus';
|
||||||
import { useAddWorkerContext } from './add-worker-context';
|
import { useAddWorkerContext } from './add-worker-context';
|
||||||
import { AddWorkerStepProps, StepNamesMap } from './config';
|
import { AddWorkerStepProps, StepNamesMap } from './config';
|
||||||
import { Title } from './constainers';
|
import { Title } from './constainers';
|
||||||
import StepCollapse from './step-collapse';
|
import StepCollapse from './step-collapse';
|
||||||
|
|
||||||
|
const buildWorkerCommand = (
|
||||||
|
driverKey: string,
|
||||||
|
itemHint?: { label?: string; link?: string }
|
||||||
|
) => ({
|
||||||
|
label: itemHint?.label || GPUsConfigs[driverKey]?.label || driverKey,
|
||||||
|
link: itemHint?.link || '',
|
||||||
|
notes: AddWorkerDockerNotes[driverKey] || []
|
||||||
|
});
|
||||||
|
|
||||||
const SelectVendor: React.FC<AddWorkerStepProps> = ({ disabled }) => {
|
const SelectVendor: React.FC<AddWorkerStepProps> = ({ disabled }) => {
|
||||||
const { stepList, registerField, updateField } = useAddWorkerContext();
|
const { stepList, registerField, updateField, provider, registrationInfo } =
|
||||||
|
useAddWorkerContext();
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
|
||||||
const stepIndex = stepList.indexOf(StepNamesMap.SelectGPU) + 1;
|
const stepIndex = stepList.indexOf(StepNamesMap.SelectGPU) + 1;
|
||||||
const [currentGPU, setCurrentGPU] = React.useState<string>(
|
|
||||||
|
// Pull the cluster so we know whether gpuVendorOverrides was configured.
|
||||||
|
// The K8s register flow gates multi-select on that being present, and
|
||||||
|
// restricts available runtimes to its keys.
|
||||||
|
const [overrideRuntimes, setOverrideRuntimes] = useState<string[]>([]);
|
||||||
|
useEffect(() => {
|
||||||
|
const id = registrationInfo?.cluster_id;
|
||||||
|
if (!id) {
|
||||||
|
// Reset when the cluster context goes away so we don't leak the
|
||||||
|
// previous cluster's overrides into the next session.
|
||||||
|
setOverrideRuntimes([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let cancelled = false;
|
||||||
|
queryClusterItem({ id })
|
||||||
|
.then((c) => {
|
||||||
|
if (cancelled) return;
|
||||||
|
const overrides = (c as ClusterListItem)?.k8s_options
|
||||||
|
?.gpuVendorOverrides;
|
||||||
|
setOverrideRuntimes(overrides ? Object.keys(overrides) : []);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
if (cancelled) return;
|
||||||
|
console.error('Failed to query cluster for vendor overrides:', err);
|
||||||
|
setOverrideRuntimes([]);
|
||||||
|
});
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
}, [registrationInfo?.cluster_id]);
|
||||||
|
|
||||||
|
// Set of GPU *driver keys* (e.g. "cuda", "cann") that match the cluster's
|
||||||
|
// override runtimes. Used to detect when multi-add becomes available.
|
||||||
|
const overrideKeys = useMemo(() => {
|
||||||
|
const set = new Set<string>();
|
||||||
|
if (!overrideRuntimes.length) return set;
|
||||||
|
Object.values(GPUsConfigs).forEach((cfg) => {
|
||||||
|
if (cfg.gpuVendor && overrideRuntimes.includes(cfg.gpuVendor)) {
|
||||||
|
set.add(cfg.value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return set;
|
||||||
|
}, [overrideRuntimes]);
|
||||||
|
|
||||||
|
// Multi-add is only meaningful when the cluster has 2+ vendor overrides
|
||||||
|
// AND the current selection already includes one. Until both hold, the
|
||||||
|
// picker behaves like a single-select (so the user can freely land on
|
||||||
|
// any vendor — including ones outside the override list).
|
||||||
|
const multiCapable =
|
||||||
|
provider === ProviderValueMap.Kubernetes && overrideKeys.size >= 2;
|
||||||
|
|
||||||
|
const [selectedKeys, setSelectedKeys] = useState<string[]>([
|
||||||
GPUDriverMap.NVIDIA
|
GPUDriverMap.NVIDIA
|
||||||
|
]);
|
||||||
|
|
||||||
|
const isMultiActive = useMemo(
|
||||||
|
() => multiCapable && selectedKeys.some((k) => overrideKeys.has(k)),
|
||||||
|
[multiCapable, selectedKeys, overrideKeys]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleSelectProvider = (value: string, item: any) => {
|
// In multi-active mode, non-override vendors are disabled — picking one
|
||||||
if (value === currentGPU) return;
|
// would break the backend invariant that a multi-vendor manifest must
|
||||||
setCurrentGPU(value);
|
// only target configured runtimes. Otherwise everything stays enabled.
|
||||||
|
const availableKeys = isMultiActive ? overrideKeys : undefined;
|
||||||
|
|
||||||
updateField('currentGPU', value);
|
// Cache vendor metadata (label/link from SupportedGPUs items) so we can
|
||||||
updateField('workerCommand', item);
|
// rebuild workerCommand on toggle without re-clicking the card.
|
||||||
|
const itemMetaRef = useRef<Record<string, { label: string; link: string }>>(
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Push current selection into the shared summary so consumers
|
||||||
|
// (K8sRunCommand, CheckEnvironment, VendorNotes) can read it.
|
||||||
|
useEffect(() => {
|
||||||
|
const primary = selectedKeys[0] || '';
|
||||||
|
updateField('currentGPU', primary);
|
||||||
|
updateField('selectedGPUs', selectedKeys);
|
||||||
|
updateField(
|
||||||
|
'workerCommand',
|
||||||
|
primary ? buildWorkerCommand(primary, itemMetaRef.current[primary]) : null
|
||||||
|
);
|
||||||
|
}, [selectedKeys]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const unregister1 = registerField('currentGPU');
|
||||||
|
const unregister2 = registerField('workerCommand');
|
||||||
|
const unregister3 = registerField('selectedGPUs');
|
||||||
|
return () => {
|
||||||
|
unregister1();
|
||||||
|
unregister2();
|
||||||
|
unregister3();
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleSelect = (key: string, item: any) => {
|
||||||
|
// Disabled cards are already blocked by TemplateCard; this is a
|
||||||
|
// defensive check for the multi-active case (only override runtimes
|
||||||
|
// can be added once multi-add is open).
|
||||||
|
if (availableKeys && !availableKeys.has(key)) return;
|
||||||
|
if (item) {
|
||||||
|
itemMetaRef.current[key] = {
|
||||||
|
label: item.label,
|
||||||
|
link: item.link
|
||||||
|
};
|
||||||
|
}
|
||||||
|
setSelectedKeys((prev) => {
|
||||||
|
const has = prev.includes(key);
|
||||||
|
if (has) {
|
||||||
|
// Clicking a selected card always toggles it off.
|
||||||
|
return prev.filter((v) => v !== key);
|
||||||
|
}
|
||||||
|
// Multi-add only when the current state already includes an override
|
||||||
|
// pick. Otherwise (still in single-select land), replace.
|
||||||
|
if (isMultiActive) return [...prev, key];
|
||||||
|
return [key];
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
// Warn when multi-select is possible on this cluster but the user's
|
||||||
const unregisterField = registerField('currentGPU');
|
// current pick lands outside the override list — they're effectively
|
||||||
return () => {
|
// locked into single-select until they switch to an override vendor.
|
||||||
unregisterField();
|
const showSingleOnlyHint =
|
||||||
};
|
multiCapable &&
|
||||||
}, []);
|
selectedKeys.length > 0 &&
|
||||||
|
selectedKeys.every((k) => !overrideKeys.has(k));
|
||||||
useEffect(() => {
|
|
||||||
const unregisterField = registerField('workerCommand');
|
|
||||||
return () => {
|
|
||||||
unregisterField();
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
updateField('currentGPU', GPUDriverMap.NVIDIA);
|
|
||||||
updateField('workerCommand', {
|
|
||||||
label: 'NVIDIA',
|
|
||||||
link: 'https://docs.gpustack.ai/latest/installation/requirements/#nvidia-gpu',
|
|
||||||
notes: AddWorkerDockerNotes[GPUDriverMap.NVIDIA]
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StepCollapse
|
<StepCollapse
|
||||||
@@ -58,12 +165,38 @@ const SelectVendor: React.FC<AddWorkerStepProps> = ({ disabled }) => {
|
|||||||
<Title>
|
<Title>
|
||||||
{stepIndex}.{' '}
|
{stepIndex}.{' '}
|
||||||
{intl.formatMessage({ id: 'clusters.addworker.selectGPU' })}
|
{intl.formatMessage({ id: 'clusters.addworker.selectGPU' })}
|
||||||
|
{multiCapable && (
|
||||||
|
<Tag
|
||||||
|
color="blue"
|
||||||
|
style={{
|
||||||
|
marginLeft: 8,
|
||||||
|
fontWeight: 400,
|
||||||
|
borderRadius: 4
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{intl.formatMessage({
|
||||||
|
id: 'clusters.addworker.selectGPU.multiTag'
|
||||||
|
})}
|
||||||
|
</Tag>
|
||||||
|
)}
|
||||||
</Title>
|
</Title>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
{showSingleOnlyHint && (
|
||||||
|
<Alert
|
||||||
|
type="info"
|
||||||
|
showIcon
|
||||||
|
icon={<BulbOutlined />}
|
||||||
|
style={{ marginBottom: 8 }}
|
||||||
|
message={intl.formatMessage({
|
||||||
|
id: 'clusters.addworker.selectGPU.singleOnly'
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<SupportedGPUs
|
<SupportedGPUs
|
||||||
onSelect={handleSelectProvider}
|
onSelect={handleSelect}
|
||||||
current={currentGPU}
|
current={selectedKeys}
|
||||||
|
availableKeys={availableKeys}
|
||||||
clickable={true}
|
clickable={true}
|
||||||
/>
|
/>
|
||||||
</StepCollapse>
|
</StepCollapse>
|
||||||
|
|||||||
@@ -6,18 +6,31 @@ import { ProviderType } from '../config';
|
|||||||
type ViewModalProps = {
|
type ViewModalProps = {
|
||||||
provider: ProviderType;
|
provider: ProviderType;
|
||||||
currentGPU: string;
|
currentGPU: string;
|
||||||
|
// When multiple vendors are selected (K8s multi-vendor register flow),
|
||||||
|
// we emit one check command per vendor so the user can verify each
|
||||||
|
// runtimeclass is registered.
|
||||||
|
currentGPUs?: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
const AddWorkerCommand: React.FC<ViewModalProps> = ({
|
const AddWorkerCommand: React.FC<ViewModalProps> = ({
|
||||||
provider = '',
|
provider = '',
|
||||||
currentGPU
|
currentGPU,
|
||||||
|
currentGPUs
|
||||||
}) => {
|
}) => {
|
||||||
console.log('check env command provider:', currentGPU);
|
|
||||||
const code = React.useMemo(() => {
|
const code = React.useMemo(() => {
|
||||||
const configs = addWorkerGuide['all'];
|
const configs = addWorkerGuide['all'];
|
||||||
const command = configs.checkEnvCommand(currentGPU);
|
const keys =
|
||||||
return command[provider || ''];
|
currentGPUs && currentGPUs.length > 0
|
||||||
}, [provider, currentGPU]);
|
? currentGPUs
|
||||||
|
: currentGPU
|
||||||
|
? [currentGPU]
|
||||||
|
: [];
|
||||||
|
if (!keys.length) return '';
|
||||||
|
const lines = keys
|
||||||
|
.map((k) => configs.checkEnvCommand(k)?.[provider || ''])
|
||||||
|
.filter((cmd): cmd is string => !!cmd);
|
||||||
|
return lines.join('\n');
|
||||||
|
}, [provider, currentGPU, currentGPUs]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<HighlightCode
|
<HighlightCode
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { systemConfigAtom } from '@/atoms/system';
|
||||||
import PluginExtraFields from '@/components/plugin-extra-fields';
|
import PluginExtraFields from '@/components/plugin-extra-fields';
|
||||||
import { PageAction } from '@/config';
|
import { PageAction } from '@/config';
|
||||||
import { PageActionType } from '@/config/types';
|
import { PageActionType } from '@/config/types';
|
||||||
@@ -8,7 +9,9 @@ import {
|
|||||||
Textarea as SealTextArea
|
Textarea as SealTextArea
|
||||||
} from '@gpustack/core-ui';
|
} from '@gpustack/core-ui';
|
||||||
import { useIntl } from '@umijs/max';
|
import { useIntl } from '@umijs/max';
|
||||||
import { Form } from 'antd';
|
import { Form, message } from 'antd';
|
||||||
|
import { useAtomValue } from 'jotai';
|
||||||
|
import _ from 'lodash';
|
||||||
import React, { forwardRef, useEffect, useImperativeHandle } from 'react';
|
import React, { forwardRef, useEffect, useImperativeHandle } from 'react';
|
||||||
import { ProviderType, ProviderValueMap } from '../config';
|
import { ProviderType, ProviderValueMap } from '../config';
|
||||||
import {
|
import {
|
||||||
@@ -32,6 +35,7 @@ const ClusterForm: React.FC<AddModalProps> = forwardRef(
|
|||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const [activeKey, setActiveKey] = React.useState<string[]>([]);
|
const [activeKey, setActiveKey] = React.useState<string[]>([]);
|
||||||
const advanceConfigRef = React.useRef<any>(null);
|
const advanceConfigRef = React.useRef<any>(null);
|
||||||
|
const systemConfig = useAtomValue(systemConfigAtom);
|
||||||
|
|
||||||
const handleOnCollapseChange = async (keys: string | string[]) => {
|
const handleOnCollapseChange = async (keys: string | string[]) => {
|
||||||
setActiveKey(Array.isArray(keys) ? keys : [keys]);
|
setActiveKey(Array.isArray(keys) ? keys : [keys]);
|
||||||
@@ -50,20 +54,107 @@ const ClusterForm: React.FC<AddModalProps> = forwardRef(
|
|||||||
}
|
}
|
||||||
}, [activeKey, action]);
|
}, [activeKey, action]);
|
||||||
|
|
||||||
const handleOnFinish = (values: FormData) => {
|
// Mirror the backend's `_validate_multi_vendor_overrides` check so the
|
||||||
const workerConfig = yaml2Json(advanceConfigRef.current?.getYamlValue());
|
// user sees the problem at save time instead of as a 400 when they
|
||||||
|
// run the register command. Returns the i18n'd message of the first
|
||||||
|
// problem found, or null when clean.
|
||||||
|
const validateGpuVendorOverrides = (values: any): string | null => {
|
||||||
|
const opts = values?.k8s_options;
|
||||||
|
const overrides = opts?.gpuVendorOverrides;
|
||||||
|
if (!overrides) return null;
|
||||||
|
const entries = Object.entries(overrides) as [string, any][];
|
||||||
|
if (entries.length === 0) return null;
|
||||||
|
|
||||||
onFinish({
|
// 1. Every override entry must declare a non-empty nodeSelector,
|
||||||
...values,
|
// otherwise it can't actually pin its DaemonSet to anything.
|
||||||
worker_config: {
|
for (const [vendor, override] of entries) {
|
||||||
...workerConfig
|
const sel = override?.nodeSelector;
|
||||||
|
if (!sel || Object.keys(sel).length === 0) {
|
||||||
|
return intl.formatMessage(
|
||||||
|
{ id: 'clusters.gpuVendorOverrides.validate.emptySelector' },
|
||||||
|
{ vendor }
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
|
// 2. Two vendors with the same selector would fight for the same
|
||||||
|
// nodes — backend rejects this at manifest render.
|
||||||
|
for (let i = 0; i < entries.length; i++) {
|
||||||
|
for (let j = i + 1; j < entries.length; j++) {
|
||||||
|
const [v1, o1] = entries[i];
|
||||||
|
const [v2, o2] = entries[j];
|
||||||
|
if (_.isEqual(o1?.nodeSelector, o2?.nodeSelector)) {
|
||||||
|
return intl.formatMessage(
|
||||||
|
{ id: 'clusters.gpuVendorOverrides.validate.duplicate' },
|
||||||
|
{ v1, v2 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Base nodeSelector keys can't be reused in any override —
|
||||||
|
// the CPU worker would require AND forbid the same key.
|
||||||
|
const baseKeys = Object.keys(opts?.nodeSelector || {});
|
||||||
|
if (baseKeys.length > 0) {
|
||||||
|
for (const [vendor, override] of entries) {
|
||||||
|
const overrideKeys = Object.keys(override?.nodeSelector || {});
|
||||||
|
const clash = overrideKeys.filter((k) => baseKeys.includes(k));
|
||||||
|
if (clash.length > 0) {
|
||||||
|
return intl.formatMessage(
|
||||||
|
{ id: 'clusters.gpuVendorOverrides.validate.keyConflict' },
|
||||||
|
{ vendor, keys: clash.join(', ') }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Empty username/password aren't meaningful as credentials — the backend
|
||||||
|
// models them as Optional[str] and treats null as "no auth". Coerce
|
||||||
|
// before sending so a public registry placeholder stays unambiguous.
|
||||||
|
const normalizeOutgoing = (values: any): any => {
|
||||||
|
const creds = values?.k8s_options?.imageCredentials;
|
||||||
|
if (!Array.isArray(creds)) return values;
|
||||||
|
const fixed = creds.map((c: any) => ({
|
||||||
|
...c,
|
||||||
|
username: c?.username ? c.username : null,
|
||||||
|
password: c?.password ? c.password : null
|
||||||
|
}));
|
||||||
|
return {
|
||||||
|
...values,
|
||||||
|
k8s_options: { ...values.k8s_options, imageCredentials: fixed }
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOnFinish = (_values: FormData) => {
|
||||||
|
const workerConfig = yaml2Json(advanceConfigRef.current?.getYamlValue());
|
||||||
|
// antd's onFinish only delivers values for registered Form.Items.
|
||||||
|
// Spreading those on top of `getFieldsValue(true)` clobbers nested
|
||||||
|
// objects (e.g. `k8s_options` would lose `gpuVendorOverrides`, which is
|
||||||
|
// only set via setFieldValue), so we go straight to the full store.
|
||||||
|
const fullValues = form.getFieldsValue(true);
|
||||||
|
|
||||||
|
const overridesErr = validateGpuVendorOverrides(fullValues);
|
||||||
|
if (overridesErr) {
|
||||||
|
message.error(overridesErr);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
onFinish(
|
||||||
|
normalizeOutgoing({
|
||||||
|
...fullValues,
|
||||||
|
worker_config: {
|
||||||
|
...workerConfig
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (currentData) {
|
if (currentData) {
|
||||||
const volumeMounts = currentData?.k8s_volume_mounts || [];
|
const volumeMounts = currentData?.k8s_options?.volumeMounts || [];
|
||||||
const realVolumeList = (volumeMounts || []).map(
|
const realVolumeList = (volumeMounts || []).map(
|
||||||
(item: any, index: number) => ({
|
(item: any, index: number) => ({
|
||||||
...item,
|
...item,
|
||||||
@@ -72,27 +163,40 @@ const ClusterForm: React.FC<AddModalProps> = forwardRef(
|
|||||||
);
|
);
|
||||||
form.setFieldsValue({
|
form.setFieldsValue({
|
||||||
...currentData,
|
...currentData,
|
||||||
k8s_volume_mounts: realVolumeList
|
k8s_options: {
|
||||||
|
...(currentData?.k8s_options || {}),
|
||||||
|
volumeMounts: realVolumeList
|
||||||
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
const defaultRegistry = systemConfig?.system_default_container_registry;
|
||||||
form.setFieldsValue({
|
form.setFieldsValue({
|
||||||
k8s_volume_mounts: [
|
k8s_options: {
|
||||||
{
|
volumeMounts: [
|
||||||
name: 'gpustack-data-dir',
|
{
|
||||||
mountPath: '/var/lib/gpustack',
|
name: 'gpustack-data-dir',
|
||||||
readOnly: false,
|
mountPath: '/var/lib/gpustack',
|
||||||
sourceType: 'hostPath',
|
readOnly: false,
|
||||||
volumeSource: {
|
sourceType: 'hostPath',
|
||||||
hostPath: {
|
volumeSource: {
|
||||||
path: '/var/lib/gpustack',
|
hostPath: {
|
||||||
type: 'DirectoryOrCreate'
|
path: '/var/lib/gpustack',
|
||||||
|
type: 'DirectoryOrCreate'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
],
|
||||||
]
|
...(defaultRegistry
|
||||||
|
? {
|
||||||
|
imageCredentials: [
|
||||||
|
{ registry: defaultRegistry, username: '', password: '' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
: {})
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [currentData]);
|
}, [currentData, systemConfig?.system_default_container_registry]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (currentData) {
|
if (currentData) {
|
||||||
@@ -130,17 +234,33 @@ const ClusterForm: React.FC<AddModalProps> = forwardRef(
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
validateFields: async () => {
|
validateFields: async () => {
|
||||||
const values = await form.validateFields();
|
// Run validation first to display any field errors. Then read the
|
||||||
|
// FULL store via `getFieldsValue(true)` so values that were set via
|
||||||
|
// setFieldValue on non-registered paths (e.g. gpuVendorOverrides)
|
||||||
|
// are still included in what we hand to the API.
|
||||||
|
await form.validateFields();
|
||||||
|
const values = form.getFieldsValue(true);
|
||||||
|
|
||||||
|
// Mirror the backend invariants for gpuVendorOverrides so the user
|
||||||
|
// sees the same constraint before submit instead of at register time.
|
||||||
|
const overridesErr = validateGpuVendorOverrides(values);
|
||||||
|
if (overridesErr) {
|
||||||
|
message.error(overridesErr);
|
||||||
|
// Reject so the step-flow's Promise.allSettled marks this form
|
||||||
|
// as failed and the outer onNext skips the submit callback.
|
||||||
|
throw new Error(overridesErr);
|
||||||
|
}
|
||||||
|
|
||||||
const workerConfig = yaml2Json(
|
const workerConfig = yaml2Json(
|
||||||
advanceConfigRef.current?.getYamlValue()
|
advanceConfigRef.current?.getYamlValue()
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return normalizeOutgoing({
|
||||||
...values,
|
...values,
|
||||||
worker_config: {
|
worker_config: {
|
||||||
...workerConfig
|
...workerConfig
|
||||||
}
|
}
|
||||||
};
|
});
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -207,6 +327,7 @@ const ClusterForm: React.FC<AddModalProps> = forwardRef(
|
|||||||
<AdvanceConfig
|
<AdvanceConfig
|
||||||
action={action}
|
action={action}
|
||||||
provider={provider}
|
provider={provider}
|
||||||
|
currentData={currentData}
|
||||||
ref={advanceConfigRef}
|
ref={advanceConfigRef}
|
||||||
></AdvanceConfig>
|
></AdvanceConfig>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,7 +1,14 @@
|
|||||||
import { Steps } from 'antd';
|
import { Steps } from 'antd';
|
||||||
|
import _ from 'lodash';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
// `description` is intentionally omitted: the upstream step list ships
|
||||||
|
// hardcoded English copy that isn't translated. Keeping it would cause the
|
||||||
|
// step to render both the localized title and the English description side
|
||||||
|
// by side. Same reason we don't surface `subTitle`.
|
||||||
|
const ANTD_STEP_KEYS = ['title', 'icon', 'status', 'disabled'] as const;
|
||||||
|
|
||||||
const Wrapper = styled.div`
|
const Wrapper = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -39,7 +46,14 @@ const ClusterSteps: React.FC<{
|
|||||||
}> = (props) => {
|
}> = (props) => {
|
||||||
const { steps, currentStep = 0, onChange } = props;
|
const { steps, currentStep = 0, onChange } = props;
|
||||||
|
|
||||||
const visibleSteps = steps.filter((step) => !step.hideInSteps);
|
// Pick only props antd's Step accepts — the upstream step objects carry
|
||||||
|
// custom keys (showModules/showForms/showButtons/...) that would otherwise
|
||||||
|
// be forwarded to the DOM and trigger "React does not recognize the X
|
||||||
|
// prop on a DOM element" warnings. _.pick keeps missing keys missing
|
||||||
|
// (rather than explicitly `undefined`) so antd's defaults still kick in.
|
||||||
|
const visibleSteps = steps
|
||||||
|
.filter((step) => !step.hideInSteps)
|
||||||
|
.map((step) => _.pick(step, ANTD_STEP_KEYS));
|
||||||
|
|
||||||
const styles: Record<string, any> = {
|
const styles: Record<string, any> = {
|
||||||
root: {
|
root: {
|
||||||
|
|||||||
@@ -0,0 +1,413 @@
|
|||||||
|
import {
|
||||||
|
MinusOutlined,
|
||||||
|
PlusOutlined,
|
||||||
|
QuestionCircleOutlined
|
||||||
|
} from '@ant-design/icons';
|
||||||
|
import {
|
||||||
|
Input as CInput,
|
||||||
|
CollapseContainer,
|
||||||
|
LabelSelector,
|
||||||
|
Select as SealSelect,
|
||||||
|
useAppUtils
|
||||||
|
} from '@gpustack/core-ui';
|
||||||
|
import { useIntl } from '@umijs/max';
|
||||||
|
import { Button, Form, Tooltip } from 'antd';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
import { GPUsConfigs } from '../../resources/config/gpu-driver';
|
||||||
|
|
||||||
|
const Title = styled.div`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
background-color: transparent;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 14px;
|
||||||
|
padding-top: 0px;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Label = styled.span`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
color: var(--ant-color-text-secondary);
|
||||||
|
`;
|
||||||
|
|
||||||
|
const SectionWrap = styled.div`
|
||||||
|
margin-bottom: 16px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const ImageCredentialsForm: React.FC = () => {
|
||||||
|
const intl = useIntl();
|
||||||
|
const { getRuleMessage } = useAppUtils();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SectionWrap>
|
||||||
|
<Form.List name={['k8s_options', 'imageCredentials']}>
|
||||||
|
{(fields, { add, remove }) => (
|
||||||
|
<>
|
||||||
|
<Title>
|
||||||
|
<div className="flex-center gap-8">
|
||||||
|
<span>
|
||||||
|
{intl.formatMessage({
|
||||||
|
id: 'clusters.imageCredentials.title'
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
<Button
|
||||||
|
type="link"
|
||||||
|
onClick={() =>
|
||||||
|
add({ registry: '', username: '', password: '' })
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<PlusOutlined />{' '}
|
||||||
|
{intl.formatMessage({
|
||||||
|
id: 'clusters.imageCredentials.add'
|
||||||
|
})}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Title>
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
|
||||||
|
{fields.map(({ key, name }) => (
|
||||||
|
<div
|
||||||
|
key={key}
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'flex-start',
|
||||||
|
gap: 8,
|
||||||
|
padding: 12,
|
||||||
|
border: '1px solid var(--ant-color-split)',
|
||||||
|
borderRadius: 'var(--ant-border-radius-lg)'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: 12
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Form.Item
|
||||||
|
name={[name, 'registry']}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: getRuleMessage(
|
||||||
|
'input',
|
||||||
|
'clusters.imageCredentials.registry'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
style={{ marginBottom: 0 }}
|
||||||
|
>
|
||||||
|
<CInput.Input
|
||||||
|
required
|
||||||
|
label={intl.formatMessage({
|
||||||
|
id: 'clusters.imageCredentials.registry'
|
||||||
|
})}
|
||||||
|
></CInput.Input>
|
||||||
|
</Form.Item>
|
||||||
|
<div style={{ display: 'flex', gap: 12 }}>
|
||||||
|
<div style={{ flex: 1 }}>
|
||||||
|
<Form.Item
|
||||||
|
name={[name, 'username']}
|
||||||
|
style={{ marginBottom: 0 }}
|
||||||
|
>
|
||||||
|
<CInput.Input
|
||||||
|
label={intl.formatMessage({
|
||||||
|
id: 'clusters.imageCredentials.username'
|
||||||
|
})}
|
||||||
|
></CInput.Input>
|
||||||
|
</Form.Item>
|
||||||
|
</div>
|
||||||
|
<div style={{ flex: 1 }}>
|
||||||
|
<Form.Item
|
||||||
|
name={[name, 'password']}
|
||||||
|
style={{ marginBottom: 0 }}
|
||||||
|
>
|
||||||
|
<CInput.Password
|
||||||
|
label={intl.formatMessage({
|
||||||
|
id: 'clusters.imageCredentials.password'
|
||||||
|
})}
|
||||||
|
></CInput.Password>
|
||||||
|
</Form.Item>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
shape="circle"
|
||||||
|
style={{ marginTop: 8 }}
|
||||||
|
onClick={() => remove(name)}
|
||||||
|
>
|
||||||
|
<MinusOutlined />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Form.List>
|
||||||
|
</SectionWrap>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const NodeSelectorForm: React.FC = () => {
|
||||||
|
const intl = useIntl();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SectionWrap>
|
||||||
|
<Title>
|
||||||
|
<span className="flex-center gap-4">
|
||||||
|
<span>
|
||||||
|
{intl.formatMessage({ id: 'clusters.nodeSelector.title' })}
|
||||||
|
</span>
|
||||||
|
<Tooltip
|
||||||
|
title={intl.formatMessage({ id: 'clusters.nodeSelector.tip' })}
|
||||||
|
>
|
||||||
|
<QuestionCircleOutlined
|
||||||
|
style={{ color: 'var(--ant-color-text-secondary)' }}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</span>
|
||||||
|
</Title>
|
||||||
|
<Form.Item name={['k8s_options', 'nodeSelector']}>
|
||||||
|
<LabelSelector
|
||||||
|
label={intl.formatMessage({ id: 'clusters.nodeSelector.title' })}
|
||||||
|
></LabelSelector>
|
||||||
|
</Form.Item>
|
||||||
|
</SectionWrap>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const vendorOptions = Object.values(GPUsConfigs)
|
||||||
|
.filter((c) => !!c.gpuVendor)
|
||||||
|
.map((c) => ({ label: c.label, value: c.gpuVendor as string }));
|
||||||
|
|
||||||
|
const GpuVendorOverridesForm: React.FC<{
|
||||||
|
initialValue?: Record<string, { nodeSelector?: Record<string, string> }>;
|
||||||
|
}> = ({ initialValue }) => {
|
||||||
|
const intl = useIntl();
|
||||||
|
const form = Form.useFormInstance();
|
||||||
|
|
||||||
|
// Single source of truth for the cards while the user is editing.
|
||||||
|
// We mirror it into the form via setFieldValue so submit picks it up.
|
||||||
|
// Seed local state directly from the parent's currentData — Form.useWatch
|
||||||
|
// on a non-registered nested path proved unreliable for picking up
|
||||||
|
// initialValues, so we cut out that indirection.
|
||||||
|
const [overrides, setOverrides] = useState<
|
||||||
|
Record<string, { nodeSelector?: Record<string, string> }>
|
||||||
|
>(() => initialValue || {});
|
||||||
|
const [collapseKey, setCollapseKey] = useState<Set<string>>(new Set());
|
||||||
|
const initializedRef = useRef(!!initialValue);
|
||||||
|
|
||||||
|
// On mount: if we seeded with an initialValue, mirror it into the form so
|
||||||
|
// submit collects it. (When seeded, initializedRef is already true.)
|
||||||
|
useEffect(() => {
|
||||||
|
if (initialValue && Object.keys(initialValue).length > 0) {
|
||||||
|
form.setFieldValue(['k8s_options', 'gpuVendorOverrides'], initialValue);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// If the parent currentData arrives after mount (e.g. async fetch), adopt
|
||||||
|
// it once. After the user has interacted (`initializedRef`), local state
|
||||||
|
// owns the visible list.
|
||||||
|
useEffect(() => {
|
||||||
|
if (initializedRef.current) return;
|
||||||
|
if (initialValue && Object.keys(initialValue).length > 0) {
|
||||||
|
setOverrides(initialValue);
|
||||||
|
form.setFieldValue(['k8s_options', 'gpuVendorOverrides'], initialValue);
|
||||||
|
initializedRef.current = true;
|
||||||
|
}
|
||||||
|
}, [initialValue, form]);
|
||||||
|
|
||||||
|
// Once the user has touched this section, keep the form mirror in sync
|
||||||
|
// so submit collects what's currently on screen even if the form was
|
||||||
|
// reset externally (e.g., parent re-renders that touch initialValues).
|
||||||
|
useEffect(() => {
|
||||||
|
if (!initializedRef.current) return;
|
||||||
|
form.setFieldValue(
|
||||||
|
['k8s_options', 'gpuVendorOverrides'],
|
||||||
|
Object.keys(overrides).length > 0 ? overrides : undefined
|
||||||
|
);
|
||||||
|
}, [overrides, form]);
|
||||||
|
|
||||||
|
const vendorKeys = useMemo(() => Object.keys(overrides), [overrides]);
|
||||||
|
|
||||||
|
const availableVendors = useMemo(
|
||||||
|
() => vendorOptions.filter((opt) => !vendorKeys.includes(opt.value)),
|
||||||
|
[vendorKeys]
|
||||||
|
);
|
||||||
|
|
||||||
|
const writeOverrides = (next: Record<string, any>) => {
|
||||||
|
setOverrides(next);
|
||||||
|
form.setFieldValue(
|
||||||
|
['k8s_options', 'gpuVendorOverrides'],
|
||||||
|
Object.keys(next).length > 0 ? next : undefined
|
||||||
|
);
|
||||||
|
initializedRef.current = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAdd = () => {
|
||||||
|
if (availableVendors.length === 0) return;
|
||||||
|
const next = availableVendors[0].value;
|
||||||
|
writeOverrides({ ...overrides, [next]: { nodeSelector: {} } });
|
||||||
|
setCollapseKey(new Set([next]));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemove = (vendor: string) => {
|
||||||
|
writeOverrides(_.omit(overrides, vendor));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleVendorChange = (oldKey: string, newKey: string) => {
|
||||||
|
if (oldKey === newKey) return;
|
||||||
|
const value = overrides[oldKey] ?? { nodeSelector: {} };
|
||||||
|
const next = _.omit(overrides, oldKey);
|
||||||
|
next[newKey] = value;
|
||||||
|
writeOverrides(next);
|
||||||
|
setCollapseKey(new Set([newKey]));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleNodeSelectorChange = (
|
||||||
|
vendor: string,
|
||||||
|
labels: Record<string, string>
|
||||||
|
) => {
|
||||||
|
writeOverrides({
|
||||||
|
...overrides,
|
||||||
|
[vendor]: { ...overrides[vendor], nodeSelector: labels }
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onToggle = (open: boolean, key: string) => {
|
||||||
|
setCollapseKey(open ? new Set([key]) : new Set());
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SectionWrap>
|
||||||
|
<Title>
|
||||||
|
<div className="flex-center gap-8">
|
||||||
|
<span className="flex-center gap-4">
|
||||||
|
<span>
|
||||||
|
{intl.formatMessage({ id: 'clusters.gpuVendorOverrides.title' })}
|
||||||
|
</span>
|
||||||
|
<Tooltip
|
||||||
|
title={intl.formatMessage({
|
||||||
|
id: 'clusters.gpuVendorOverrides.tip'
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<QuestionCircleOutlined
|
||||||
|
style={{ color: 'var(--ant-color-text-secondary)' }}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</span>
|
||||||
|
<Button
|
||||||
|
type="link"
|
||||||
|
onClick={handleAdd}
|
||||||
|
disabled={availableVendors.length === 0}
|
||||||
|
>
|
||||||
|
<PlusOutlined />{' '}
|
||||||
|
{intl.formatMessage({ id: 'clusters.gpuVendorOverrides.add' })}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Title>
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
|
||||||
|
{vendorKeys.map((vendor) => {
|
||||||
|
const optionsForThisRow = [
|
||||||
|
...vendorOptions.filter(
|
||||||
|
(opt) => opt.value === vendor || !vendorKeys.includes(opt.value)
|
||||||
|
)
|
||||||
|
];
|
||||||
|
const vendorLabel =
|
||||||
|
vendorOptions.find((o) => o.value === vendor)?.label || vendor;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={vendor}
|
||||||
|
style={{
|
||||||
|
border: '1px solid var(--ant-color-split)',
|
||||||
|
borderRadius: 'var(--ant-border-radius-lg)'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CollapseContainer
|
||||||
|
collapsible={true}
|
||||||
|
showExpandIcon={true}
|
||||||
|
open={collapseKey.has(vendor)}
|
||||||
|
onToggle={(open: boolean) => onToggle(open, vendor)}
|
||||||
|
styles={{
|
||||||
|
body: collapseKey.has(vendor) ? { padding: 16 } : {},
|
||||||
|
content: { paddingTop: 0 },
|
||||||
|
header: { backgroundColor: 'unset' }
|
||||||
|
}}
|
||||||
|
title={
|
||||||
|
<Label>
|
||||||
|
<span>
|
||||||
|
{intl.formatMessage({
|
||||||
|
id: 'clusters.gpuVendorOverrides.vendor'
|
||||||
|
})}
|
||||||
|
:
|
||||||
|
</span>
|
||||||
|
<span>{vendorLabel}</span>
|
||||||
|
</Label>
|
||||||
|
}
|
||||||
|
right={
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
shape="circle"
|
||||||
|
onClick={() => handleRemove(vendor)}
|
||||||
|
>
|
||||||
|
<MinusOutlined />
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div style={{ marginBottom: 16, width: '100%' }}>
|
||||||
|
<SealSelect
|
||||||
|
isInFormItems={false}
|
||||||
|
required
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
label={intl.formatMessage({
|
||||||
|
id: 'clusters.gpuVendorOverrides.vendor'
|
||||||
|
})}
|
||||||
|
value={vendor}
|
||||||
|
options={optionsForThisRow}
|
||||||
|
onChange={(value: string) =>
|
||||||
|
handleVendorChange(vendor, value)
|
||||||
|
}
|
||||||
|
></SealSelect>
|
||||||
|
</div>
|
||||||
|
<LabelSelector
|
||||||
|
label={intl.formatMessage({
|
||||||
|
id: 'clusters.gpuVendorOverrides.nodeSelector'
|
||||||
|
})}
|
||||||
|
value={overrides[vendor]?.nodeSelector || {}}
|
||||||
|
onChange={(labels) =>
|
||||||
|
handleNodeSelectorChange(vendor, labels)
|
||||||
|
}
|
||||||
|
></LabelSelector>
|
||||||
|
</CollapseContainer>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</SectionWrap>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
type GpuVendorOverridesValue = Record<
|
||||||
|
string,
|
||||||
|
{ nodeSelector?: Record<string, string> }
|
||||||
|
>;
|
||||||
|
|
||||||
|
const K8sPodSpec: React.FC<{
|
||||||
|
initialOverrides?: GpuVendorOverridesValue;
|
||||||
|
}> = ({ initialOverrides }) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ImageCredentialsForm />
|
||||||
|
<NodeSelectorForm />
|
||||||
|
<GpuVendorOverridesForm initialValue={initialOverrides} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default K8sPodSpec;
|
||||||
@@ -26,7 +26,7 @@ const Title = styled.div`
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
background-color: var(--ant-color-bg-container);
|
background-color: transparent;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
padding-top: 0px;
|
padding-top: 0px;
|
||||||
@@ -37,7 +37,7 @@ const VolumeMountsForm: React.FC<{ action: PageActionType }> = ({ action }) => {
|
|||||||
const form = Form.useFormInstance();
|
const form = Form.useFormInstance();
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const { getRuleMessage } = useAppUtils();
|
const { getRuleMessage } = useAppUtils();
|
||||||
const k8sVolumeMounts = Form.useWatch('k8s_volume_mounts', form);
|
const k8sVolumeMounts = Form.useWatch(['k8s_options', 'volumeMounts'], form);
|
||||||
|
|
||||||
const [collapseKey, setCollapseKey] = useState<Set<number | string>>(
|
const [collapseKey, setCollapseKey] = useState<Set<number | string>>(
|
||||||
new Set([0])
|
new Set([0])
|
||||||
@@ -60,7 +60,7 @@ const VolumeMountsForm: React.FC<{ action: PageActionType }> = ({ action }) => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (action === PageAction.CREATE) {
|
if (action === PageAction.CREATE) {
|
||||||
form.setFieldValue('k8s_volume_mounts', []);
|
form.setFieldValue(['k8s_options', 'volumeMounts'], []);
|
||||||
}
|
}
|
||||||
}, [action]);
|
}, [action]);
|
||||||
|
|
||||||
@@ -70,33 +70,36 @@ const VolumeMountsForm: React.FC<{ action: PageActionType }> = ({ action }) => {
|
|||||||
|
|
||||||
const handleAdd = async () => {
|
const handleAdd = async () => {
|
||||||
try {
|
try {
|
||||||
await form.validateFields(['k8s_volume_mounts'], {
|
await form.validateFields([['k8s_options', 'volumeMounts']], {
|
||||||
recursive: true
|
recursive: true
|
||||||
});
|
});
|
||||||
|
|
||||||
const list = form.getFieldValue('k8s_volume_mounts') || [];
|
const list = form.getFieldValue(['k8s_options', 'volumeMounts']) || [];
|
||||||
|
|
||||||
form.setFieldValue('k8s_volume_mounts', [
|
form.setFieldValue(
|
||||||
...list,
|
['k8s_options', 'volumeMounts'],
|
||||||
{
|
[
|
||||||
name: `volume-${list.length + 1}`,
|
...list,
|
||||||
mountPath: '',
|
{
|
||||||
readOnly: false,
|
name: `volume-${list.length + 1}`,
|
||||||
sourceType: 'hostPath',
|
mountPath: '',
|
||||||
volumeSource: {
|
readOnly: false,
|
||||||
hostPath: {
|
sourceType: 'hostPath',
|
||||||
path: '',
|
volumeSource: {
|
||||||
type: 'DirectoryOrCreate'
|
hostPath: {
|
||||||
|
path: '',
|
||||||
|
type: 'DirectoryOrCreate'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
]
|
||||||
]);
|
);
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setCollapseKey(new Set([list.length]));
|
setCollapseKey(new Set([list.length]));
|
||||||
}, 100);
|
}, 100);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
const errorIndex = e?.errorFields?.[0]?.name?.[1];
|
const errorIndex = e?.errorFields?.[0]?.name?.[2];
|
||||||
if (typeof errorIndex === 'number') {
|
if (typeof errorIndex === 'number') {
|
||||||
setCollapseKey(new Set([errorIndex]));
|
setCollapseKey(new Set([errorIndex]));
|
||||||
}
|
}
|
||||||
@@ -121,7 +124,7 @@ const VolumeMountsForm: React.FC<{ action: PageActionType }> = ({ action }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
form.setFieldValue(
|
form.setFieldValue(
|
||||||
['k8s_volume_mounts', index, 'volumeSource'],
|
['k8s_options', 'volumeMounts', index, 'volumeSource'],
|
||||||
volumeSource
|
volumeSource
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -144,9 +147,10 @@ const VolumeMountsForm: React.FC<{ action: PageActionType }> = ({ action }) => {
|
|||||||
marginBottom: '8px'
|
marginBottom: '8px'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Form.List name="k8s_volume_mounts">
|
<Form.List name={['k8s_options', 'volumeMounts']}>
|
||||||
{(fields, { remove }) => {
|
{(fields, { remove }) => {
|
||||||
const list = form.getFieldValue('k8s_volume_mounts') || [];
|
const list =
|
||||||
|
form.getFieldValue(['k8s_options', 'volumeMounts']) || [];
|
||||||
|
|
||||||
return fields.map(({ name }) => {
|
return fields.map(({ name }) => {
|
||||||
const item = list[name] || {};
|
const item = list[name] || {};
|
||||||
|
|||||||
@@ -68,7 +68,8 @@ interface ProviderCatalogProps {
|
|||||||
onSelect?: (provider: string, item: any) => void;
|
onSelect?: (provider: string, item: any) => void;
|
||||||
groupIcons?: Record<string, string>;
|
groupIcons?: Record<string, string>;
|
||||||
cols?: number;
|
cols?: number;
|
||||||
current?: ProviderType | string;
|
// Single value (legacy) or array of selected keys for multi-select.
|
||||||
|
current?: ProviderType | string | string[];
|
||||||
clickable?: boolean;
|
clickable?: boolean;
|
||||||
height: string | number;
|
height: string | number;
|
||||||
showTooltip?: boolean;
|
showTooltip?: boolean;
|
||||||
@@ -149,7 +150,11 @@ const ProviderCatalog: React.FC<ProviderCatalogProps> = ({
|
|||||||
<TemplateCard
|
<TemplateCard
|
||||||
height={height}
|
height={height}
|
||||||
onClick={() => onSelect?.(action.key as string, action)}
|
onClick={() => onSelect?.(action.key as string, action)}
|
||||||
active={current === action.key}
|
active={
|
||||||
|
Array.isArray(current)
|
||||||
|
? current.includes(action.key)
|
||||||
|
: current === action.key
|
||||||
|
}
|
||||||
disabled={action.disabled}
|
disabled={action.disabled}
|
||||||
clickable={clickable}
|
clickable={clickable}
|
||||||
header={renderTitle(action)}
|
header={renderTitle(action)}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { generateK8sRegisterCommand } from '../config';
|
|||||||
|
|
||||||
type AddModalProps = {
|
type AddModalProps = {
|
||||||
currentGPU?: string;
|
currentGPU?: string;
|
||||||
|
currentGPUs?: string[];
|
||||||
registrationInfo: {
|
registrationInfo: {
|
||||||
token: string;
|
token: string;
|
||||||
image: string;
|
image: string;
|
||||||
@@ -13,16 +14,18 @@ type AddModalProps = {
|
|||||||
};
|
};
|
||||||
const AddCluster: React.FC<AddModalProps> = ({
|
const AddCluster: React.FC<AddModalProps> = ({
|
||||||
registrationInfo,
|
registrationInfo,
|
||||||
currentGPU
|
currentGPU,
|
||||||
|
currentGPUs
|
||||||
}) => {
|
}) => {
|
||||||
const code = useMemo(() => {
|
const code = useMemo(() => {
|
||||||
return generateK8sRegisterCommand({
|
return generateK8sRegisterCommand({
|
||||||
server: registrationInfo?.server_url || window.location.origin,
|
server: registrationInfo?.server_url || window.location.origin,
|
||||||
clusterId: registrationInfo?.cluster_id,
|
clusterId: registrationInfo?.cluster_id,
|
||||||
registrationToken: registrationInfo?.token,
|
registrationToken: registrationInfo?.token,
|
||||||
currentGPU
|
currentGPU,
|
||||||
|
currentGPUs
|
||||||
});
|
});
|
||||||
}, [registrationInfo, currentGPU]);
|
}, [registrationInfo, currentGPU, currentGPUs]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -76,14 +76,19 @@ const ProviderImage = ({ src, height }: { src: string; height?: number }) => {
|
|||||||
|
|
||||||
interface SupportedHardwareProps {
|
interface SupportedHardwareProps {
|
||||||
onSelect?: (provider: string, item: any) => void;
|
onSelect?: (provider: string, item: any) => void;
|
||||||
current?: string;
|
// Single (legacy) or array of selected GPU driver keys (multi-select).
|
||||||
|
current?: string | string[];
|
||||||
clickable?: boolean;
|
clickable?: boolean;
|
||||||
|
// Set of GPU driver keys that are valid to pick. When provided, items
|
||||||
|
// outside this set render as disabled. Undefined means "no restriction".
|
||||||
|
availableKeys?: Set<string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SupportedHardware: React.FC<SupportedHardwareProps> = ({
|
const SupportedHardware: React.FC<SupportedHardwareProps> = ({
|
||||||
onSelect,
|
onSelect,
|
||||||
clickable,
|
clickable,
|
||||||
current
|
current,
|
||||||
|
availableKeys
|
||||||
}) => {
|
}) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const { userSettings } = useUserSettings();
|
const { userSettings } = useUserSettings();
|
||||||
@@ -199,13 +204,20 @@ const SupportedHardware: React.FC<SupportedHardwareProps> = ({
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const platformsWithDisabled = availableKeys
|
||||||
|
? supportedHardPlatforms.map((p) => ({
|
||||||
|
...p,
|
||||||
|
disabled: !availableKeys.has(p.value)
|
||||||
|
}))
|
||||||
|
: supportedHardPlatforms;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box className={userSettings?.theme === 'realDark' ? 'dark-theme' : ''}>
|
<Box className={userSettings?.theme === 'realDark' ? 'dark-theme' : ''}>
|
||||||
<ProviderCatalog
|
<ProviderCatalog
|
||||||
onSelect={onSelect}
|
onSelect={onSelect}
|
||||||
height={60}
|
height={60}
|
||||||
current={current}
|
current={current}
|
||||||
dataList={supportedHardPlatforms}
|
dataList={platformsWithDisabled}
|
||||||
clickable={clickable}
|
clickable={clickable}
|
||||||
showTooltip={true}
|
showTooltip={true}
|
||||||
cols={5}
|
cols={5}
|
||||||
|
|||||||
@@ -44,13 +44,26 @@ export const ProviderLabelMap = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const generateK8sRegisterCommand = (params: {
|
export const generateK8sRegisterCommand = (params: {
|
||||||
|
// Either a single GPU driver key (legacy single-select) or an array of
|
||||||
|
// keys (multi-vendor mode). Both feed into a list of runtimes for the
|
||||||
|
// ?runtime=... query parameters the backend accepts (repeatable).
|
||||||
currentGPU?: string;
|
currentGPU?: string;
|
||||||
|
currentGPUs?: string[];
|
||||||
server: string;
|
server: string;
|
||||||
clusterId: number | null;
|
clusterId: number | null;
|
||||||
registrationToken: string;
|
registrationToken: string;
|
||||||
}) => {
|
}) => {
|
||||||
const runtime = GPUsConfigs[params.currentGPU || '']?.runtime || '';
|
const keys =
|
||||||
return `curl -k -L '${params.server}/${GPUSTACK_API_BASE_URL}/clusters/${params.clusterId}/manifests${runtime ? `?runtime=${runtime}` : ''}' \\
|
params.currentGPUs && params.currentGPUs.length > 0
|
||||||
|
? params.currentGPUs
|
||||||
|
: params.currentGPU
|
||||||
|
? [params.currentGPU]
|
||||||
|
: [];
|
||||||
|
const runtimes = keys
|
||||||
|
.map((k) => GPUsConfigs[k]?.runtime)
|
||||||
|
.filter((r): r is string => !!r);
|
||||||
|
const query = runtimes.map((r) => `runtime=${r}`).join('&');
|
||||||
|
return `curl -k -L '${params.server}/${GPUSTACK_API_BASE_URL}/clusters/${params.clusterId}/manifests${query ? `?${query}` : ''}' \\
|
||||||
--header 'Authorization: Bearer ${params.registrationToken}' | kubectl apply -f -`;
|
--header 'Authorization: Bearer ${params.registrationToken}' | kubectl apply -f -`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -68,6 +68,25 @@ export interface VolumeMount {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ImageCredential {
|
||||||
|
registry: string;
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface K8sOptions {
|
||||||
|
// Backend serializes K8sOptions with camelCase aliases (by_alias=True on
|
||||||
|
// the SQL JSON column). The top-level `k8s_options` field on the cluster
|
||||||
|
// stays snake_case, but everything inside follows the backend wire shape.
|
||||||
|
volumeMounts?: VolumeMount[];
|
||||||
|
imageCredentials?: ImageCredential[];
|
||||||
|
nodeSelector?: Record<string, string>;
|
||||||
|
gpuVendorOverrides?: Record<
|
||||||
|
string,
|
||||||
|
{ nodeSelector?: Record<string, string> }
|
||||||
|
>;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ClusterListItem {
|
export interface ClusterListItem {
|
||||||
name: string;
|
name: string;
|
||||||
display_name: string;
|
display_name: string;
|
||||||
@@ -87,9 +106,9 @@ export interface ClusterListItem {
|
|||||||
state: ClusterStatusType;
|
state: ClusterStatusType;
|
||||||
state_message: string;
|
state_message: string;
|
||||||
worker_pools: NodePoolListItem[];
|
worker_pools: NodePoolListItem[];
|
||||||
k8s_volume_mounts?: VolumeMount[];
|
k8s_options?: K8sOptions;
|
||||||
// Backend ClusterPublic carries this; admin-"All" namespace
|
// Backend ClusterPublic carries this; admin-"All" namespace
|
||||||
// resolution falls back to the cluster's owner Org slug.
|
// resolution falls back to the cluster's owner Org name.
|
||||||
owner_principal_id?: number;
|
owner_principal_id?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,7 +123,7 @@ export interface ClusterFormData {
|
|||||||
server_url?: string;
|
server_url?: string;
|
||||||
worker_config?: Record<string, any>;
|
worker_config?: Record<string, any>;
|
||||||
worker_pools?: NodePoolFormData[];
|
worker_pools?: NodePoolFormData[];
|
||||||
k8s_volume_mounts?: VolumeMount[];
|
k8s_options?: K8sOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SystemConfig {
|
export interface SystemConfig {
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user