Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a19fea0c6c | ||
|
|
a516e6ce72 | ||
|
|
2e375c6603 | ||
|
|
7d94c77c15 | ||
|
|
457d2f2f72 | ||
|
|
5e7d83e5dd | ||
|
|
b1cffe047c | ||
|
|
6509f2a4ff | ||
|
|
f719c11606 | ||
|
|
b8d7873b77 | ||
|
|
a9248a98a5 | ||
|
|
8f85e9a082 | ||
|
|
1a6d1654b2 | ||
|
|
abe705c034 | ||
|
|
8b34f68824 | ||
|
|
e37828a2cd | ||
|
|
4709661f93 | ||
|
|
1d7543f19c | ||
|
|
1cf146ca32 | ||
|
|
b05c510776 | ||
|
|
e79d5f8962 | ||
|
|
5503fdc21e | ||
|
|
36a1038d12 | ||
|
|
ee42a9d0ed | ||
|
|
28976dea4b | ||
|
|
22e5cc3aa5 | ||
|
|
3052c0b23e | ||
|
|
1c9cabf7e7 | ||
|
|
03842d1741 | ||
|
|
84304eadb7 | ||
|
|
ab3739219e | ||
|
|
7cf4932f3a | ||
|
|
032f1eefef | ||
|
|
e35be94f7c | ||
|
|
e76e2ad85a | ||
|
|
cb19cd2476 | ||
|
|
c532732ce9 | ||
|
|
2bc10dbb2e | ||
|
|
b634a03ef9 | ||
|
|
ac64bb98ad | ||
|
|
aa49539210 | ||
|
|
1f5678f781 | ||
|
|
7adc089e94 | ||
|
|
6e1e9c07ee | ||
|
|
1d55382f3b | ||
|
|
e68bb0bc4e | ||
|
|
c8e14dade5 | ||
|
|
e4c1a3f5c2 | ||
|
|
cdd6ef9cb3 | ||
|
|
737d43fd16 | ||
|
|
9f5163531c | ||
|
|
df5cf0f4e5 | ||
|
|
e160b64697 | ||
|
|
7d36c6ac4a | ||
|
|
0820edf55f | ||
|
|
12e324893f | ||
|
|
4017c4dd33 | ||
|
|
17b65d80e1 | ||
|
|
47cb3321ab | ||
|
|
547b9f617b | ||
|
|
9e7bd78693 | ||
|
|
c53d60b628 | ||
|
|
9566e0cdb2 | ||
|
|
ed3e779d22 | ||
|
|
3e2133753d | ||
|
|
736a853192 | ||
|
|
08ef2ffa9a | ||
|
|
6cf512cccd | ||
|
|
6f56e614f3 | ||
|
|
8c06862705 | ||
|
|
48159171c2 | ||
|
|
d5d0749b72 | ||
|
|
dc4dd81e11 | ||
|
|
c736e2186d | ||
|
|
7a76bda484 | ||
|
|
778ea36a9d | ||
|
|
04e1e1e566 | ||
|
|
01f66085ab | ||
|
|
ddf723525a | ||
|
|
7b18a92388 | ||
|
|
b985dcee63 | ||
|
|
77e92432ed | ||
|
|
a7b33a928b | ||
|
|
1218c6d096 | ||
|
|
6337431ed4 | ||
|
|
2ba5118bc2 | ||
|
|
12649c7ccf | ||
|
|
5d96932870 | ||
|
|
e6cfa6cf77 | ||
|
|
55d16c5681 | ||
|
|
ab75685955 | ||
|
|
bd0bf0bf63 | ||
|
|
fd050b33d9 | ||
|
|
698090b17a | ||
|
|
6f96260993 | ||
|
|
98a6b19298 | ||
|
|
ded270ff25 | ||
|
|
c8860207e8 | ||
|
|
8d045ffd8b | ||
|
|
fd7437e74a | ||
|
|
807ba866d3 | ||
|
|
5e2808d7c4 | ||
|
|
a26a3ff8f9 | ||
|
|
a2e9f69b96 | ||
|
|
ac800a303a | ||
|
|
dd542a9f29 | ||
|
|
26ce456bde | ||
|
|
7ce1faaf6c | ||
|
|
878bdaa5a7 | ||
|
|
68a7e3f66a | ||
|
|
feccc81c5f | ||
|
|
620045d7a3 | ||
|
|
6dc28d61b0 | ||
|
|
754cc29793 | ||
|
|
ed525509ea | ||
|
|
2b6f26e6c8 | ||
|
|
09821aab85 | ||
|
|
636190b8e6 | ||
|
|
e420c22a45 | ||
|
|
993f6edcc4 | ||
|
|
bda8264788 | ||
|
|
ab8e792276 | ||
|
|
0e4be63b6b | ||
|
|
d3ebea9090 | ||
|
|
15a9e23243 | ||
|
|
eab7d9c77f | ||
|
|
4a86b71018 | ||
|
|
6350f7f11a | ||
|
|
ce6388253c | ||
|
|
d8f6064d5e | ||
|
|
9f5d95f565 | ||
|
|
0cabbeb81c | ||
|
|
76618aa3a1 | ||
|
|
b19db0f67d | ||
|
|
76f228bfe7 | ||
|
|
be40692773 | ||
|
|
42ac377c17 | ||
|
|
46f859cc3c | ||
|
|
f2bff0d1fb | ||
|
|
0bd98b9662 | ||
|
|
1ac2c02197 | ||
|
|
907187d53a | ||
|
|
faf6246fe6 | ||
|
|
0c16e2be7a | ||
|
|
3212882895 | ||
|
|
62f34ffcaf | ||
|
|
50601b0aff | ||
|
|
4c5d42cb13 | ||
|
|
85268b8917 | ||
|
|
973d2c4529 | ||
|
|
ab766b8c54 | ||
|
|
4945242c87 | ||
|
|
c4a72eb375 | ||
|
|
b12d11728d | ||
|
|
8f0e3a7576 | ||
|
|
4810ff2dbe | ||
|
|
a221b22be9 | ||
|
|
24b458530d | ||
|
|
caea43590a | ||
|
|
c3d7a0b85e | ||
|
|
5c6f28027d | ||
|
|
dc04529f5b | ||
|
|
2bb04a419d | ||
|
|
8e31b71668 | ||
|
|
f055453889 | ||
|
|
820a6cecdd | ||
|
|
c3a779e4c8 | ||
|
|
00008abf03 | ||
|
|
d9c6f2f780 | ||
|
|
9a4d77e869 | ||
|
|
2407416e33 | ||
|
|
234e42ccfa | ||
|
|
4426b8dc30 | ||
|
|
9541f49f39 | ||
|
|
9f7a99f370 | ||
|
|
12f0bb2f98 | ||
|
|
ac45a72e0e | ||
|
|
9705818b15 | ||
|
|
39374eafda | ||
|
|
110bc3205e | ||
|
|
57fec89f2d | ||
|
|
72e794c281 | ||
|
|
fadbcc3e44 | ||
|
|
9e30e964e6 | ||
|
|
fbb707d014 | ||
|
|
d2d82ee5ee | ||
|
|
9061fa08ba | ||
|
|
f29236518e | ||
|
|
6537de5224 | ||
|
|
0413d470a8 | ||
|
|
c01f4d3ce8 | ||
|
|
dd9d359bb0 | ||
|
|
b444c35643 | ||
|
|
abd0d52fad | ||
|
|
c8771dd3b9 | ||
|
|
6d16f56183 | ||
|
|
95ffe0350e | ||
|
|
e314f25a77 | ||
|
|
65ea65e9ae | ||
|
|
4ce975562e | ||
|
|
07e625f2bc | ||
|
|
d95fd8dc90 | ||
|
|
55028cf3a3 | ||
|
|
16a97772d3 | ||
|
|
d33167d070 | ||
|
|
f62e0ba372 | ||
|
|
1b6466ae78 | ||
|
|
29c5987bf1 | ||
|
|
fcd1d01e83 | ||
|
|
a80b889760 | ||
|
|
f71bf1b074 | ||
|
|
7e048ee537 | ||
|
|
34098e8950 | ||
|
|
faf0d4d605 | ||
|
|
85ade88d55 | ||
|
|
7d0b94330d | ||
|
|
5cff1298fc | ||
|
|
d3a3d4e96e | ||
|
|
f99621ec92 | ||
|
|
e47a9304f4 | ||
|
|
9488fc1251 | ||
|
|
b590aa746b | ||
|
|
ef61134b8b | ||
|
|
a9daa0b586 | ||
|
|
ae0ad25ed6 | ||
|
|
a4c48c1981 | ||
|
|
3497590d2e | ||
|
|
11cf1329c8 | ||
|
|
ba5b41cf3a | ||
|
|
1d70446cc4 | ||
|
|
f15d0ab243 | ||
|
|
34be4800d7 | ||
|
|
e40b0c2e07 | ||
|
|
45e0ad9e93 | ||
|
|
96e6ddfe8a | ||
|
|
6b47bacfac | ||
|
|
099b419e34 | ||
|
|
e343ec0a1e | ||
|
|
e3d6d08916 | ||
|
|
d0bc206c83 | ||
|
|
7287f8c81b | ||
|
|
1878753cde | ||
|
|
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 | ||
|
|
96b50538ed |
@@ -0,0 +1,143 @@
|
|||||||
|
---
|
||||||
|
name: create-crud-page
|
||||||
|
description: Scaffold a CRUD list/table page module in the gpustack-ui monorepo. Use when creating a new page module, building a list/table page, adding a create/edit drawer, or setting up the components/config/forms/hooks/services structure for a feature.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Create a CRUD Table Page
|
||||||
|
|
||||||
|
## Inputs (do this first)
|
||||||
|
|
||||||
|
- The argument passed to this skill is the **module name** (e.g. `/create-crud-page api-keys` → module `api-keys`). If no name was given, ask for it.
|
||||||
|
- **Always ask the user for the API documentation before generating any code**, even if a module name was provided:
|
||||||
|
|
||||||
|
> Where is the API documentation for this module? (OpenAPI/Swagger URL, schema file path, or an interface description)
|
||||||
|
|
||||||
|
- Wait for the answer, then read/fetch it. Derive `config/types.ts` (`FormData`, `ListItem`), the `services` request hooks, and form fields from that schema. Do not guess field names or endpoints — if the doc is missing details, ask.
|
||||||
|
|
||||||
|
- **Also ask which form layout to scaffold:**
|
||||||
|
|
||||||
|
> Should the form use tabs? (1) a plain form without tabs, or (2) a tabbed form
|
||||||
|
|
||||||
|
Choose the form structure in section 3 accordingly. Default to **no tabs** unless the user picks tabs or the schema clearly has many grouped sections.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Before anything: **reuse common `components`, `hooks`, and `utils` from `@gpustack/core-ui` whenever possible.**
|
||||||
|
|
||||||
|
Reference implementation for sections below: `src/pages/model-routes`.
|
||||||
|
|
||||||
|
## Module structure
|
||||||
|
|
||||||
|
Create the module under `src/pages/{module}`:
|
||||||
|
|
||||||
|
```text
|
||||||
|
{module}
|
||||||
|
├── components
|
||||||
|
├── config
|
||||||
|
├── forms
|
||||||
|
├── hooks
|
||||||
|
├── index.tsx
|
||||||
|
└── services
|
||||||
|
```
|
||||||
|
|
||||||
|
## 1. components
|
||||||
|
|
||||||
|
Module-specific components.
|
||||||
|
|
||||||
|
- The create/edit form component is named `add-xxx-modal.tsx` (repo convention — keep the `-modal` suffix even though it is built with `FormDrawer`).
|
||||||
|
- Use `FormDrawer` from `@gpustack/core-ui`.
|
||||||
|
- If a table cell's render logic/structure is complex, extract it into `xxx-cell.tsx`.
|
||||||
|
|
||||||
|
## 2. config
|
||||||
|
|
||||||
|
```text
|
||||||
|
config
|
||||||
|
├── index.ts # static configs & constants
|
||||||
|
└── types.ts # TypeScript types
|
||||||
|
```
|
||||||
|
|
||||||
|
Naming: form types → `FormData`; table list item types → `ListItem`.
|
||||||
|
|
||||||
|
## 3. forms
|
||||||
|
|
||||||
|
Main form component goes in `forms/index.tsx`.
|
||||||
|
|
||||||
|
- **Complex interactions** (Form.Item split across components): create a dedicated Form Context and wrap with `FormContext.Provider`.
|
||||||
|
- **Tab-based forms**: use `ScrollSpyTabs` from `@gpustack/core-ui`, wrapping the `Form` or `FormContext.Provider`. Do not use tabs unless necessary.
|
||||||
|
- **Required-field validation**: use `getRuleMessage` for standard `input`/`select`.
|
||||||
|
- For cascading selectors and async race protection, follow the **form-patterns** skill.
|
||||||
|
|
||||||
|
## 4. hooks
|
||||||
|
|
||||||
|
- Table columns → `use-xxx-columns.tsx`.
|
||||||
|
- Open/close hooks for `add-xxx-modal.tsx` → `use-create-xxx.ts`.
|
||||||
|
|
||||||
|
## 5. index.tsx (list page entry)
|
||||||
|
|
||||||
|
- **Data fetching**: `useTableFetch` from `@gpustack/core-ui`.
|
||||||
|
- **Data display**:
|
||||||
|
- Standard table → Ant Design `Table`. Ref: `src/pages/users/index.tsx`.
|
||||||
|
- Expandable/collapsible rows → `Table` from `@gpustack/core-ui`. Ref: `src/pages/model-routes/index.tsx`.
|
||||||
|
- Card-style lists → use `InfiniteScrollerProvider`. Ref: `src/pages/backends/index.tsx`.
|
||||||
|
|
||||||
|
## 6. services
|
||||||
|
|
||||||
|
`request` is injected via a provider — do **not** create a centralized `apis` directory like in `gpustack-ui`. Define request hooks directly in `services`.
|
||||||
|
|
||||||
|
- Use `useRequest` from `@gpustack/core-ui`, or `useQueryData` (same underlying method).
|
||||||
|
- Ref: `src/pages/gpu-service/storage-types/services/use-create-storage-type.ts`.
|
||||||
|
|
||||||
|
## 7. Empty data
|
||||||
|
|
||||||
|
- Page table lists → `NoResult`.
|
||||||
|
- Simple (non-page) tables → `Empty` with `image={Empty.PRESENTED_IMAGE_SIMPLE}`.
|
||||||
|
|
||||||
|
## Common UI conventions
|
||||||
|
|
||||||
|
- **Drawer/Modal open/close**: use `useBodyScroll` from `@gpustack/core-ui`. Ref: `src/pages/model-routes/hooks/use-create-route.ts`.
|
||||||
|
- **Status display** (success/failed/processing/warning): use `StatusTag`, never `Tag` from `antd` directly. See **Status display** below. Ref: `src/pages/llmodels/components/table-list.tsx`.
|
||||||
|
- **Permission-gated visibility**: use `Access` / `useAccess`. Ref: `src/pages/access/index.tsx`.
|
||||||
|
- **Styles**: avoid `styled-components` for complex/large styling. Prefer `createStyles` for component-scoped dynamic styles, CSS Modules (`xxx.module.less`) for static structured styles.
|
||||||
|
|
||||||
|
## Status display
|
||||||
|
|
||||||
|
Use `StatusTag` from `@gpustack/core-ui` for status display. Do not use `Tag` from `antd` directly.
|
||||||
|
|
||||||
|
1. Define the mapping from business status values to UI status values in the module's `config/index.ts`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { StatusMaps } from '@/config';
|
||||||
|
import { StatusType } from '@/config/types';
|
||||||
|
|
||||||
|
export const XxxStatusValueMap = {
|
||||||
|
Running: 'running',
|
||||||
|
Pending: 'pending',
|
||||||
|
Failed: 'failed'
|
||||||
|
};
|
||||||
|
|
||||||
|
export const XxxStatusLabelMap: Record<string, string> = {
|
||||||
|
[XxxStatusValueMap.Running]: 'Running',
|
||||||
|
[XxxStatusValueMap.Pending]: 'Pending',
|
||||||
|
[XxxStatusValueMap.Failed]: 'Failed'
|
||||||
|
};
|
||||||
|
|
||||||
|
export const status: Record<string, StatusType> = {
|
||||||
|
[XxxStatusValueMap.Running]: StatusMaps.success,
|
||||||
|
[XxxStatusValueMap.Pending]: StatusMaps.transitioning,
|
||||||
|
[XxxStatusValueMap.Failed]: StatusMaps.error
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
2. In table columns, pass only the UI status value and display text required by `StatusTag`:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<StatusTag
|
||||||
|
statusValue={{
|
||||||
|
status: status[value],
|
||||||
|
text: XxxStatusLabelMap[value] || value,
|
||||||
|
message: record.state_message
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
`statusValue.status` must be a value mapped from `StatusMaps` (`success`, `transitioning`, `warning`, `error`, `inactive`). Do not pass business status values such as `running` or `pending` directly.
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
---
|
||||||
|
name: form-patterns
|
||||||
|
description: Patterns for forms with cascading/dependent selections in the gpustack-ui monorepo. Use when building a form where picking one field derives another (pick A → auto-pick B → write form), handling async option loading on modal open, or protecting against stale async results.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Form Patterns
|
||||||
|
|
||||||
|
Theme: keep cascading selections (pick A → derive B → write form) on a single, predictable path.
|
||||||
|
|
||||||
|
## Accessing the form: `form` vs `form.current`
|
||||||
|
|
||||||
|
This is **not** absolute — it depends on the call site:
|
||||||
|
|
||||||
|
- **Inside the form component** (`forms/index.tsx`), or anywhere holding a `Form.useForm()` instance → call it directly: `form.setFieldsValue(...)`.
|
||||||
|
- **In the outer Drawer/Modal wrapper** that opens the form and holds it via `ref={form}` (`const form = useRef(null)`), driven by an `open` prop → go through the ref: `form.current?.setFieldsValue(...)`.
|
||||||
|
|
||||||
|
The reference template below is written for the **Drawer-wrapper scenario** (it reacts to `open` and owns the shared `selection` state), so it uses `form.current?` throughout. If you lift this logic into the form body with a `useForm()` instance, drop the `.current`.
|
||||||
|
|
||||||
|
## 1. No fallback for derived selection
|
||||||
|
|
||||||
|
When "pick A then auto-pick B", match by rule and return `undefined` if no match — let the form field stay empty. Do **not** silently fall back to `list[0]`; a fallback hides data issues and fakes a valid selection.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const findB = (key, list) =>
|
||||||
|
key ? list.find((x) => x.key === key) : undefined;
|
||||||
|
```
|
||||||
|
|
||||||
|
For form fields, clear with `undefined`, not `''`. In 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 rotated (modal closed and re-opened) before 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
|
||||||
|
|
||||||
|
Two cascading selectors backed by a single shared state, with a single atomic write (state + form together):
|
||||||
|
|
||||||
|
```ts
|
||||||
|
type Selection = { a?: string; b?: number };
|
||||||
|
|
||||||
|
const [selection, setSelection] = useState<Selection>({});
|
||||||
|
const sessionRef = useRef(0);
|
||||||
|
const form = useRef<any>(null); // wrapper holds the form via <Form ref={form} /> — see "Accessing the form" above
|
||||||
|
|
||||||
|
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 });
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Related
|
||||||
|
|
||||||
|
- Module/file structure for forms lives in the **create-crud-page** skill (section 3).
|
||||||
|
- Required-field validation: use `getRuleMessage`.
|
||||||
@@ -13,5 +13,6 @@
|
|||||||
.swc
|
.swc
|
||||||
.DS_Store
|
.DS_Store
|
||||||
.idea
|
.idea
|
||||||
.claude
|
.claude/settings.local.json
|
||||||
/dist.zip
|
/dist.zip
|
||||||
|
.cache
|
||||||
@@ -3,7 +3,6 @@ node_modules
|
|||||||
.umi-production
|
.umi-production
|
||||||
public/static/*.js
|
public/static/*.js
|
||||||
public/static/*.css
|
public/static/*.css
|
||||||
src/components/icon-font/iconfont/iconfont.js
|
src/components/iconfont/
|
||||||
src/components/icon-font/iconfont/*.css
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -3,5 +3,5 @@ module.exports = {
|
|||||||
rules: {
|
rules: {
|
||||||
'selector-class-pattern': null
|
'selector-class-pattern': null
|
||||||
},
|
},
|
||||||
ignoreFiles: ['public/static/*.css']
|
ignoreFiles: ['public/static/*.css', 'src/components/iconfont/iconfont.css']
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
# Agent Instructions
|
||||||
|
|
||||||
|
This project keeps a single source of truth for agent/contributor conventions in [`CLAUDE.md`](./CLAUDE.md). **Read [`CLAUDE.md`](./CLAUDE.md) and follow it.**
|
||||||
|
|
||||||
|
@CLAUDE.md
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
# Repo
|
||||||
|
|
||||||
|
This is the **open source UI** (`gpustack-ui`). Common `components`, `hooks`, and `utils` are published as `@gpustack/core-ui` and consumed throughout `src`.
|
||||||
|
|
||||||
|
**Always prioritize reusing common `components`, `hooks`, and `utils` from `@gpustack/core-ui`.**
|
||||||
|
|
||||||
|
Task-specific conventions live in skills: use **create-crud-page** when building a page module, **form-patterns** when building cascading/dependent forms.
|
||||||
|
|
||||||
|
# React State and Request Patterns
|
||||||
|
|
||||||
|
Keep data flow explicit, predictable, and performant. The triggering **action** is the source of truth for UI updates — not effect-driven synchronization.
|
||||||
|
|
||||||
|
## 1. Avoid effect-driven requests
|
||||||
|
|
||||||
|
Do not use request functions as `useEffect` dependencies. Trigger requests explicitly from user actions or lifecycle entry points.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// Avoid
|
||||||
|
useEffect(() => {
|
||||||
|
fetchData();
|
||||||
|
}, [fetchData]);
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. Form requests should be action-driven
|
||||||
|
|
||||||
|
- Fetch form data (e.g. `Select` options) when the form first opens.
|
||||||
|
- If later requests depend on interactions, trigger them inside the interaction handler.
|
||||||
|
- Do not rely on `useEffect` dependency changes.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// Recommended
|
||||||
|
const handleOnChange = (value) => {
|
||||||
|
fetchData(value);
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. Update related states together
|
||||||
|
|
||||||
|
When one action updates multiple related states, update them all directly in the handler. Do not sync via `useEffect` or derive indirectly via `useMemo`.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const handleOnChange = (value) => {
|
||||||
|
setState1(...);
|
||||||
|
setState2(...);
|
||||||
|
buildState(...);
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. Group strongly related state
|
||||||
|
|
||||||
|
If multiple states always update together, use a single state object instead of multiple `useState` calls — fewer rerenders, more predictable transitions.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const [state, setState] = useState({ state1: ..., state2: ..., state3: ... });
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. Prefer explicit state flow
|
||||||
|
|
||||||
|
Keep request execution, state updates, and derived calculations close to the triggering action. Avoid chaining business logic through multiple `useEffect` hooks.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// Prefer
|
||||||
|
const handleAction = () => {
|
||||||
|
fetchData();
|
||||||
|
setTableData(...);
|
||||||
|
setSelectedRow(...);
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## 6. Avoid premature memoization
|
||||||
|
|
||||||
|
Do not use `useMemo` / `useCallback` unless there is a confirmed bottleneck. Overuse adds complexity, obscures state flow, and risks stale dependencies. Optimize only when necessary.
|
||||||
|
|
||||||
|
## 7. Keep request logic predictable
|
||||||
|
|
||||||
|
A user interaction should clearly show: what request fires, which states update, how the UI changes. Avoid indirect update chains from dependency-driven effects.
|
||||||
|
|
||||||
|
## 8. Prefer action-driven architecture
|
||||||
|
|
||||||
|
Prefer action-driven updates, explicit handlers, and localized state transitions over effect-driven synchronization, cross-hook implicit updates, and reactive chains between states.
|
||||||
|
|
||||||
|
# Styles
|
||||||
|
|
||||||
|
**Future direction (apply to all new code):** avoid `styled-components`. Prefer:
|
||||||
|
|
||||||
|
1. `createStyles` for component-scoped dynamic styles
|
||||||
|
2. CSS Modules (`xxx.module.less`) for structured static styles
|
||||||
|
|
||||||
|
Existing `styled-components` usage is legacy tech debt — do not migrate it wholesale, but do not add new `styled-components` either. Theme tokens (`var(--ant-color-*)`) work in all three approaches.
|
||||||
|
|
||||||
|
# Naming conventions
|
||||||
|
|
||||||
|
A page module lives under `src/pages/{module}` with this sub-structure: `components/`, `config/`, `forms/`, `hooks/`, `services/`, `index.tsx`. File naming:
|
||||||
|
|
||||||
|
- **Create/edit modal**: `add-{feature}-modal.tsx` (keep the `-modal` suffix even when built with `FormDrawer`).
|
||||||
|
- **Table columns hook**: `use-{feature}-columns.tsx`.
|
||||||
|
- **Open/close & request hooks**: `use-{verb}-{noun}.ts` (e.g. `use-create-user.ts`, `use-query-user-list.ts`).
|
||||||
|
- **Complex table cell**: extract into `{feature}-cell.tsx`.
|
||||||
|
|
||||||
|
# Config & types
|
||||||
|
|
||||||
|
- `config/types.ts` — TypeScript types. Form shape → `FormData`; table/list row → `ListItem`.
|
||||||
|
- `config/index.ts` — static constants, enums, and value/label maps (e.g. `XxxStatusValueMap`, `XxxStatusLabelMap`). Keep constants out of `types.ts`.
|
||||||
|
|
||||||
|
# Common components
|
||||||
|
|
||||||
|
Always check `@gpustack/core-ui` first. Frequently reused:
|
||||||
|
|
||||||
|
- **Drawer/Modal open/close**: `useBodyScroll`.
|
||||||
|
- **Form drawer / footer**: `FormDrawer`, `ModalFooter`.
|
||||||
|
- **Delete confirmation**: `DeleteModal`.
|
||||||
|
- **Search + bulk actions bar**: `FilterBar`.
|
||||||
|
- **Form fields**: `BaseSelect`, `Input` (labeled).
|
||||||
|
- **Text overflow**: `AutoTooltip`.
|
||||||
|
- **Icons**: `IconFont`.
|
||||||
|
- **Status display** (success/failed/processing/warning): `StatusTag`.
|
||||||
|
- **Permission-gated visibility**: `Access` / `useAccess`.
|
||||||
|
- **Request hooks**: `useRequest` / `useQueryData` / `useQueryDataList`.
|
||||||
|
- **Table data fetching**: `useTableFetch`.
|
||||||
|
- **Submit guard** (prevent double-submit): `useSubmitLock`.
|
||||||
|
- **Tabbed forms**: `ScrollSpyTabs`.
|
||||||
|
|
||||||
|
# Dynamic add-item form fields
|
||||||
|
|
||||||
|
When building a form, select the add-item component from the **shape of the field's data** (its schema). Match the schema, don't hand-roll a list UI:
|
||||||
|
|
||||||
|
- **Plain object** (key→value map) → `LabelSelector`.
|
||||||
|
- **String array** → `ListInput`. Ref `src/pages/llmodels/forms/backend-parameters-list.tsx`.
|
||||||
|
- **Object array** → `MetadataList` with a custom item renderer per entry. Ref `src/pages/llmodels/forms/model-lora-list.tsx`.
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
## Create form table list
|
|
||||||
|
|
||||||
## Create a form
|
|
||||||
|
|
||||||
## StatusTag
|
|
||||||
|
|
||||||
Use `StatusTag` from `@gpustack/core-ui` for status display. Do not use `Tag` from `antd` directly.
|
|
||||||
|
|
||||||
1. Define the mapping from business status values to UI status values in the module's `config/index.ts`:
|
|
||||||
|
|
||||||
```ts
|
|
||||||
import { StatusMaps } from '@/config';
|
|
||||||
import { StatusType } from '@/config/types';
|
|
||||||
|
|
||||||
export const XxxStatusValueMap = {
|
|
||||||
Running: 'running',
|
|
||||||
Pending: 'pending',
|
|
||||||
Failed: 'failed'
|
|
||||||
};
|
|
||||||
|
|
||||||
export const XxxStatusLabelMap: Record<string, string> = {
|
|
||||||
[XxxStatusValueMap.Running]: 'Running',
|
|
||||||
[XxxStatusValueMap.Pending]: 'Pending',
|
|
||||||
[XxxStatusValueMap.Failed]: 'Failed'
|
|
||||||
};
|
|
||||||
|
|
||||||
export const status: Record<string, StatusType> = {
|
|
||||||
[XxxStatusValueMap.Running]: StatusMaps.success,
|
|
||||||
[XxxStatusValueMap.Pending]: StatusMaps.transitioning,
|
|
||||||
[XxxStatusValueMap.Failed]: StatusMaps.error
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
2. In table columns, pass only the UI status value and display text required by `StatusTag`:
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
<StatusTag
|
|
||||||
statusValue={{
|
|
||||||
status: status[value],
|
|
||||||
text: XxxStatusLabelMap[value] || value,
|
|
||||||
message: record.state_message
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
```
|
|
||||||
|
|
||||||
`statusValue.status` must be a value mapped from `StatusMaps`, such as `success`, `transitioning`, `warning`, `error`, or `inactive`. Do not pass business status values such as `running` or `pending` directly.
|
|
||||||
@@ -31,5 +31,6 @@ export default function createProxyTable(target?: string) {
|
|||||||
},
|
},
|
||||||
{}
|
{}
|
||||||
);
|
);
|
||||||
|
|
||||||
return proxyTable;
|
return proxyTable;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -140,15 +140,6 @@ const baseRoutes = [
|
|||||||
access: 'canSeeOrgAdmin',
|
access: 'canSeeOrgAdmin',
|
||||||
component: './model-routes/index'
|
component: './model-routes/index'
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: 'usage',
|
|
||||||
path: '/models/usage',
|
|
||||||
key: 'usage',
|
|
||||||
icon: 'icon-usage-outlined',
|
|
||||||
selectedIcon: 'icon-usage-filled',
|
|
||||||
defaultIcon: 'icon-usage-outlined',
|
|
||||||
component: './usage/index'
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: 'providers',
|
name: 'providers',
|
||||||
path: '/models/providers',
|
path: '/models/providers',
|
||||||
@@ -179,6 +170,26 @@ const baseRoutes = [
|
|||||||
access: 'canSeeOrgAdmin',
|
access: 'canSeeOrgAdmin',
|
||||||
hideInMenu: true,
|
hideInMenu: true,
|
||||||
component: './benchmark/details'
|
component: './benchmark/details'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'backendsList',
|
||||||
|
path: '/models/backends',
|
||||||
|
key: 'backendsList',
|
||||||
|
icon: 'icon-backend',
|
||||||
|
selectedIcon: 'icon-backend-filled',
|
||||||
|
defaultIcon: 'icon-backend',
|
||||||
|
access: 'canSeeOrgAdmin',
|
||||||
|
component: './backends/index'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'modelfiles',
|
||||||
|
path: '/models/modelfiles',
|
||||||
|
key: 'modelfiles',
|
||||||
|
icon: 'icon-files',
|
||||||
|
selectedIcon: 'icon-files-filled',
|
||||||
|
defaultIcon: 'icon-files',
|
||||||
|
access: 'canSeeOrgAdmin',
|
||||||
|
component: './resources/components/model-files'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -186,6 +197,7 @@ const baseRoutes = [
|
|||||||
name: 'gpuService',
|
name: 'gpuService',
|
||||||
path: '/gpu-service',
|
path: '/gpu-service',
|
||||||
key: 'gpuService',
|
key: 'gpuService',
|
||||||
|
access: 'canSeeGpuService',
|
||||||
routes: [
|
routes: [
|
||||||
{
|
{
|
||||||
path: '/gpu-service',
|
path: '/gpu-service',
|
||||||
@@ -213,10 +225,25 @@ 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',
|
||||||
|
// Storage types are tenant-scoped on the backend (Org owners
|
||||||
|
// can create/list their own), so the menu shouldn't be
|
||||||
|
// platform-admin-only. ``canSeeOrgAdmin`` keeps the gate at
|
||||||
|
// "admin or current-org owner" — Org members still don't see
|
||||||
|
// it, which matches the read/write model in the route.
|
||||||
|
access: 'canSeeOrgAdmin',
|
||||||
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',
|
||||||
@@ -239,6 +266,16 @@ const baseRoutes = [
|
|||||||
path: '/resources',
|
path: '/resources',
|
||||||
redirect: '/resources/workers'
|
redirect: '/resources/workers'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'clusters',
|
||||||
|
path: '/resources/clusters/list',
|
||||||
|
key: 'clusters',
|
||||||
|
icon: 'icon-cluster2-outline',
|
||||||
|
selectedIcon: 'icon-cluster2-filled',
|
||||||
|
defaultIcon: 'icon-cluster2-outline',
|
||||||
|
component: './cluster-management/clusters',
|
||||||
|
subMenu: ['/resources/clusters/detail', '/resources/clusters/create']
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'workers',
|
name: 'workers',
|
||||||
path: '/resources/workers',
|
path: '/resources/workers',
|
||||||
@@ -258,67 +295,63 @@ const baseRoutes = [
|
|||||||
component: './resources/components/gpus'
|
component: './resources/components/gpus'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'backendsList',
|
name: 'credentials',
|
||||||
path: '/resources/backends',
|
path: '/resources/credentials',
|
||||||
key: 'backendsList',
|
key: 'credentials',
|
||||||
icon: 'icon-backend',
|
icon: 'icon-credential-outline',
|
||||||
selectedIcon: 'icon-backend-filled',
|
selectedIcon: 'icon-credential-filled',
|
||||||
defaultIcon: 'icon-backend',
|
defaultIcon: 'icon-credential-outline',
|
||||||
access: 'canSeeOrgAdmin',
|
component: './cluster-management/credentials'
|
||||||
component: './backends/index'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'modelfiles',
|
|
||||||
path: '/resources/modelfiles',
|
|
||||||
key: 'modelfiles',
|
|
||||||
icon: 'icon-files',
|
|
||||||
selectedIcon: 'icon-files-filled',
|
|
||||||
defaultIcon: 'icon-files',
|
|
||||||
component: './resources/components/model-files'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'clusterManagement',
|
|
||||||
path: '/cluster-management',
|
|
||||||
key: 'clusterManagement',
|
|
||||||
access: 'canSeeOrgAdmin',
|
|
||||||
routes: [
|
|
||||||
{
|
|
||||||
path: '/cluster-management',
|
|
||||||
redirect: '/cluster-management/clusters/list'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'clusters',
|
|
||||||
path: '/cluster-management/clusters/list',
|
|
||||||
key: 'clusters',
|
|
||||||
icon: 'icon-cluster2-outline',
|
|
||||||
selectedIcon: 'icon-cluster2-filled',
|
|
||||||
defaultIcon: 'icon-cluster2-outline',
|
|
||||||
component: './cluster-management/clusters',
|
|
||||||
subMenu: [
|
|
||||||
'/cluster-management/clusters/detail',
|
|
||||||
'/cluster-management/clusters/create'
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'clusterDetail',
|
name: 'clusterDetail',
|
||||||
path: '/cluster-management/clusters/detail',
|
path: '/resources/clusters/detail',
|
||||||
key: 'clusterDetail',
|
key: 'clusterDetail',
|
||||||
icon: 'icon-cluster2-outline',
|
icon: 'icon-cluster2-outline',
|
||||||
selectedIcon: 'icon-cluster2-filled',
|
selectedIcon: 'icon-cluster2-filled',
|
||||||
defaultIcon: 'icon-cluster2-outline',
|
defaultIcon: 'icon-cluster2-outline',
|
||||||
hideInMenu: true,
|
hideInMenu: true,
|
||||||
component: './cluster-management/cluster-detail'
|
component: './cluster-management/cluster-detail'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Cross-resource consumption (tokens + GPU/CPU instances + storage).
|
||||||
|
// A folder so it matches the other top-level groups; more usage views can
|
||||||
|
// graduate in here later.
|
||||||
|
name: 'billingAndUsage',
|
||||||
|
path: '/usage',
|
||||||
|
key: 'usageGroup',
|
||||||
|
icon: 'icon-usage-outlined',
|
||||||
|
selectedIcon: 'icon-usage-filled',
|
||||||
|
defaultIcon: 'icon-usage-outlined',
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
path: '/usage',
|
||||||
|
redirect: '/usage/overview'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'credentials',
|
name: 'usage',
|
||||||
path: '/cluster-management/credentials',
|
path: '/usage/overview',
|
||||||
key: 'credentials',
|
key: 'usage',
|
||||||
icon: 'icon-credential-outline',
|
icon: 'icon-usage-outlined',
|
||||||
selectedIcon: 'icon-credential-filled',
|
selectedIcon: 'icon-usage-filled',
|
||||||
defaultIcon: 'icon-credential-outline',
|
defaultIcon: 'icon-usage-outlined',
|
||||||
component: './cluster-management/credentials'
|
component: './usage/index'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'billing',
|
||||||
|
path: '/usage/billing',
|
||||||
|
key: 'billing',
|
||||||
|
icon: 'icon-billing-outlined',
|
||||||
|
selectedIcon: 'icon-billing-filled',
|
||||||
|
defaultIcon: 'icon-billing-outlined',
|
||||||
|
hideInMenu: process.env.ENABLE_ENTERPRISE === 'true',
|
||||||
|
// OSS exposes the menu as a teaser for the enterprise billing
|
||||||
|
// module. The page itself just renders an upsell notice — the real
|
||||||
|
// billing UI lives in the enterprise plugin and shadows this route
|
||||||
|
// via `routes.extensions.ts`.
|
||||||
|
component: './billing'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -331,6 +364,20 @@ const baseRoutes = [
|
|||||||
path: '/access-control',
|
path: '/access-control',
|
||||||
redirect: '/access-control/users'
|
redirect: '/access-control/users'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'organizations',
|
||||||
|
path: '/access-control/organizations',
|
||||||
|
key: 'organizations',
|
||||||
|
icon: 'icon-org-outlined',
|
||||||
|
selectedIcon: 'icon-org-filled',
|
||||||
|
defaultIcon: 'icon-org-outlined',
|
||||||
|
// OSS exposes the menu to platform admins as a teaser for the
|
||||||
|
// enterprise multi-tenancy module. The page itself just renders
|
||||||
|
// an upsell notice — the real CRUD UI lives in the enterprise
|
||||||
|
// plugin and shadows this route via `routes.extensions.ts`.
|
||||||
|
access: 'canSeeAdmin',
|
||||||
|
component: './organizations'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'users',
|
name: 'users',
|
||||||
path: '/access-control/users',
|
path: '/access-control/users',
|
||||||
@@ -364,8 +411,8 @@ const baseRoutes = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'profile',
|
name: 'profile',
|
||||||
path: '/profile',
|
path: '/preferences',
|
||||||
key: 'profile',
|
key: 'preferences',
|
||||||
hideInMenu: true,
|
hideInMenu: true,
|
||||||
component: './profile',
|
component: './profile',
|
||||||
icon: 'User'
|
icon: 'User'
|
||||||
|
|||||||
@@ -1,13 +1,27 @@
|
|||||||
const child_process = require('child_process');
|
import { execSync } from 'child_process';
|
||||||
|
|
||||||
export const getBranchInfo = () => {
|
export const getBranchInfo = () => {
|
||||||
const latestCommit = child_process
|
// git may be absent (source archive, bare container) or this tree may
|
||||||
.execSync('git rev-parse HEAD')
|
// not be a git checkout. Swallow the failure and fall back to the env
|
||||||
.toString()
|
// overrides below — losing build info shouldn't fail the build.
|
||||||
.trim();
|
let latestCommit = '';
|
||||||
const versionTag = child_process
|
let versionTag = '';
|
||||||
.execSync(`git tag --contains ${latestCommit}`)
|
try {
|
||||||
.toString()
|
latestCommit = execSync('git rev-parse HEAD').toString().trim();
|
||||||
.trim();
|
versionTag = execSync(`git tag --contains ${latestCommit}`)
|
||||||
return { version: versionTag || '', commitId: latestCommit.slice(0, 7) };
|
.toString()
|
||||||
|
.trim();
|
||||||
|
} catch {
|
||||||
|
// Not a git checkout / git unavailable; rely on env overrides.
|
||||||
|
}
|
||||||
|
// Respect explicit GPUSTACK_UI_* overrides so a wrapping build that
|
||||||
|
// checks this source tree out as a sub-package can stamp its own
|
||||||
|
// release tag and commit id onto the UI (otherwise the panel reports
|
||||||
|
// the host tree's git HEAD, which the wrapper doesn't control).
|
||||||
|
const overrideVersion = process.env.GPUSTACK_UI_VERSION?.trim();
|
||||||
|
const overrideCommitId = process.env.GPUSTACK_UI_COMMIT_ID?.trim();
|
||||||
|
return {
|
||||||
|
version: overrideVersion || versionTag || '',
|
||||||
|
commitId: overrideCommitId || latestCommit.slice(0, 7)
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -14,7 +14,8 @@ export default defineConfig([
|
|||||||
'dist',
|
'dist',
|
||||||
'src/.umi/',
|
'src/.umi/',
|
||||||
'src/.umi-production/',
|
'src/.umi-production/',
|
||||||
'src/.umi-test/'
|
'src/.umi-test/',
|
||||||
|
'src/components/iconfont/'
|
||||||
]),
|
]),
|
||||||
{
|
{
|
||||||
files: ['**/*.{ts,tsx,js,jsx}'],
|
files: ['**/*.{ts,tsx,js,jsx}'],
|
||||||
|
|||||||
@@ -15,8 +15,9 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ant-design/icons": "^6.1.0",
|
"@ant-design/icons": "^6.1.0",
|
||||||
"@ant-design/pro-components": "3.1.0-0",
|
"@ant-design/pro-components": "3.1.0-0",
|
||||||
|
"@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.32",
|
||||||
"@huggingface/gguf": "^0.1.7",
|
"@huggingface/gguf": "^0.1.7",
|
||||||
"@huggingface/hub": "^0.15.1",
|
"@huggingface/hub": "^0.15.1",
|
||||||
"@huggingface/tasks": "^0.11.6",
|
"@huggingface/tasks": "^0.11.6",
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ export default (api: IApi) => {
|
|||||||
const info = JSON.parse(process.env.VERSION || '{}');
|
const info = JSON.parse(process.env.VERSION || '{}');
|
||||||
const env = process.env.NODE_ENV;
|
const env = process.env.NODE_ENV;
|
||||||
|
|
||||||
|
$('html').attr('lang', 'en');
|
||||||
|
|
||||||
$('html').attr('data-env', env);
|
$('html').attr('data-env', env);
|
||||||
|
|
||||||
$('html').attr(
|
$('html').attr(
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
import { applyAccessExtensions } from './access.extensions';
|
import { applyAccessExtensions } from './access.extensions';
|
||||||
|
|
||||||
export default (initialState: { currentUser?: Global.UserInfo }) => {
|
export default (initialState: {
|
||||||
|
currentUser?: Global.UserInfo;
|
||||||
|
hasKubernetesCluster?: boolean;
|
||||||
|
hasResourceEvents?: boolean;
|
||||||
|
}) => {
|
||||||
const isPlatformAdmin = !!(
|
const isPlatformAdmin = !!(
|
||||||
initialState &&
|
initialState &&
|
||||||
initialState.currentUser &&
|
initialState.currentUser &&
|
||||||
@@ -11,6 +15,16 @@ export default (initialState: { currentUser?: Global.UserInfo }) => {
|
|||||||
initialState.currentUser &&
|
initialState.currentUser &&
|
||||||
!initialState.currentUser.is_admin
|
!initialState.currentUser.is_admin
|
||||||
);
|
);
|
||||||
|
// GPU Service is Kubernetes-only. We only gate visibility down when
|
||||||
|
// the probe in `getInitialState` came back with a definitive answer;
|
||||||
|
// `undefined` (probe failed / not yet ready) collapses to the
|
||||||
|
// role-based default so a transient network blip can't lock anyone
|
||||||
|
// out of the menu.
|
||||||
|
const hasKubernetesCluster = initialState?.hasKubernetesCluster;
|
||||||
|
// Having run GPU/CPU instances or storage (any resource_events) also unlocks
|
||||||
|
// GPU Service / the full Usage page — a user who used it keeps seeing it even
|
||||||
|
// without a current cluster. MaaS-only users (no cluster, no events) don't.
|
||||||
|
const hasResourceEvents = !!initialState?.hasResourceEvents;
|
||||||
|
|
||||||
// Predicate roles, top-down by strictness:
|
// Predicate roles, top-down by strictness:
|
||||||
// * `canSeeAdmin` — strictly platform admin (`users.is_admin`).
|
// * `canSeeAdmin` — strictly platform admin (`users.is_admin`).
|
||||||
@@ -18,6 +32,11 @@ export default (initialState: { currentUser?: Global.UserInfo }) => {
|
|||||||
// * `canSeeOrgAdmin` — admin-style menus that work cross-org
|
// * `canSeeOrgAdmin` — admin-style menus that work cross-org
|
||||||
// (Dashboard, Resources, Models, Cluster Management). Defaults
|
// (Dashboard, Resources, Models, Cluster Management). Defaults
|
||||||
// to platform admin; extensions widen to include org admins.
|
// to platform admin; extensions widen to include org admins.
|
||||||
|
// * `canSeeGpuService` — GPU Service menu. Anyone allowed to
|
||||||
|
// manage clusters (admins, Org owners) sees it; non-admins fall
|
||||||
|
// through to "show only if a Kubernetes cluster is actually
|
||||||
|
// reachable" so Org members without scheduling access don't see
|
||||||
|
// a dead-end menu item.
|
||||||
// * `canManageCurrentOrg` — pages that only make sense inside a
|
// * `canManageCurrentOrg` — pages that only make sense inside a
|
||||||
// specific org context (member / group management). Defaults to
|
// specific org context (member / group management). Defaults to
|
||||||
// `false`; extensions widen when both an org is selected AND
|
// `false`; extensions widen when both an org is selected AND
|
||||||
@@ -27,6 +46,8 @@ export default (initialState: { currentUser?: Global.UserInfo }) => {
|
|||||||
return applyAccessExtensions({
|
return applyAccessExtensions({
|
||||||
canSeeAdmin: isPlatformAdmin,
|
canSeeAdmin: isPlatformAdmin,
|
||||||
canSeeOrgAdmin: isPlatformAdmin,
|
canSeeOrgAdmin: isPlatformAdmin,
|
||||||
|
canSeeGpuService:
|
||||||
|
isPlatformAdmin || hasKubernetesCluster !== false || hasResourceEvents,
|
||||||
canManageCurrentOrg: false,
|
canManageCurrentOrg: false,
|
||||||
canSeeUser,
|
canSeeUser,
|
||||||
canDelete: true,
|
canDelete: true,
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { userSettingsHelperAtom } from '@/atoms/settings';
|
import { userSettingsHelperAtom } from '@/atoms/settings';
|
||||||
import { GPUStackVersionAtom, UpdateCheckAtom } from '@/atoms/user';
|
import { GPUStackVersionAtom, UpdateCheckAtom, userAtom } from '@/atoms/user';
|
||||||
import { setAtomStorage } from '@/atoms/utils';
|
import { setAtomStorage } from '@/atoms/utils';
|
||||||
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/constants';
|
import { COLOR_PRIMARY } from '@/config/theme/constants';
|
||||||
|
import { getGPUStackPlugin } from '@/plugins';
|
||||||
import { enterprisePluginReady } from '@/plugins/enterprise-ready';
|
import { enterprisePluginReady } from '@/plugins/enterprise-ready';
|
||||||
import { GPUStackPluginManager } from '@/plugins/manager';
|
import { GPUStackPluginManager } from '@/plugins/manager';
|
||||||
import { requestConfig } from '@/request-config';
|
import { requestConfig } from '@/request-config';
|
||||||
@@ -13,6 +14,11 @@ import {
|
|||||||
} from '@/services/profile/apis';
|
} from '@/services/profile/apis';
|
||||||
import { fetchSystemConfig } from '@/services/system/query-system-config';
|
import { fetchSystemConfig } from '@/services/system/query-system-config';
|
||||||
import { isOnline } from '@/utils';
|
import { isOnline } from '@/utils';
|
||||||
|
import {
|
||||||
|
markInitialStateProbed,
|
||||||
|
probeAccessFlags
|
||||||
|
} from '@/utils/access-probes';
|
||||||
|
import { installTenantFetch } from '@/utils/install-fetch';
|
||||||
import {
|
import {
|
||||||
IS_FIRST_LOGIN,
|
IS_FIRST_LOGIN,
|
||||||
readState,
|
readState,
|
||||||
@@ -22,6 +28,8 @@ import '@gpustack/core-ui/style.css';
|
|||||||
import { RequestConfig, history, request as umiRequest } from '@umijs/max';
|
import { RequestConfig, history, request as umiRequest } from '@umijs/max';
|
||||||
import { message } from 'antd';
|
import { message } from 'antd';
|
||||||
|
|
||||||
|
installTenantFetch();
|
||||||
|
|
||||||
// only for the first login and access from http://localhost
|
// only for the first login and access from http://localhost
|
||||||
|
|
||||||
const checkDefaultPage = async (userInfo: any) => {
|
const checkDefaultPage = async (userInfo: any) => {
|
||||||
@@ -39,6 +47,8 @@ export async function getInitialState(): Promise<{
|
|||||||
fetchUserInfo: () => Promise<Global.UserInfo>;
|
fetchUserInfo: () => Promise<Global.UserInfo>;
|
||||||
currentUser?: Global.UserInfo;
|
currentUser?: Global.UserInfo;
|
||||||
pluginData?: Record<string, any>;
|
pluginData?: Record<string, any>;
|
||||||
|
hasKubernetesCluster?: boolean;
|
||||||
|
hasResourceEvents?: boolean;
|
||||||
}> {
|
}> {
|
||||||
const { location } = history;
|
const { location } = history;
|
||||||
|
|
||||||
@@ -83,6 +93,36 @@ export async function getInitialState(): Promise<{
|
|||||||
getUpdateCheck();
|
getUpdateCheck();
|
||||||
fetchSystemConfig();
|
fetchSystemConfig();
|
||||||
}
|
}
|
||||||
|
// Only commit a substantive user object. A truthy-but-empty
|
||||||
|
// `data` (e.g. server responded 200 with an empty body) would
|
||||||
|
// otherwise look like "logged in" to every `currentUser`
|
||||||
|
// reader and the access seam — break out instead and let the
|
||||||
|
// caller treat the request as failed.
|
||||||
|
if (data && typeof data === 'object' && Object.keys(data).length > 0) {
|
||||||
|
// Commit the identity to atom storage (and so to localStorage)
|
||||||
|
// before returning. The access function — memoized on
|
||||||
|
// `initialState` and run once per commit — reads identity from
|
||||||
|
// localStorage; without this preemptive write the predicate
|
||||||
|
// sees the prior session's identity on its first evaluation
|
||||||
|
// after login, and stays stale until the next identity change
|
||||||
|
// (which usually doesn't come without a manual refresh).
|
||||||
|
try {
|
||||||
|
setAtomStorage(userAtom, data);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('userAtom commit error:', err);
|
||||||
|
}
|
||||||
|
// Fire `onUserFetched` so plugins maintaining identity-scoped
|
||||||
|
// caches can seed them under the new identity before any
|
||||||
|
// caller commits this user to `initialState`. Errors here are
|
||||||
|
// swallowed and logged — fetchUserInfo must still return.
|
||||||
|
try {
|
||||||
|
await getGPUStackPlugin()?.login?.onUserFetched?.(data, {
|
||||||
|
request: umiRequest
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error('onUserFetched plugin hook error:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
return data;
|
return data;
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
const data = error?.response?.data;
|
const data = error?.response?.data;
|
||||||
@@ -122,12 +162,24 @@ export async function getInitialState(): Promise<{
|
|||||||
getAppVersionInfo();
|
getAppVersionInfo();
|
||||||
|
|
||||||
if (![DEFAULT_ENTER_PAGE.login].includes(location.pathname)) {
|
if (![DEFAULT_ENTER_PAGE.login].includes(location.pathname)) {
|
||||||
const userInfo = await fetchUserInfo();
|
const [userInfo, accessFlags] = await Promise.all([
|
||||||
|
fetchUserInfo(),
|
||||||
|
probeAccessFlags()
|
||||||
|
]);
|
||||||
|
// Record that the probes ran for an authenticated user this page load
|
||||||
|
// (the refresh path) so the layout doesn't re-probe. A failed
|
||||||
|
// fetch (empty user — e.g. unauthenticated deep link that bounces to
|
||||||
|
// login) is NOT marked: the user will log in via SPA afterwards and
|
||||||
|
// the layout becomes responsible for probing.
|
||||||
|
if (userInfo?.username) {
|
||||||
|
markInitialStateProbed();
|
||||||
|
}
|
||||||
checkDefaultPage(userInfo);
|
checkDefaultPage(userInfo);
|
||||||
return {
|
return {
|
||||||
fetchUserInfo,
|
fetchUserInfo,
|
||||||
currentUser: userInfo,
|
currentUser: userInfo,
|
||||||
pluginData
|
pluginData,
|
||||||
|
...accessFlags
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
|
|||||||
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 96 KiB |
|
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 |
|
After Width: | Height: | Size: 21 KiB |
@@ -0,0 +1 @@
|
|||||||
|
<svg height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>AlibabaCloud</title><path d="M14.752 4.64h5.274C22.242 4.64 24 6.475 24 8.691V15.8a3.947 3.947 0 01-3.974 3.975h-5.274l1.299-1.835 3.822-1.222c.688-.23 1.146-.918 1.146-1.605v-5.81c0-.687-.458-1.375-1.146-1.605L16.05 6.475l-1.3-1.835zM2.98 15.111c0 .688.46 1.376 1.147 1.606l3.822 1.146 1.3 1.835H3.974A3.947 3.947 0 010 15.723V8.69c0-2.216 1.758-4.05 3.975-4.05h5.273L7.95 6.474 4.127 7.697c-.688.23-1.146.918-1.146 1.606v5.808z" fill="#FF6A00"></path><path d="M16.051 11.213H8.025v1.835h8.026v-1.835z" fill="#FF6A00"></path></svg>
|
||||||
|
After Width: | Height: | Size: 656 B |
@@ -0,0 +1 @@
|
|||||||
|
<svg height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>BaiLian</title><path d="M6.336 8.919v6.162l5.335-3.083L6.337 8.92z" fill="#1C54E3"></path><path d="M21.394 5.288s-.006-.006-.01-.006L17.01 2.754 6.336 8.92l5.335 3.082 9.701-5.6.016-.01a.635.635 0 00.006-1.1v-.003z" fill="#AA9AFF"></path><path d="M21.71 12.465a.62.62 0 00-.316.085s-.006 0-.009.003l-4.375 2.528 5.05 2.915h.006a2.06 2.06 0 00.28-1.04v-3.855a.637.637 0 00-.636-.636z" fill="#00EAD1"></path><path d="M22.06 17.996l-5.05-2.915L6.34 21.242l4.27 2.465s.016.006.022.012a2.102 2.102 0 002.093 0c.006-.003.016-.006.022-.012l8.538-4.93c.003 0 .006-.003.01-.006.321-.183.589-.45.775-.772h-.006l-.004-.003z" fill="#00CEC9"></path><path d="M11.672 11.998l-5.336 3.083-1.444.832-3.605 2.083H1.28c.173.303.416.555.709.738l.078.044.016.01.02.012 4.232 2.442 10.671-6.161-5.335-3.082z" fill="#00EAD1"></path><path d="M12.74.29c-.1-.06-.208-.107-.315-.148-.02-.006-.038-.016-.057-.022a2.121 2.121 0 00-.7-.12c-.233 0-.457.038-.668.11l-.031.01a2.196 2.196 0 00-.372.17L2.068 5.222s-.003 0-.006.003c-.324.183-.592.451-.781.773h.006l5.049 2.918L17.01 2.758 12.74.29z" fill="#7347FF"></path><path d="M1.287 6.001H1.28A2.06 2.06 0 001 7.041v9.915c0 .378.1.735.28 1.043h.007l5.049-2.918V8.919l-5.05-2.918z" fill="#0423DA"></path></svg>
|
||||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -3,6 +3,7 @@
|
|||||||
.ant-layout-sider-children {
|
.ant-layout-sider-children {
|
||||||
border-inline: none;
|
border-inline: none;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
|
padding-inline-end: 0;
|
||||||
padding-block-end: 8px;
|
padding-block-end: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -63,6 +63,13 @@ export const fromClusterCreationAtom = atom(false);
|
|||||||
export const clusterSessionAtom = atom<{
|
export const clusterSessionAtom = atom<{
|
||||||
firstAddWorker: boolean;
|
firstAddWorker: boolean;
|
||||||
firstAddCluster: boolean;
|
firstAddCluster: boolean;
|
||||||
|
presetClusterType?: 'model' | 'gpu';
|
||||||
|
// Provider to preselect when the create flow opens — set by the
|
||||||
|
// empty-state CTA on feature pages that need a specific provider
|
||||||
|
// (e.g. GPU Service can only schedule on Kubernetes, so its
|
||||||
|
// "Add Cluster" button skips provider catalog and lands on the
|
||||||
|
// K8s configure step). Consumed once by ClusterCreate on mount.
|
||||||
|
providerHint?: string;
|
||||||
} | null>(null);
|
} | null>(null);
|
||||||
|
|
||||||
export const clusterDetailAtom = atom<ClusterListItem | null>(null);
|
export const clusterDetailAtom = atom<ClusterListItem | null>(null);
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
import { ClusterListItem } from '@/pages/cluster-management/config/types';
|
|
||||||
import { atom } from 'jotai';
|
|
||||||
|
|
||||||
export const currentClusterAtom = atom<
|
|
||||||
(Partial<ClusterListItem> & { label?: string; value?: number }) | null
|
|
||||||
>(null);
|
|
||||||
@@ -55,3 +55,10 @@ export const userSettingsHelperAtom = atom(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
export const hideModalTemporarilyAtom = atom<boolean>(false);
|
export const hideModalTemporarilyAtom = atom<boolean>(false);
|
||||||
|
|
||||||
|
export const collapsedMenuGroupsAtom = atomWithStorage<string[]>(
|
||||||
|
'collapsedMenuGroups',
|
||||||
|
[],
|
||||||
|
undefined,
|
||||||
|
{ getOnInit: true }
|
||||||
|
);
|
||||||
|
|||||||
@@ -3,6 +3,15 @@ import { atomWithStorage } from 'jotai/utils';
|
|||||||
|
|
||||||
export const userAtom = atomWithStorage<any>('userInfo', null);
|
export const userAtom = atomWithStorage<any>('userInfo', null);
|
||||||
|
|
||||||
|
// Backs the `currentOrganizationId` localStorage key. Stays null in
|
||||||
|
// builds with no Org context (single-tenant), and is shared with any
|
||||||
|
// extension that persists the same key so both sides stay in sync
|
||||||
|
// without one side having to import from the other.
|
||||||
|
export const currentOrganizationIdAtom = atomWithStorage<number | null>(
|
||||||
|
'currentOrganizationId',
|
||||||
|
null
|
||||||
|
);
|
||||||
|
|
||||||
export const GPUStackVersionAtom = atom<{
|
export const GPUStackVersionAtom = atom<{
|
||||||
version: string;
|
version: string;
|
||||||
git_commit: string;
|
git_commit: string;
|
||||||
@@ -23,16 +32,15 @@ export const UpdateCheckAtom = atom<{
|
|||||||
latest_version: ''
|
latest_version: ''
|
||||||
});
|
});
|
||||||
|
|
||||||
export const initialPasswordAtom = atomWithStorage<string>(
|
export const initialPasswordAtom = atom<string>('');
|
||||||
'initialPassword',
|
|
||||||
''
|
|
||||||
);
|
|
||||||
|
|
||||||
// Namespace the server creates for an Org's resources on each Kubernetes
|
// Namespace the server creates for an Org's resources on each Kubernetes
|
||||||
// cluster. The format must match the backend's ``get_namespace_name``
|
// cluster. The format must match the backend's ``get_namespace_name``
|
||||||
// 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,27 +83,61 @@ 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;
|
||||||
|
|
||||||
const lookupOrgNamespace = (id: number | null): string | null => {
|
export interface CachedOrg {
|
||||||
|
id: number;
|
||||||
|
name?: string;
|
||||||
|
// The platform Org (single global tenant). Its models are NOT
|
||||||
|
// namespaced in ``/v1/models`` — they appear under their bare name.
|
||||||
|
is_platform?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve the cached Org record for an owner/principal id by scanning both
|
||||||
|
// org caches. ``organizationList`` (the caller's member orgs) is checked
|
||||||
|
// alongside the admin-only ``allOrganizations`` so member sessions resolve
|
||||||
|
// too. Id types vary between localStorage payloads (some writers stringify,
|
||||||
|
// others persist as a JSON number), so compare as strings — strict equality
|
||||||
|
// would silently miss those cases.
|
||||||
|
export const getOrgById = (
|
||||||
|
id: number | string | null | undefined
|
||||||
|
): CachedOrg | null => {
|
||||||
if (id == null) return null;
|
if (id == null) return null;
|
||||||
// Normalise both sides to strings — the stored id type varies between
|
|
||||||
// localStorage payloads (some writers stringify, others persist as a
|
|
||||||
// JSON number); strict equality would silently miss those cases.
|
|
||||||
const target = String(id);
|
const target = String(id);
|
||||||
for (const key of ORG_CACHE_KEYS) {
|
for (const key of ORG_CACHE_KEYS) {
|
||||||
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 CachedOrg[];
|
||||||
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) return match;
|
||||||
} catch {
|
} catch {
|
||||||
// ignore malformed cache; continue checking other keys
|
// ignore malformed cache; continue checking other keys
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Bare Org *name* (e.g. ``org1``) for an owner/principal id, or null.
|
||||||
|
export const getOrgNameById = (
|
||||||
|
id: number | string | null | undefined
|
||||||
|
): string | null => {
|
||||||
|
return getOrgById(id)?.name ?? null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const lookupOrgNamespace = (id: number | null): string | null => {
|
||||||
|
const name = getOrgNameById(id);
|
||||||
|
return name ? `gpustack-${name}` : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
// The Org the caller is currently acting under, or null in the admin-"All"
|
||||||
|
// context. The org-switcher reloads the page on switch, so the list pages
|
||||||
|
// always show this org's resources — which is how ``/v1/models`` namespaces
|
||||||
|
// their model ids (``{org}/{name}``). Callers reconstructing that id use this
|
||||||
|
// as the fallback owner when a row carries no explicit ``owner_principal_id``.
|
||||||
|
export const getCurrentOrg = (): CachedOrg | null => {
|
||||||
|
return getOrgById(getStoredCurrentOrgId());
|
||||||
|
};
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -92,6 +92,12 @@ declare namespace Global {
|
|||||||
interface InitialStateType {
|
interface InitialStateType {
|
||||||
fetchUserInfo: () => Promise<UserInfo>;
|
fetchUserInfo: () => Promise<UserInfo>;
|
||||||
currentUser?: UserInfo;
|
currentUser?: UserInfo;
|
||||||
|
// Captured at app boot so access predicates can gate GPU Service —
|
||||||
|
// the feature is Kubernetes-only, and Org members without a K8s
|
||||||
|
// cluster they can schedule on shouldn't see the menu. Refreshed
|
||||||
|
// by full page reload (e.g. OrgSwitcher) which re-runs
|
||||||
|
// getInitialState.
|
||||||
|
hasKubernetesCluster?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
type SearchParams = Pagination & { search?: string; [key: string]: any };
|
type SearchParams = Pagination & { search?: string; [key: string]: any };
|
||||||
|
|||||||
@@ -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])?$/;
|
||||||
|
|||||||
@@ -54,7 +54,8 @@ export default {
|
|||||||
itemHoverColor: 'rgba(0,0,0,1)',
|
itemHoverColor: 'rgba(0,0,0,1)',
|
||||||
itemColor: 'rgba(0,0,0,1)',
|
itemColor: 'rgba(0,0,0,1)',
|
||||||
itemHoverBg: 'rgb(24 25 27)',
|
itemHoverBg: 'rgb(24 25 27)',
|
||||||
itemActiveBg: 'rgb(24 25 27)'
|
itemActiveBg: 'rgb(24 25 27)',
|
||||||
|
menuItemSelectedBg: '#292929'
|
||||||
},
|
},
|
||||||
Progress: {
|
Progress: {
|
||||||
lineBorderRadius: 4
|
lineBorderRadius: 4
|
||||||
@@ -108,6 +109,7 @@ export default {
|
|||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
motion: true,
|
motion: true,
|
||||||
colorFill: '#0A0A0A',
|
colorFill: '#0A0A0A',
|
||||||
colorBgBase: '#0A0A0A'
|
colorBgBase: '#0A0A0A',
|
||||||
|
menuItemSelectedBg: '#292929'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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: {
|
||||||
@@ -55,7 +56,8 @@ export default {
|
|||||||
itemHoverColor: 'rgba(0,0,0,1)',
|
itemHoverColor: 'rgba(0,0,0,1)',
|
||||||
itemColor: 'rgba(0,0,0,1)',
|
itemColor: 'rgba(0,0,0,1)',
|
||||||
itemHoverBg: 'rgba(0,0,0,0.04)',
|
itemHoverBg: 'rgba(0,0,0,0.04)',
|
||||||
itemActiveBg: 'rgba(0,0,0,0.04)'
|
itemActiveBg: 'rgba(0,0,0,0.04)',
|
||||||
|
menuItemSelectedBg: '#e8eaed'
|
||||||
},
|
},
|
||||||
Progress: {
|
Progress: {
|
||||||
lineBorderRadius: 3
|
lineBorderRadius: 3
|
||||||
|
|||||||
@@ -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%);
|
||||||
@@ -170,8 +171,6 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.ant-table .ant-table-container table {
|
.ant-table .ant-table-container table {
|
||||||
// border-spacing: 0 20px;
|
|
||||||
|
|
||||||
.ant-table-thead th.ant-table-column-sort {
|
.ant-table-thead th.ant-table-column-sort {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
|
|
||||||
@@ -237,12 +236,11 @@ 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 {
|
||||||
padding-block: 2px 0;
|
padding-block: 4px 0;
|
||||||
padding-left: 6px;
|
padding-left: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ant-pro-sider-actions-list-item {
|
.ant-pro-sider-actions-list-item {
|
||||||
@@ -259,46 +257,26 @@ body {
|
|||||||
border-block-end: none;
|
border-block-end: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ant-pro-sider-logo-collapsed {
|
.ant-pro-sider-logo {
|
||||||
padding-left: 12px;
|
.collapse-btn {
|
||||||
cursor: e-resize;
|
color: var(--ant-color-text-tertiary);
|
||||||
|
:hover {
|
||||||
.collapse-wrap {
|
color: var(--ant-color-text);
|
||||||
display: none;
|
|
||||||
position: absolute;
|
|
||||||
top: 12px;
|
|
||||||
|
|
||||||
&::after {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
height: 48px;
|
|
||||||
width: 64px;
|
|
||||||
top: -16px;
|
|
||||||
left: -16px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
.collapse-wrap {
|
|
||||||
display: block;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.ant-pro-sider .ant-layout-sider-children {
|
.ant-pro-sider-logo-collapsed {
|
||||||
// border-right: 1px solid var(--ant-color-split);
|
padding-left: 6px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.ant-table-content table {
|
.ant-table .ant-table-tbody {
|
||||||
.ant-table-tbody {
|
.ant-table-row {
|
||||||
.ant-table-row {
|
border-radius: var(--table-td-radius);
|
||||||
border-radius: var(--table-td-radius);
|
|
||||||
|
|
||||||
> td {
|
> td {
|
||||||
background-color: unset;
|
border-bottom: 1px solid var(--ant-color-split);
|
||||||
border-bottom: 1px solid var(--ant-color-split);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -347,7 +325,6 @@ body {
|
|||||||
.ant-pro-layout-container {
|
.ant-pro-layout-container {
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
// background-color: var(--ant-color-bg-container);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.ant-pro-sider {
|
.ant-pro-sider {
|
||||||
@@ -661,10 +638,15 @@ body {
|
|||||||
inset: 53px auto auto 0 !important;
|
inset: 53px auto auto 0 !important;
|
||||||
border-radius: 0 0 var(--border-radius-base) var(--border-radius-base);
|
border-radius: 0 0 var(--border-radius-base) var(--border-radius-base);
|
||||||
border-top: none;
|
border-top: none;
|
||||||
max-height: 240px;
|
.ant-cascader-menus {
|
||||||
min-height: 100px;
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
&.gpu-selector {
|
&.gpu-selector {
|
||||||
|
.ant-cascader-menu {
|
||||||
|
max-height: 400px;
|
||||||
|
height: unset;
|
||||||
|
}
|
||||||
.ant-cascader-menu:last-child {
|
.ant-cascader-menu:last-child {
|
||||||
flex: auto;
|
flex: auto;
|
||||||
|
|
||||||
@@ -812,6 +794,8 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.ant-pro-sider-logo + div {
|
.ant-pro-sider-logo + div {
|
||||||
|
display: flex;
|
||||||
|
overflow: hidden;
|
||||||
&::-webkit-scrollbar {
|
&::-webkit-scrollbar {
|
||||||
width: 0;
|
width: 0;
|
||||||
}
|
}
|
||||||
@@ -867,3 +851,6 @@ body {
|
|||||||
padding-right: 8px;
|
padding-right: 8px;
|
||||||
padding-block: 8px;
|
padding-block: 8px;
|
||||||
}
|
}
|
||||||
|
.ant-select-multiple .ant-select-content {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,54 +1,132 @@
|
|||||||
import useMemoizedFn from 'ahooks/lib/useMemoizedFn';
|
import useMemoizedFn from 'ahooks/lib/useMemoizedFn';
|
||||||
import { converter, modeHsl, modeRgb, useMode } from 'culori/fn';
|
import { clampChroma, formatHex, modeOklch, modeRgb, useMode } from 'culori/fn';
|
||||||
|
import useUserSettings from './use-user-settings';
|
||||||
|
|
||||||
const toHsl = converter('hsl');
|
// Linear-style minimal/neutral palette: a TIGHT blue -> violet band in OKLCH
|
||||||
|
// (perceptually uniform), kept moderate-chroma so fills read as clean and gentle
|
||||||
const DEFAULT_BRAND_HUE = 211; // brand color
|
// rather than candy-colored. We deliberately stay narrow and DON'T fan out to
|
||||||
const COOL_HUE_START = 180;
|
// green/magenta — in this aesthetic series are separated by LIGHTNESS, not by
|
||||||
const COOL_HUE_END = 280;
|
// spreading across the hue wheel. Every series (including the first) is generated
|
||||||
|
// from this one ramp, so the whole palette stays in a single cohesive color
|
||||||
|
// family — no special high-saturation brand color that clashes with the rest.
|
||||||
|
const COOL_HUE_START = 250; // blue
|
||||||
|
const COOL_HUE_END = 315; // violet (stops short of magenta/pink, stays neutral-cool)
|
||||||
const COOL_HUE_RANGE = COOL_HUE_END - COOL_HUE_START;
|
const COOL_HUE_RANGE = COOL_HUE_END - COOL_HUE_START;
|
||||||
const GOLDEN_RATIO_CONJUGATE = 0.618033988749895;
|
|
||||||
|
|
||||||
function colorStringToHue(input?: string): number | undefined {
|
/**
|
||||||
if (!input) return undefined;
|
* Vivid, distinct cool accents — for places that need a handful of "primary"
|
||||||
const color = toHsl(input);
|
* colors, one per card/section (e.g. the summary trend cards), NOT a stacked
|
||||||
if (!color || typeof color.h !== 'number') return undefined;
|
* multi-series palette. Every color is anchor-quality (bright + saturated) and
|
||||||
return color.h;
|
* spread evenly across the blue→violet band, so the set reads as several equally
|
||||||
}
|
* strong primaries rather than one bold + several washed-out fills.
|
||||||
|
*/
|
||||||
export default function useCoolColors() {
|
export function useCoolAccents() {
|
||||||
// const brandHue = useMemo(
|
|
||||||
// () => colorStringToHue(userSettings.colorPrimary) ?? DEFAULT_BRAND_HUE,
|
|
||||||
// [userSettings.colorPrimary]
|
|
||||||
// );
|
|
||||||
useMode(modeRgb);
|
useMode(modeRgb);
|
||||||
useMode(modeHsl);
|
useMode(modeOklch);
|
||||||
|
|
||||||
const brandHue = DEFAULT_BRAND_HUE;
|
const { isDarkTheme } = useUserSettings();
|
||||||
|
|
||||||
return useMemoizedFn((count: number): string[] => {
|
return useMemoizedFn((count: number): string[] => {
|
||||||
if (count <= 0) return [];
|
if (count <= 0) return [];
|
||||||
|
|
||||||
const colors: string[] = [];
|
const l = isDarkTheme ? 0.62 : 0.64;
|
||||||
|
const c = isDarkTheme ? 0.16 : 0.2;
|
||||||
|
|
||||||
|
const out: string[] = [];
|
||||||
for (let i = 0; i < count; i++) {
|
for (let i = 0; i < count; i++) {
|
||||||
let hue: number;
|
const t = count <= 1 ? 0 : i / (count - 1);
|
||||||
|
const hue = COOL_HUE_START + t * COOL_HUE_RANGE;
|
||||||
|
out.push(
|
||||||
|
formatHex(clampChroma({ mode: 'oklch', l, c, h: hue }, 'oklch'))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (i === 0) {
|
export default function useCoolColors() {
|
||||||
hue = brandHue - 5;
|
useMode(modeRgb);
|
||||||
} else {
|
useMode(modeOklch);
|
||||||
const offset = (i * GOLDEN_RATIO_CONJUGATE) % 1;
|
|
||||||
hue = COOL_HUE_START + offset * COOL_HUE_RANGE;
|
|
||||||
|
|
||||||
if (Math.abs(hue - brandHue) < 5) {
|
const { isDarkTheme } = useUserSettings();
|
||||||
hue = (hue + 10) % COOL_HUE_END;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const s = i === 0 ? 100 : 75 + (i % 3) * 5;
|
return useMemoizedFn((count: number): string[] => {
|
||||||
const l = i === 0 ? 50 : 55 + (i % 2) * 5;
|
if (count <= 0) return [];
|
||||||
|
|
||||||
colors.push(`hsl(${Math.round(hue)}, ${s}%, ${l}%)`);
|
// Moderate chroma: clean and crisp, but gentle (not neon). Too low reads as
|
||||||
|
// muddy/dirty; too high reads as harsh. Dark mode a touch lower so fills
|
||||||
|
// stay calm against the dark canvas.
|
||||||
|
const baseChroma = isDarkTheme ? 0.085 : 0.12;
|
||||||
|
|
||||||
|
// First series is the "primary" anchor: SAME blue family (the bluest end of
|
||||||
|
// the ramp, nearest the brand hue) but clearly brighter and more saturated —
|
||||||
|
// a vivid, clean brand-blue that reads as the base color. The rest of the
|
||||||
|
// palette stays low-chroma, so the anchor pops as the primary while the
|
||||||
|
// family still feels cohesive.
|
||||||
|
const colors: string[] = [
|
||||||
|
formatHex(
|
||||||
|
clampChroma(
|
||||||
|
{
|
||||||
|
mode: 'oklch',
|
||||||
|
l: isDarkTheme ? 0.62 : 0.66,
|
||||||
|
c: isDarkTheme ? 0.16 : 0.2,
|
||||||
|
h: COOL_HUE_START
|
||||||
|
},
|
||||||
|
'oklch'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
];
|
||||||
|
|
||||||
|
const rest = count - 1;
|
||||||
|
if (rest <= 0) return colors;
|
||||||
|
|
||||||
|
// Separation is driven by LIGHTNESS, not hue. The hue band is narrow, so as
|
||||||
|
// the count grows we add lightness TIERS — each tier reuses the same narrow
|
||||||
|
// hue ramp at a distinct lightness level, multiplying how many separable
|
||||||
|
// colors fit while keeping the whole palette in one cohesive family.
|
||||||
|
const tiers = rest <= 6 ? 1 : rest <= 12 ? 2 : 3;
|
||||||
|
const steps = Math.ceil(rest / tiers);
|
||||||
|
|
||||||
|
// Distinct lightness levels per tier count (index 0 → 2 tiers, 1 → 3 tiers).
|
||||||
|
// Lighter, airier levels for a fresh/crisp feel; kept in the upper-mid range
|
||||||
|
// so fills stay clean and legible.
|
||||||
|
const lightTiers = isDarkTheme
|
||||||
|
? [
|
||||||
|
[0.68, 0.5],
|
||||||
|
[0.72, 0.6, 0.48]
|
||||||
|
]
|
||||||
|
: [
|
||||||
|
[0.82, 0.64],
|
||||||
|
[0.84, 0.72, 0.6]
|
||||||
|
];
|
||||||
|
// Single-tier light/dark zig-zag for the common small-count case.
|
||||||
|
const [lightLo, lightHi] = isDarkTheme ? [0.5, 0.68] : [0.64, 0.82];
|
||||||
|
|
||||||
|
for (let i = 0; i < rest; i++) {
|
||||||
|
// Cycle the tier on every step so consecutive series always differ in
|
||||||
|
// lightness — exactly where stacked bars are hardest to tell apart.
|
||||||
|
const tier = i % tiers;
|
||||||
|
const step = Math.floor(i / tiers);
|
||||||
|
|
||||||
|
// Even hue ramp across the narrow band. Each tier is offset by a fraction
|
||||||
|
// of a step so same-step colors in different tiers don't share a hue.
|
||||||
|
const t = steps <= 1 ? 0.5 : (step + tier / tiers) / steps;
|
||||||
|
const hue = COOL_HUE_START + t * COOL_HUE_RANGE;
|
||||||
|
|
||||||
|
// 1 tier (≤6 series): light/dark zig-zag. 2-3 tiers: the tier's level.
|
||||||
|
const lightness =
|
||||||
|
tiers === 1 ? (i % 2 ? lightLo : lightHi) : lightTiers[tiers - 2][tier];
|
||||||
|
|
||||||
|
// Clamp chroma into the sRGB gamut so values don't get distorted by a raw
|
||||||
|
// channel clip when serialized to hex.
|
||||||
|
colors.push(
|
||||||
|
formatHex(
|
||||||
|
clampChroma(
|
||||||
|
{ mode: 'oklch', l: lightness, c: baseChroma, h: hue },
|
||||||
|
'oklch'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return colors;
|
return colors;
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ export function useQueryDataList<
|
|||||||
Response = Array<ListItem>
|
Response = Array<ListItem>
|
||||||
>(option: {
|
>(option: {
|
||||||
key: string;
|
key: string;
|
||||||
|
manual?: boolean;
|
||||||
responseType?: 'array' | 'object';
|
responseType?: 'array' | 'object';
|
||||||
fetchList: (
|
fetchList: (
|
||||||
params: Params,
|
params: Params,
|
||||||
@@ -38,6 +39,7 @@ export function useQueryDataList<
|
|||||||
fetchList,
|
fetchList,
|
||||||
getLabel,
|
getLabel,
|
||||||
getValue,
|
getValue,
|
||||||
|
manual = true,
|
||||||
responseType = 'array',
|
responseType = 'array',
|
||||||
errorMsg
|
errorMsg
|
||||||
} = option;
|
} = option;
|
||||||
@@ -70,7 +72,7 @@ export function useQueryDataList<
|
|||||||
return responseType === 'array' ? res.items || [] : res;
|
return responseType === 'array' ? res.items || [] : res;
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
manual: true,
|
manual: manual,
|
||||||
debounceWait: option.debounceWait || 300,
|
debounceWait: option.debounceWait || 300,
|
||||||
onSuccess: () => {},
|
onSuccess: () => {},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
@@ -105,16 +107,18 @@ export function useQueryDataList<
|
|||||||
export function useQueryData<Detail, Params = any>(option: {
|
export function useQueryData<Detail, Params = any>(option: {
|
||||||
key: string;
|
key: string;
|
||||||
delay?: number;
|
delay?: number;
|
||||||
|
manual?: boolean;
|
||||||
fetchDetail: (params: Params, options?: any) => Promise<Detail>;
|
fetchDetail: (params: Params, options?: any) => Promise<Detail>;
|
||||||
getData?: (response: Detail, params?: any) => any;
|
getData?: (response: Detail, params?: any) => any;
|
||||||
errorMsg?: string;
|
errorMsg?: string;
|
||||||
}): {
|
}): {
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
detailData: Detail;
|
detailData: Detail;
|
||||||
|
manual?: boolean;
|
||||||
cancelRequest: () => void;
|
cancelRequest: () => void;
|
||||||
fetchData: (params: Params, extra?: any) => Promise<Detail>;
|
fetchData: (params: Params, extra?: any) => Promise<Detail>;
|
||||||
} {
|
} {
|
||||||
const { key, fetchDetail, getData, errorMsg, delay } = option;
|
const { key, fetchDetail, getData, errorMsg, delay, manual = true } = option;
|
||||||
const axiosTokenRef = useRef<CancelTokenSource | null>(null);
|
const axiosTokenRef = useRef<CancelTokenSource | null>(null);
|
||||||
const [detailData, setDetailData] = useState<Detail>({} as Detail);
|
const [detailData, setDetailData] = useState<Detail>({} as Detail);
|
||||||
|
|
||||||
@@ -142,7 +146,7 @@ export function useQueryData<Detail, Params = any>(option: {
|
|||||||
return res;
|
return res;
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
manual: true,
|
manual: manual,
|
||||||
onSuccess: () => {},
|
onSuccess: () => {},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
message.error(
|
message.error(
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import { useMemoizedFn } from 'ahooks';
|
||||||
|
import { useRef, useState } from 'react';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Centralizes the anti-double-submit + anti-deadlock logic shared by the
|
||||||
|
* "add/edit" modals.
|
||||||
|
*
|
||||||
|
* The lock has to straddle antd's async field validation, whose result comes
|
||||||
|
* back through two separate callbacks (`onFinish` / `onFinishFailed`). So the
|
||||||
|
* three wiring points below are intentional and map 1:1 to those anchors:
|
||||||
|
*
|
||||||
|
* - `guard` wrap the submit trigger (button / ModalFooter onOk). Blocks
|
||||||
|
* re-entry while a submit is already in flight, then fires submit.
|
||||||
|
* - `run` wrap the `onFinish` handler. Holds the lock + loading until the
|
||||||
|
* `onOk` request settles (success or error).
|
||||||
|
* - `release` pass as the form's `onFinishFailed`. Releases the lock when
|
||||||
|
* validation fails, otherwise the button would dead-lock.
|
||||||
|
*/
|
||||||
|
export default function useSubmitLock() {
|
||||||
|
const [loading, setLoading] = useState<boolean>(false);
|
||||||
|
const lockRef = useRef<boolean>(false);
|
||||||
|
|
||||||
|
const release = useMemoizedFn(() => {
|
||||||
|
setLoading(false);
|
||||||
|
lockRef.current = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
const guard = useMemoizedFn((submit: () => void) => {
|
||||||
|
if (lockRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
lockRef.current = true;
|
||||||
|
submit();
|
||||||
|
});
|
||||||
|
|
||||||
|
const run = useMemoizedFn(async (task: () => void | Promise<void>) => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
await task();
|
||||||
|
} finally {
|
||||||
|
release();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { loading, guard, run, release };
|
||||||
|
}
|
||||||
@@ -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';
|
||||||
|
|
||||||
@@ -38,6 +37,7 @@ export default function useTableFetch<T>(
|
|||||||
key?: (typeof PaginationKey)[keyof typeof PaginationKey];
|
key?: (typeof PaginationKey)[keyof typeof PaginationKey];
|
||||||
fetchAPI: (params: any, options?: any) => Promise<Global.PageResponse<T>>;
|
fetchAPI: (params: any, options?: any) => Promise<Global.PageResponse<T>>;
|
||||||
deleteAPI?: (id: number, params?: any) => Promise<any>;
|
deleteAPI?: (id: number, params?: any) => Promise<any>;
|
||||||
|
afterDelete?: (id?: number | number[]) => void;
|
||||||
contentForDelete?: string;
|
contentForDelete?: string;
|
||||||
defaultData?: any[];
|
defaultData?: any[];
|
||||||
events?: EventsType[];
|
events?: EventsType[];
|
||||||
@@ -49,6 +49,7 @@ export default function useTableFetch<T>(
|
|||||||
const {
|
const {
|
||||||
fetchAPI,
|
fetchAPI,
|
||||||
deleteAPI,
|
deleteAPI,
|
||||||
|
afterDelete,
|
||||||
contentForDelete,
|
contentForDelete,
|
||||||
API,
|
API,
|
||||||
polling = false,
|
polling = false,
|
||||||
@@ -265,6 +266,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 +363,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]);
|
||||||
|
afterDelete?.(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();
|
||||||
@@ -387,6 +392,7 @@ export default function useTableFetch<T>(
|
|||||||
successIds.push(id);
|
successIds.push(id);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
afterDelete?.(successIds);
|
||||||
rowSelection.removeSelectedKeys(successIds);
|
rowSelection.removeSelectedKeys(successIds);
|
||||||
fetchData();
|
fetchData();
|
||||||
return res;
|
return res;
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ type EventsType = 'CREATE' | 'UPDATE' | 'DELETE' | 'INSERT';
|
|||||||
export function useUpdateChunkedList(options: {
|
export function useUpdateChunkedList(options: {
|
||||||
events?: EventsType[];
|
events?: EventsType[];
|
||||||
dataList?: any[];
|
dataList?: any[];
|
||||||
triggerAt?: React.MutableRefObject<number>;
|
|
||||||
limit?: number;
|
limit?: number;
|
||||||
onCreate?: (args: any) => void;
|
onCreate?: (args: any) => void;
|
||||||
onUpdate?: (args: any) => void;
|
onUpdate?: (args: any) => void;
|
||||||
@@ -24,9 +23,9 @@ export function useUpdateChunkedList(options: {
|
|||||||
filterFun?: (args: any) => boolean;
|
filterFun?: (args: any) => boolean;
|
||||||
mapFun?: (args: any) => any;
|
mapFun?: (args: any) => any;
|
||||||
computedID?: (d: object) => string;
|
computedID?: (d: object) => string;
|
||||||
|
isNewItem?: (item: any) => boolean;
|
||||||
}) {
|
}) {
|
||||||
const { events = ['CREATE', 'DELETE', 'UPDATE', 'INSERT'], triggerAt } =
|
const { events = ['CREATE', 'DELETE', 'UPDATE', 'INSERT'] } = options;
|
||||||
options;
|
|
||||||
const deletedIdsRef = useRef<Set<number | string>>(new Set());
|
const deletedIdsRef = useRef<Set<number | string>>(new Set());
|
||||||
const cacheDataListRef = useRef<any[]>(options.dataList || []);
|
const cacheDataListRef = useRef<any[]>(options.dataList || []);
|
||||||
const timerRef = useRef<any>(null);
|
const timerRef = useRef<any>(null);
|
||||||
@@ -71,17 +70,14 @@ export function useUpdateChunkedList(options: {
|
|||||||
(sItem: any) => sItem.id === item.id
|
(sItem: any) => sItem.id === item.id
|
||||||
);
|
);
|
||||||
const updateItem = { ...item };
|
const updateItem = { ...item };
|
||||||
if (updateIndex === -1 && !triggerAt?.current) {
|
if (updateIndex === -1) {
|
||||||
acc.push(updateItem);
|
acc.push(updateItem);
|
||||||
} else if (!triggerAt?.current) {
|
} else {
|
||||||
cacheDataListRef.current[updateIndex] = updateItem;
|
cacheDataListRef.current[updateIndex] = updateItem;
|
||||||
}
|
}
|
||||||
|
// only push items created after the watch started
|
||||||
|
|
||||||
// TODO: only push items created after triggerAt
|
if (options.isNewItem?.(item)) {
|
||||||
if (
|
|
||||||
triggerAt?.current &&
|
|
||||||
Date.parse(item.created_at) >= triggerAt.current
|
|
||||||
) {
|
|
||||||
latestCreateList.push(updateItem);
|
latestCreateList.push(updateItem);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,6 +89,10 @@ export function useUpdateChunkedList(options: {
|
|||||||
...cacheDataListRef.current
|
...cacheDataListRef.current
|
||||||
].slice(0, limit);
|
].slice(0, limit);
|
||||||
|
|
||||||
|
options.setDataList?.([...cacheDataListRef.current], {
|
||||||
|
createdIds: newDataList.map((item) => item.id)
|
||||||
|
});
|
||||||
|
|
||||||
options.onCreate?.(latestCreateList);
|
options.onCreate?.(latestCreateList);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,10 +103,8 @@ export function useUpdateChunkedList(options: {
|
|||||||
cacheDataListRef.current = cacheDataListRef.current?.filter(
|
cacheDataListRef.current = cacheDataListRef.current?.filter(
|
||||||
(item: any) => {
|
(item: any) => {
|
||||||
// collect deleted items
|
// collect deleted items
|
||||||
if (triggerAt?.current) {
|
if (ids?.includes(item.id) && !options.isNewItem?.(item)) {
|
||||||
if (ids?.includes(item.id)) {
|
deletedList.push(item);
|
||||||
deletedList.push(item);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return !ids?.includes(item.id);
|
return !ids?.includes(item.id);
|
||||||
}
|
}
|
||||||
@@ -133,10 +131,8 @@ export function useUpdateChunkedList(options: {
|
|||||||
updateItem,
|
updateItem,
|
||||||
...cacheDataListRef.current.slice(0, limit - 1)
|
...cacheDataListRef.current.slice(0, limit - 1)
|
||||||
];
|
];
|
||||||
if (options.onUpdate && triggerAt?.current) {
|
if (options.onUpdate && options.isNewItem?.(item)) {
|
||||||
if (Date.parse(item.created_at) >= triggerAt.current) {
|
options.onUpdate?.([updateItem]);
|
||||||
options.onUpdate?.([updateItem]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -63,8 +63,7 @@ export default function useWatchList<T = Record<string, any>>(API: string) {
|
|||||||
listRequestTokenRef.current?.cancel?.();
|
listRequestTokenRef.current?.cancel?.();
|
||||||
listRequestTokenRef.current = createAxiosToken();
|
listRequestTokenRef.current = createAxiosToken();
|
||||||
const params = {
|
const params = {
|
||||||
page: 1,
|
page: -1
|
||||||
perPage: 100
|
|
||||||
};
|
};
|
||||||
const res: any = await queryAllDataList(params, {
|
const res: any = await queryAllDataList(params, {
|
||||||
token: listRequestTokenRef.current.token
|
token: listRequestTokenRef.current.token
|
||||||
@@ -86,6 +85,7 @@ export default function useWatchList<T = Record<string, any>>(API: string) {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
watchDataList,
|
watchDataList,
|
||||||
|
setWatchDataList,
|
||||||
deleteItemFromCache: handleDeleteItemFromCache
|
deleteItemFromCache: handleDeleteItemFromCache
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import { useAtom } from 'jotai';
|
|||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import { DEFAULT_ENTER_PAGE } from '../config/settings';
|
import { DEFAULT_ENTER_PAGE } from '../config/settings';
|
||||||
|
import GithubStar from './github-star';
|
||||||
|
|
||||||
const NewLabel = styled.span`
|
const NewLabel = styled.span`
|
||||||
position: relative;
|
position: relative;
|
||||||
@@ -210,14 +211,14 @@ export const ExtraContent = (props: { isDarkTheme?: boolean }) => {
|
|||||||
key: 'settings',
|
key: 'settings',
|
||||||
label: (
|
label: (
|
||||||
<span className="flex flex-center">
|
<span className="flex flex-center">
|
||||||
<IconFont type="icon-settings-02" />
|
<IconFont type="icon-preferences" />
|
||||||
<span className="m-l-8" style={{ marginLeft: 8 }}>
|
<span className="m-l-8" style={{ marginLeft: 8 }}>
|
||||||
{intl?.formatMessage?.({ id: 'common.button.settings' })}
|
{intl?.formatMessage?.({ id: 'common.preferences' })}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
),
|
),
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
history.push('/profile');
|
history.push('/preferences');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -260,6 +261,7 @@ export const ExtraContent = (props: { isDarkTheme?: boolean }) => {
|
|||||||
<Wrapper>
|
<Wrapper>
|
||||||
{contextHolder}
|
{contextHolder}
|
||||||
<PluginExtraField name="OrgSwitcher" isDarkTheme={isDarkTheme} />
|
<PluginExtraField name="OrgSwitcher" isDarkTheme={isDarkTheme} />
|
||||||
|
{process.env.ENABLE_ENTERPRISE !== 'true' && <GithubStar />}
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
|
|||||||
@@ -0,0 +1,140 @@
|
|||||||
|
import externalLinks from '@/constants/external-links';
|
||||||
|
import { GithubFilled } from '@ant-design/icons';
|
||||||
|
import { useIntl } from '@umijs/max';
|
||||||
|
import { Tooltip } from 'antd';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
const REPO = 'gpustack/gpustack';
|
||||||
|
const CACHE_KEY = 'gpustack:github-stars';
|
||||||
|
const CACHE_TTL = 24 * 60 * 60 * 1000;
|
||||||
|
const FETCH_TIMEOUT = 4000;
|
||||||
|
|
||||||
|
const StarLink = styled.a`
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: stretch;
|
||||||
|
height: 24px;
|
||||||
|
border-radius: var(--ant-border-radius);
|
||||||
|
border: 1px solid var(--ant-color-border-secondary);
|
||||||
|
background-color: var(--ant-color-bg-container);
|
||||||
|
color: var(--ant-color-text-secondary);
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
transition:
|
||||||
|
border-color 0.2s,
|
||||||
|
color 0.2s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: var(--ant-color-border);
|
||||||
|
color: var(--ant-color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.seg {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 0 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.seg + .seg {
|
||||||
|
border-left: 1px solid var(--ant-color-border-secondary);
|
||||||
|
background-color: var(--ant-color-fill-quaternary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.anticon {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.count {
|
||||||
|
font-weight: 500;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
min-width: 1.5em;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const formatCount = (n: number): string => {
|
||||||
|
if (n >= 1000) {
|
||||||
|
const k = n / 1000;
|
||||||
|
return k >= 10 ? `${Math.round(k)}k` : `${k.toFixed(1)}k`;
|
||||||
|
}
|
||||||
|
return String(n);
|
||||||
|
};
|
||||||
|
|
||||||
|
type CacheEntry = { value: number; time: number };
|
||||||
|
|
||||||
|
const readCache = (): CacheEntry | null => {
|
||||||
|
try {
|
||||||
|
const raw = localStorage.getItem(CACHE_KEY);
|
||||||
|
if (!raw) return null;
|
||||||
|
const parsed = JSON.parse(raw);
|
||||||
|
if (typeof parsed?.value !== 'number' || typeof parsed?.time !== 'number') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return parsed;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const writeCache = (value: number) => {
|
||||||
|
try {
|
||||||
|
localStorage.setItem(
|
||||||
|
CACHE_KEY,
|
||||||
|
JSON.stringify({ value, time: Date.now() })
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
// ignore quota errors
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const GithubStar = () => {
|
||||||
|
const intl = useIntl();
|
||||||
|
const [count, setCount] = useState<number | null>(
|
||||||
|
() => readCache()?.value ?? null
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const cached = readCache();
|
||||||
|
const fresh = cached && Date.now() - cached.time < CACHE_TTL;
|
||||||
|
if (fresh) return;
|
||||||
|
|
||||||
|
const controller = new AbortController();
|
||||||
|
const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT);
|
||||||
|
|
||||||
|
fetch(`https://api.github.com/repos/${REPO}`, { signal: controller.signal })
|
||||||
|
.then((r) => (r.ok ? r.json() : null))
|
||||||
|
.then((data) => {
|
||||||
|
if (!data || typeof data.stargazers_count !== 'number') return;
|
||||||
|
setCount(data.stargazers_count);
|
||||||
|
writeCache(data.stargazers_count);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
// offline, blocked, rate-limited — stay hidden if no cache
|
||||||
|
})
|
||||||
|
.finally(() => clearTimeout(timer));
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearTimeout(timer);
|
||||||
|
controller.abort();
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip title={intl.formatMessage({ id: 'common.github.star.tooltip' })}>
|
||||||
|
<StarLink href={externalLinks.github} target="_blank" rel="noreferrer">
|
||||||
|
<span className="seg">
|
||||||
|
<GithubFilled />
|
||||||
|
</span>
|
||||||
|
<span className="seg">
|
||||||
|
<span className="count">
|
||||||
|
{count != null ? formatCount(count) : 'Star'}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</StarLink>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default GithubStar;
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
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 '@/components/iconfont/iconfont.js';
|
||||||
|
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';
|
||||||
@@ -10,6 +12,7 @@ import useUserSettings from '@/hooks/use-user-settings';
|
|||||||
import useUserSettingsStorage from '@/hooks/use-user-settings-storage';
|
import useUserSettingsStorage from '@/hooks/use-user-settings-storage';
|
||||||
import useAddResource from '@/pages/dashboard/hooks/use-add-resource';
|
import useAddResource from '@/pages/dashboard/hooks/use-add-resource';
|
||||||
import { logout } from '@/pages/login/apis';
|
import { logout } from '@/pages/login/apis';
|
||||||
|
import { didInitialStateProbe, probeAccessFlags } from '@/utils/access-probes';
|
||||||
import {
|
import {
|
||||||
readColumnSettings,
|
readColumnSettings,
|
||||||
readState,
|
readState,
|
||||||
@@ -19,12 +22,9 @@ import {
|
|||||||
import { useAccessMarkedRoutes } from '@@/plugin-access';
|
import { useAccessMarkedRoutes } from '@@/plugin-access';
|
||||||
import { useModel } from '@@/plugin-model';
|
import { useModel } from '@@/plugin-model';
|
||||||
import { ProLayout } from '@ant-design/pro-components';
|
import { ProLayout } from '@ant-design/pro-components';
|
||||||
|
import { CoreUIProvider, IconFont } from '@gpustack/core-ui';
|
||||||
import {
|
import {
|
||||||
CoreUIProvider,
|
Access,
|
||||||
IconFont,
|
|
||||||
useOverlayScroller
|
|
||||||
} from '@gpustack/core-ui';
|
|
||||||
import {
|
|
||||||
Outlet,
|
Outlet,
|
||||||
dropByCacheKey,
|
dropByCacheKey,
|
||||||
getAllLocales,
|
getAllLocales,
|
||||||
@@ -42,7 +42,7 @@ import {
|
|||||||
import { Button, ConfigProvider, Modal, theme } from 'antd';
|
import { Button, ConfigProvider, Modal, theme } from 'antd';
|
||||||
import { useAtom } from 'jotai';
|
import { useAtom } from 'jotai';
|
||||||
import 'overlayscrollbars/overlayscrollbars.css';
|
import 'overlayscrollbars/overlayscrollbars.css';
|
||||||
import { useMemo, useRef } from 'react';
|
import { useEffect, useMemo, useRef } from 'react';
|
||||||
import { PageContainerInner } from '../pages/_components/page-box';
|
import { PageContainerInner } from '../pages/_components/page-box';
|
||||||
import Exception from './Exception';
|
import Exception from './Exception';
|
||||||
import './Layout.css';
|
import './Layout.css';
|
||||||
@@ -64,16 +64,14 @@ const NO_CONTAINER_PAGES = [
|
|||||||
'clusterCreate',
|
'clusterCreate',
|
||||||
'benchmarkDetail',
|
'benchmarkDetail',
|
||||||
'deployment',
|
'deployment',
|
||||||
'video',
|
'video'
|
||||||
'instances',
|
|
||||||
'storage'
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const CHECK_RESOURCE_PATH = [
|
const CHECK_RESOURCE_PATH = [
|
||||||
'/resources/workers',
|
'/resources/workers',
|
||||||
'/cluster-management/clusters/list',
|
'/resources/clusters/list',
|
||||||
'/cluster-management/credentials',
|
'/resources/credentials',
|
||||||
'/cluster-management/clusters/create'
|
'/resources/clusters/create'
|
||||||
];
|
];
|
||||||
|
|
||||||
type NewRoute = IRoute & {
|
type NewRoute = IRoute & {
|
||||||
@@ -134,9 +132,6 @@ const mapRoutes = (routes: IRoute[], role: string) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default (props: any) => {
|
export default (props: any) => {
|
||||||
const { initialize: initialize } = useOverlayScroller({
|
|
||||||
defer: false
|
|
||||||
});
|
|
||||||
const [, contextHolder] = Modal.useModal();
|
const [, contextHolder] = Modal.useModal();
|
||||||
const { themeData, setUserSettings, userSettings } = useUserSettings();
|
const { themeData, setUserSettings, userSettings } = useUserSettings();
|
||||||
const [userInfo] = useAtom(userAtom);
|
const [userInfo] = useAtom(userAtom);
|
||||||
@@ -161,6 +156,50 @@ export default (props: any) => {
|
|||||||
|
|
||||||
const { initialState, loading, setInitialState } = initialInfo;
|
const { initialState, loading, setInitialState } = initialInfo;
|
||||||
const access = useAccess();
|
const access = useAccess();
|
||||||
|
const probedForUserRef = useRef<string | null>(null);
|
||||||
|
|
||||||
|
// Backfill the access probes (cluster / resource-events) once we're in
|
||||||
|
// the authenticated shell. `getInitialState` runs only once at app
|
||||||
|
// boot and can't probe on the login page (no session => 401), so after
|
||||||
|
// a first login the flags arrive here as `undefined` — which the
|
||||||
|
// access predicate treats as "don't restrict", flashing GPU Service /
|
||||||
|
// the full Usage page on until a manual refresh. This layout mounts
|
||||||
|
// only post-auth (login is `layout:false`) and on every entry, so it's
|
||||||
|
// the reliable place to resolve them.
|
||||||
|
//
|
||||||
|
// The effect is keyed on the user IDENTITY, not on the flag values —
|
||||||
|
// gating on the flags is what made this fragile (on a refresh
|
||||||
|
// `getInitialState` commits `currentUser` and the flags in the same
|
||||||
|
// update, so a flag-gated effect sees them already-known and never
|
||||||
|
// fires). Whether to actually hit the network is decided by the
|
||||||
|
// module-scoped `didInitialStateProbe()` marker, which is true only
|
||||||
|
// when `getInitialState` already probed for an authenticated user this
|
||||||
|
// page load (the refresh path) — so we skip the duplicate request there
|
||||||
|
// but still probe on the SPA-login path. Keying on identity also
|
||||||
|
// re-probes correctly if the signed-in user changes.
|
||||||
|
const currentUser = initialState?.currentUser;
|
||||||
|
const username = currentUser?.username;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!username || !setInitialState) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (probedForUserRef.current === username) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
probedForUserRef.current = username;
|
||||||
|
// The refresh path already resolved the flags inside `getInitialState`
|
||||||
|
// for this user — don't issue a duplicate probe.
|
||||||
|
if (didInitialStateProbe()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
probeAccessFlags().then((accessFlags) => {
|
||||||
|
setInitialState((prev: any) => ({
|
||||||
|
...prev,
|
||||||
|
...accessFlags
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
}, [username, setInitialState]);
|
||||||
|
|
||||||
const userConfig = {
|
const userConfig = {
|
||||||
title: '',
|
title: '',
|
||||||
@@ -240,30 +279,7 @@ export default (props: any) => {
|
|||||||
}, [userSettings.collapsed]);
|
}, [userSettings.collapsed]);
|
||||||
|
|
||||||
const renderMenuHeader = (logo: React.ReactNode, title: React.ReactNode) => {
|
const renderMenuHeader = (logo: React.ReactNode, title: React.ReactNode) => {
|
||||||
return (
|
return <>{logo}</>;
|
||||||
<>
|
|
||||||
{logo}
|
|
||||||
<div className="collapse-wrap" onClick={handleToggleCollapse}>
|
|
||||||
<Button
|
|
||||||
style={{
|
|
||||||
marginRight: collapsed ? 0 : -14,
|
|
||||||
border: 'none',
|
|
||||||
cursor: 'w-resize'
|
|
||||||
}}
|
|
||||||
size="small"
|
|
||||||
type={collapsed ? 'default' : 'text'}
|
|
||||||
>
|
|
||||||
<IconFont
|
|
||||||
type={collapsed ? 'icon-expand-left' : 'icon-expand-right'}
|
|
||||||
className="font-size-18 text-secondary"
|
|
||||||
style={{
|
|
||||||
display: 'block'
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const menuContentRender = (menuProps: any, defaultDom: React.ReactNode) => {
|
const menuContentRender = (menuProps: any, defaultDom: React.ReactNode) => {
|
||||||
@@ -356,7 +372,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: '',
|
||||||
isDarkTheme: userSettings.isDarkTheme,
|
isDarkTheme: userSettings.isDarkTheme,
|
||||||
defaultColorPrimary: COLOR_PRIMARY
|
defaultColorPrimary: COLOR_PRIMARY
|
||||||
}}
|
}}
|
||||||
@@ -387,6 +403,7 @@ export default (props: any) => {
|
|||||||
writeState
|
writeState
|
||||||
}}
|
}}
|
||||||
slots={coreUISlots}
|
slots={coreUISlots}
|
||||||
|
access={{ Access, useAccess }}
|
||||||
>
|
>
|
||||||
<DarkMask></DarkMask>
|
<DarkMask></DarkMask>
|
||||||
<ProLayout
|
<ProLayout
|
||||||
@@ -406,9 +423,23 @@ export default (props: any) => {
|
|||||||
openKeys={false}
|
openKeys={false}
|
||||||
disableMobile={true}
|
disableMobile={true}
|
||||||
siderWidth={220}
|
siderWidth={220}
|
||||||
|
menuFooterRender={() => (
|
||||||
|
<Button
|
||||||
|
style={{
|
||||||
|
border: 'none'
|
||||||
|
}}
|
||||||
|
size="small"
|
||||||
|
type={'text'}
|
||||||
|
onClick={handleToggleCollapse}
|
||||||
|
>
|
||||||
|
<IconFont
|
||||||
|
type={collapsed ? 'icon-expand-left' : 'icon-expand-right'}
|
||||||
|
className="font-size-18"
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
onCollapse={onCollapse}
|
onCollapse={onCollapse}
|
||||||
onMenuHeaderClick={onMenuHeaderClick}
|
onMenuHeaderClick={onMenuHeaderClick}
|
||||||
menuHeaderRender={renderMenuHeader}
|
|
||||||
collapsed={userSettings.collapsed}
|
collapsed={userSettings.collapsed}
|
||||||
onPageChange={onPageChange}
|
onPageChange={onPageChange}
|
||||||
formatMessage={formatMessage}
|
formatMessage={formatMessage}
|
||||||
@@ -422,23 +453,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>
|
||||||
|
|||||||
@@ -256,7 +256,7 @@ export const getRightRenderContent = (opts: {
|
|||||||
</span>
|
</span>
|
||||||
),
|
),
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
history.push('/profile');
|
history.push('/preferences');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
|
import { collapsedMenuGroupsAtom } from '@/atoms/settings';
|
||||||
import { CaretDownOutlined } from '@ant-design/icons';
|
import { CaretDownOutlined } from '@ant-design/icons';
|
||||||
import { IconFont } from '@gpustack/core-ui';
|
import { IconFont, OverlayScroller } from '@gpustack/core-ui';
|
||||||
import { Link, useLocation } from '@umijs/max';
|
import { Link, useLocation } from '@umijs/max';
|
||||||
import { Tooltip } from 'antd';
|
import { Tooltip } from 'antd';
|
||||||
import { createStyles } from 'antd-style';
|
import { createStyles, type FullToken } from 'antd-style';
|
||||||
import React, { useMemo, useState } from 'react';
|
import { useAtom } from 'jotai';
|
||||||
|
import React, { useMemo } from 'react';
|
||||||
|
|
||||||
interface MenuItem {
|
interface MenuItem {
|
||||||
icon?: string;
|
icon?: string;
|
||||||
@@ -20,125 +22,143 @@ interface SiderMenuProps {
|
|||||||
initialState: Global.InitialStateType;
|
initialState: Global.InitialStateType;
|
||||||
}
|
}
|
||||||
|
|
||||||
const useStyles = createStyles(({ css, token }) => {
|
const useStyles = createStyles(
|
||||||
console.log('useStyles', token);
|
({ css, token }: { css: any; token: FullToken }) => {
|
||||||
|
console.log('useStyles', token);
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const { Menu } = token;
|
const { Menu } = token;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
siderMenu: css`
|
siderMenu: css`
|
||||||
&.sider-menu-collapsed {
|
|
||||||
.menu-item {
|
|
||||||
justify-content: center;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
groupTitle: css`
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
cursor: pointer;
|
|
||||||
white-space: nowrap;
|
|
||||||
padding: var(--ant-padding-xs) var(--ant-padding);
|
|
||||||
font-size: 12px;
|
|
||||||
padding-bottom: 4px;
|
|
||||||
overflow: hidden;
|
|
||||||
height: 30px;
|
|
||||||
&:hover {
|
|
||||||
.group-title-text {
|
|
||||||
color: var(--ant-color-text);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.anticon {
|
|
||||||
transform: scale(0.8);
|
|
||||||
}
|
|
||||||
.group-title-text {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 4px;
|
|
||||||
font-size: 12px;
|
|
||||||
color: var(--ant-color-text-tertiary);
|
|
||||||
font-weight: 400;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.menu-item-group-title-collapsed {
|
|
||||||
position: relative;
|
|
||||||
height: 1px;
|
|
||||||
padding-block: 0;
|
|
||||||
padding-inline: 0;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
menuItemContent: css`
|
|
||||||
margin: 2px 0;
|
|
||||||
border-radius: 4px;
|
|
||||||
overflow: hidden;
|
|
||||||
`,
|
|
||||||
menuItemWrapper: css`
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: flex-start;
|
|
||||||
gap: 12px;
|
|
||||||
cursor: pointer;
|
|
||||||
position: relative;
|
|
||||||
padding-inline: calc(var(--ant-font-size) * 2) var(--ant-padding);
|
|
||||||
padding-left: 16px;
|
|
||||||
overflow: hidden;
|
|
||||||
white-space: nowrap;
|
|
||||||
height: ${Menu.itemHeight}px;
|
|
||||||
line-height: ${Menu.itemHeight}px;
|
|
||||||
color: var(--ant-color-text-tertiary);
|
|
||||||
&:hover {
|
|
||||||
background-color: ${Menu.itemHoverBg};
|
|
||||||
color: ${Menu.itemHoverColor};
|
|
||||||
}
|
|
||||||
&.menu-item-selected {
|
|
||||||
background-color: ${Menu.itemSelectedBg};
|
|
||||||
color: ${Menu.itemSelectedColor};
|
|
||||||
.anticon {
|
|
||||||
color: ${Menu.itemSelectedColor};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
&:active {
|
|
||||||
background-color: ${Menu.itemActiveBg};
|
|
||||||
color: ${Menu.itemActiveColor};
|
|
||||||
}
|
|
||||||
.anticon {
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
.icon-wrapper {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
&.sider-menu-collapsed {
|
||||||
}
|
.menu-item {
|
||||||
`,
|
justify-content: center;
|
||||||
menuItemGroup: css`
|
padding: 0;
|
||||||
&.menu-item-group-hidden {
|
}
|
||||||
display: none;
|
}
|
||||||
}
|
.os-scrollbar-vertical .os-scrollbar-handle {
|
||||||
`,
|
min-width: 4px;
|
||||||
line: css`
|
max-width: 4px;
|
||||||
height: 1px;
|
}
|
||||||
margin-block: 6px;
|
`,
|
||||||
background-color: ${token.colorSplit};
|
groupTitle: css`
|
||||||
position: absolute;
|
display: flex;
|
||||||
left: -2px;
|
align-items: center;
|
||||||
right: -2px;
|
gap: 8px;
|
||||||
`
|
cursor: pointer;
|
||||||
};
|
white-space: nowrap;
|
||||||
});
|
padding: var(--ant-padding-xs) var(--ant-padding);
|
||||||
|
font-size: 12px;
|
||||||
|
padding-bottom: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
height: 30px;
|
||||||
|
&:hover {
|
||||||
|
.group-title-text {
|
||||||
|
color: var(--ant-color-text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.anticon {
|
||||||
|
transform: scale(0.8);
|
||||||
|
}
|
||||||
|
.group-title-text {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--ant-color-text);
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.menu-item-group-title-collapsed {
|
||||||
|
position: relative;
|
||||||
|
height: 1px;
|
||||||
|
padding-block: 0;
|
||||||
|
padding-inline: 0;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
menuItemContent: css`
|
||||||
|
margin: 2px 0;
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
`,
|
||||||
|
menuItemWrapper: css`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-start;
|
||||||
|
gap: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
padding-inline: calc(var(--ant-font-size) * 2) var(--ant-padding);
|
||||||
|
padding-left: 16px;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
height: ${Menu.itemHeight}px;
|
||||||
|
line-height: ${Menu.itemHeight}px;
|
||||||
|
color: var(--ant-color-text-tertiary);
|
||||||
|
&:hover {
|
||||||
|
background-color: ${Menu.itemHoverBg};
|
||||||
|
color: ${Menu.itemHoverColor};
|
||||||
|
}
|
||||||
|
&.menu-item-selected {
|
||||||
|
background-color: ${Menu.menuItemSelectedBg};
|
||||||
|
color: ${Menu.itemSelectedColor};
|
||||||
|
|
||||||
|
.anticon {
|
||||||
|
color: ${Menu.itemSelectedColor};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&:active {
|
||||||
|
background-color: ${Menu.itemActiveBg};
|
||||||
|
color: ${Menu.itemActiveColor};
|
||||||
|
}
|
||||||
|
.anticon {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
.icon-wrapper {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
menuItemGroup: css`
|
||||||
|
&.menu-item-group-hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
line: css`
|
||||||
|
height: 1px;
|
||||||
|
margin-block: 6px;
|
||||||
|
background-color: ${token.colorSplit};
|
||||||
|
position: absolute;
|
||||||
|
left: -2px;
|
||||||
|
right: -2px;
|
||||||
|
`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
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 [storedCollapsedGroups, setCollapsedGroups] = useAtom(
|
||||||
console.log('SiderMenu', location.pathname);
|
collapsedMenuGroupsAtom
|
||||||
|
);
|
||||||
|
// atomWithStorage falls back to the initial value on JSON parse
|
||||||
|
// errors, but not when the stored value is valid JSON of another
|
||||||
|
// shape — normalize so array methods below can't throw.
|
||||||
|
const collapsedGroups = Array.isArray(storedCollapsedGroups)
|
||||||
|
? storedCollapsedGroups
|
||||||
|
: [];
|
||||||
|
const collapseKeys = useMemo(
|
||||||
|
() => new Set(collapsedGroups),
|
||||||
|
[collapsedGroups]
|
||||||
|
);
|
||||||
|
|
||||||
const dividerStyles = useMemo(() => {
|
const dividerStyles = useMemo(() => {
|
||||||
if (collapsed) {
|
if (collapsed) {
|
||||||
@@ -157,14 +177,11 @@ const SiderMenu: React.FC<SiderMenuProps> = (props) => {
|
|||||||
const handleToggleGroup = (e: any, menuGroup: any) => {
|
const handleToggleGroup = (e: any, menuGroup: any) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
||||||
console.log('handleToggleGroup', menuGroup.key);
|
setCollapsedGroups(
|
||||||
|
collapsedGroups.includes(menuGroup.key)
|
||||||
if (collapseKeys.has(menuGroup.key)) {
|
? collapsedGroups.filter((key) => key !== menuGroup.key)
|
||||||
collapseKeys.delete(menuGroup.key);
|
: [...collapsedGroups, menuGroup.key]
|
||||||
} else {
|
);
|
||||||
collapseKeys.add(menuGroup.key);
|
|
||||||
}
|
|
||||||
setCollapseKeys(new Set(collapseKeys));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const menuItemRender = (menuItem: MenuItem, key: string) => {
|
const menuItemRender = (menuItem: MenuItem, key: string) => {
|
||||||
@@ -220,44 +237,55 @@ const SiderMenu: React.FC<SiderMenuProps> = (props) => {
|
|||||||
'sider-menu-collapsed': collapsed
|
'sider-menu-collapsed': collapsed
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{menuData.map((item: MenuItem, index: number) => (
|
<OverlayScroller
|
||||||
<div key={item.key}>
|
styles={{
|
||||||
{item.children && item.children.length > 0 ? (
|
wrapper: {
|
||||||
<>
|
paddingInline: 0,
|
||||||
<div
|
maxHeight: '100%'
|
||||||
className={cx(styles.groupTitle, {
|
}
|
||||||
'menu-item-group-title-collapsed': collapsed
|
}}
|
||||||
})}
|
>
|
||||||
onClick={(e) => handleToggleGroup(e, item)}
|
<div style={{ paddingRight: 8 }}>
|
||||||
>
|
{menuData.map((item: MenuItem, index: number) => (
|
||||||
{!collapsed ? (
|
<div key={item.key}>
|
||||||
<span className="group-title-text">
|
{item.children && item.children.length > 0 ? (
|
||||||
<span>{item.name}</span>
|
<>
|
||||||
<CaretDownOutlined
|
<div
|
||||||
rotate={collapseKeys.has(item.key) ? -90 : 0}
|
className={cx(styles.groupTitle, {
|
||||||
></CaretDownOutlined>
|
'menu-item-group-title-collapsed': collapsed
|
||||||
</span>
|
})}
|
||||||
) : is_admin ? (
|
onClick={(e) => handleToggleGroup(e, item)}
|
||||||
<span className={styles.line}></span>
|
>
|
||||||
) : null}
|
{!collapsed ? (
|
||||||
</div>
|
<span className="group-title-text">
|
||||||
<div
|
<span>{item.name}</span>
|
||||||
className={cx(styles.menuItemGroup, {
|
<CaretDownOutlined
|
||||||
'menu-item-group-collapsed': collapsed,
|
rotate={collapseKeys.has(item.key) ? -90 : 0}
|
||||||
'menu-item-group-hidden':
|
></CaretDownOutlined>
|
||||||
!collapsed && collapseKeys.has(item.key)
|
</span>
|
||||||
})}
|
) : (
|
||||||
>
|
<span className={styles.line}></span>
|
||||||
{item.children?.map((child: MenuItem) =>
|
)}
|
||||||
menuItemRender(child, child.key)
|
</div>
|
||||||
)}
|
<div
|
||||||
</div>
|
className={cx(styles.menuItemGroup, {
|
||||||
</>
|
'menu-item-group-collapsed': collapsed,
|
||||||
) : (
|
'menu-item-group-hidden':
|
||||||
menuItemRender(item, item.key)
|
!collapsed && collapseKeys.has(item.key)
|
||||||
)}
|
})}
|
||||||
|
>
|
||||||
|
{item.children?.map((child: MenuItem) =>
|
||||||
|
menuItemRender(child, child.key)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
menuItemRender(item, item.key)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
))}
|
</OverlayScroller>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export default {
|
|||||||
'ai.provider.ollama': 'Ollama',
|
'ai.provider.ollama': 'Ollama',
|
||||||
'ai.provider.openai': 'OpenAI',
|
'ai.provider.openai': 'OpenAI',
|
||||||
'ai.provider.openrouter': 'OpenRouter',
|
'ai.provider.openrouter': 'OpenRouter',
|
||||||
'ai.provider.qwen': 'Qwen',
|
'ai.provider.qwen': 'Alibaba Cloud Model Studio',
|
||||||
'ai.provider.spark': 'Spark',
|
'ai.provider.spark': 'Spark',
|
||||||
'ai.provider.stepfun': 'StepFun',
|
'ai.provider.stepfun': 'StepFun',
|
||||||
'ai.provider.together-ai': 'TogetherAI',
|
'ai.provider.together-ai': 'TogetherAI',
|
||||||
|
|||||||
@@ -23,5 +23,7 @@ export default {
|
|||||||
'apikeys.accessScope.inference': 'Inference APIs',
|
'apikeys.accessScope.inference': 'Inference APIs',
|
||||||
'apikeys.access.permissions': 'Access Permissions',
|
'apikeys.access.permissions': 'Access Permissions',
|
||||||
'apikeys.type.auto': 'Auto-generated',
|
'apikeys.type.auto': 'Auto-generated',
|
||||||
'apikeys.type.custom': 'Custom'
|
'apikeys.type.custom': 'Custom',
|
||||||
|
'apikeys.button.ipConfig': 'IP Access Control',
|
||||||
|
'quotaLimits.button.title': 'Quota Limit'
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -23,6 +23,14 @@ 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.flagFormat': 'Flag Format',
|
||||||
|
'backend.form.flagFormat.tips':
|
||||||
|
'The format applied between an option and its value. Leave empty to keep each parameter as entered, without normalizing it.',
|
||||||
|
'backend.form.flagFormat.space': 'Space Separated (--key value)',
|
||||||
|
'backend.form.flagFormat.equal': 'Equal Sign (--key=value)',
|
||||||
|
'backend.form.commonParameters': 'Common Backend 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',
|
||||||
|
|||||||
@@ -37,10 +37,10 @@ export default {
|
|||||||
'Stress test for long-context handling. Evaluates KV cache behavior, memory usage, and backend stability.',
|
'Stress test for long-context handling. Evaluates KV cache behavior, memory usage, and backend stability.',
|
||||||
'benchmark.form.profile.heavy.tips':
|
'benchmark.form.profile.heavy.tips':
|
||||||
'Decode-heavy generation benchmark. Measures sustained decoding speed and output token throughput.',
|
'Decode-heavy generation benchmark. Measures sustained decoding speed and output token throughput.',
|
||||||
'benchmark.table.filter.bygpu': 'Filter by GPU',
|
'benchmark.table.filter.bygpu': 'Search by GPU',
|
||||||
'benchmark.table.filter.bymodel': 'Filter by Model',
|
'benchmark.table.filter.bymodel': 'Search by model',
|
||||||
'benchmark.table.filter.bydataset': 'Filter by Dataset',
|
'benchmark.table.filter.bydataset': 'Filter by Dataset',
|
||||||
'benchmark.table.filter.byProfile': 'Filter by Profile',
|
'benchmark.table.filter.byProfile': 'Filter by profile',
|
||||||
'benchmark.table.avg': 'Avg',
|
'benchmark.table.avg': 'Avg',
|
||||||
'benchmark.table.columnSettings': 'Column Settings',
|
'benchmark.table.columnSettings': 'Column Settings',
|
||||||
'benchmark.detail.summary.results': 'Test Results',
|
'benchmark.detail.summary.results': 'Test Results',
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
export default {
|
||||||
|
'billing.upsell.title': 'Billing is an Enterprise feature',
|
||||||
|
'billing.upsell.subtitle':
|
||||||
|
'Track spend, generate invoices, and enforce budgets across teams. Upgrade to GPUStack Enterprise to manage billing.',
|
||||||
|
'billing.upsell.featuresTitle': 'What you get in Enterprise',
|
||||||
|
'billing.upsell.feature.usage':
|
||||||
|
'See cost breakdowns by organization, user, and model',
|
||||||
|
'billing.upsell.feature.invoices':
|
||||||
|
'Generate invoices and export billing reports',
|
||||||
|
'billing.upsell.feature.budgets':
|
||||||
|
'Set budgets and spending limits with alerts',
|
||||||
|
'billing.upsell.feature.chargeback':
|
||||||
|
'Attribute and charge back usage to teams and projects',
|
||||||
|
'billing.upsell.cta': 'Learn about Enterprise'
|
||||||
|
};
|
||||||
@@ -39,9 +39,11 @@ export default {
|
|||||||
'clusters.workerpool.batchSize.desc':
|
'clusters.workerpool.batchSize.desc':
|
||||||
'Number of workers created simultaneously in the Worker pool',
|
'Number of workers created simultaneously in the Worker pool',
|
||||||
'clusters.create.addworker.tips':
|
'clusters.create.addworker.tips':
|
||||||
'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> 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,8 +69,14 @@ 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.subtitle':
|
||||||
|
'You can select multiple GPU Vendors or none for CPU-only clusters',
|
||||||
'clusters.addworker.checkEnv': 'Check Environment',
|
'clusters.addworker.checkEnv': 'Check Environment',
|
||||||
|
'clusters.addworker.checkEnv.cpuOnlyTips':
|
||||||
|
'Use the following command to verify that the Kubernetes cluster has at least one ready node. You are registering a CPU-only cluster.',
|
||||||
'clusters.addworker.specifyArgs': 'Specify Arguments',
|
'clusters.addworker.specifyArgs': 'Specify Arguments',
|
||||||
|
'clusters.addworker.dtkVersion': 'DTK Version',
|
||||||
'clusters.addworker.runCommand': 'Run Command',
|
'clusters.addworker.runCommand': 'Run Command',
|
||||||
'clusters.addworker.specifyWorkerIP': 'Worker IP',
|
'clusters.addworker.specifyWorkerIP': 'Worker IP',
|
||||||
'clusters.addworker.detectWorkerIP': 'Auto-detect Worker IP',
|
'clusters.addworker.detectWorkerIP': 'Auto-detect Worker IP',
|
||||||
@@ -107,6 +115,8 @@ export default {
|
|||||||
'{count} new workers have been added to the cluster.',
|
'{count} new workers have been added to the cluster.',
|
||||||
'clusters.create.serverUrl': 'GPUStack Server URL',
|
'clusters.create.serverUrl': 'GPUStack Server URL',
|
||||||
'clusters.create.workerConfig': 'Worker Configuration',
|
'clusters.create.workerConfig': 'Worker Configuration',
|
||||||
|
'clusters.edit.k8sOptions.changed.tip':
|
||||||
|
'You have changed the Kubernetes options. Re-run the registration command on the target cluster for the changes to take effect.',
|
||||||
'clusters.addworker.containerName': 'Worker Container Name',
|
'clusters.addworker.containerName': 'Worker Container Name',
|
||||||
'clusters.addworker.containerName.tips':
|
'clusters.addworker.containerName.tips':
|
||||||
'Specify a name for the worker container.',
|
'Specify a name for the worker container.',
|
||||||
@@ -134,7 +144,7 @@ export default {
|
|||||||
'clusters.addworker.theadNotes-02':
|
'clusters.addworker.theadNotes-02':
|
||||||
'T-Head PPU uses the Container Device Interface (CDI) for device injection and requires the <span class="bold-text">/var/run/cdi</span> directory to be available for CDI generation.',
|
'T-Head PPU uses the Container Device Interface (CDI) for device injection and requires the <span class="bold-text">/var/run/cdi</span> directory to be available for CDI generation.',
|
||||||
'clusters.addworker.nvidiaNotes':
|
'clusters.addworker.nvidiaNotes':
|
||||||
'The built-in inference backends in GPUStack v2.1 require <span class="bold-text">CUDA 12.6+</span>. Please ensure your NVIDIA driver version is <span class="bold-text">560</span> or newer.',
|
'The built-in inference backends in GPUStack require <span class="bold-text">CUDA 12.8+</span>. Please ensure your NVIDIA driver version is <span class="bold-text">570</span> or newer.',
|
||||||
'clusters.volume.title': 'Volume Mounts',
|
'clusters.volume.title': 'Volume Mounts',
|
||||||
'clusters.volume.name': 'Volume Name',
|
'clusters.volume.name': 'Volume Name',
|
||||||
'clusters.volume.mountPath': 'Container Path',
|
'clusters.volume.mountPath': 'Container Path',
|
||||||
@@ -158,5 +168,33 @@ 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.systemDefaultContainerRegistry.title': 'Default Container Registry',
|
||||||
|
'clusters.systemDefaultContainerRegistry.tip':
|
||||||
|
'Default registry used to resolve GPUStack images for this cluster. Falls back to the server default when unset.',
|
||||||
|
'clusters.k8sOptions.title': 'Kubernetes Deployment Options',
|
||||||
|
'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.operatorImage.title': 'Operator Image',
|
||||||
|
'clusters.operatorImage.tip':
|
||||||
|
'Override for the GPUStack Operator container image. Leave empty to use the server default.',
|
||||||
|
'clusters.namespace.title': 'Namespace',
|
||||||
|
'clusters.namespace.tip':
|
||||||
|
'Kubernetes namespace the cluster’s manifests render into. Leave empty to use gpustack-system.',
|
||||||
|
'clusters.clusterType.title': 'Cluster Type',
|
||||||
|
'clusters.modelService.title': 'Model Service',
|
||||||
|
'clusters.modelService.tip':
|
||||||
|
'For LLM inference and API serving — e.g. exposing model APIs and token-based services.',
|
||||||
|
'clusters.gpuInstances.title': 'GPU Service',
|
||||||
|
'clusters.gpuInstances.tip':
|
||||||
|
'For on-demand GPU compute — e.g. interactive development, training jobs, or custom environments.',
|
||||||
|
'clusters.gpuInstances.staticAddress': 'GPU Service Static Access Address',
|
||||||
|
'clusters.gpuInstances.staticAddress.tip':
|
||||||
|
'Static address the operator uses to access GPU instances in this cluster (e.g. a LoadBalancer VIP). Optional.'
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -46,22 +46,28 @@ export default {
|
|||||||
'common.button.enabled': 'Enabled',
|
'common.button.enabled': 'Enabled',
|
||||||
'common.button.disabled': 'Disabled',
|
'common.button.disabled': 'Disabled',
|
||||||
'common.button.upgrade': 'Upgrade',
|
'common.button.upgrade': 'Upgrade',
|
||||||
|
'common.enterprise.feature': 'Available in GPUStack Enterprise',
|
||||||
'common.input.holder': 'Please enter',
|
'common.input.holder': 'Please enter',
|
||||||
'common.validate.value': '{name} value is required',
|
'common.validate.value': '{name} value is required',
|
||||||
'common.button.edit': 'Edit',
|
'common.button.edit': 'Edit',
|
||||||
'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.creator': 'Creator',
|
||||||
'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',
|
||||||
'common.search.name.placeholder': 'filter by name',
|
'common.search.name.placeholder': 'filter by name',
|
||||||
'common.search.id.placeholder': 'filter by ID',
|
'common.search.id.placeholder': 'filter by ID',
|
||||||
'common.filter.byId': 'filter by ID',
|
'common.filter.byId': 'filter by ID',
|
||||||
|
'common.filter.byCreator': 'Filter by creator',
|
||||||
'common.table.type': 'Type',
|
'common.table.type': 'Type',
|
||||||
'common.table.default': 'Default Value',
|
'common.table.default': 'Default Value',
|
||||||
'common.copy.success': 'Copied success!',
|
'common.copy.success': 'Copied success!',
|
||||||
@@ -163,6 +169,7 @@ export default {
|
|||||||
'common.time.hour': 'hour',
|
'common.time.hour': 'hour',
|
||||||
'common.time.minute': 'minutes',
|
'common.time.minute': 'minutes',
|
||||||
'common.issue.report': 'Report an issue',
|
'common.issue.report': 'Report an issue',
|
||||||
|
'common.github.star.tooltip': 'Star us on GitHub',
|
||||||
'common.social.discord': 'Join Our Discord',
|
'common.social.discord': 'Join Our Discord',
|
||||||
'common.table.mark': 'Comment',
|
'common.table.mark': 'Comment',
|
||||||
'common.table.rollback.mark': 'Rollback Comment',
|
'common.table.rollback.mark': 'Rollback Comment',
|
||||||
@@ -191,6 +198,8 @@ export default {
|
|||||||
'common.table.user': 'User',
|
'common.table.user': 'User',
|
||||||
'common.settings.instructions': 'Instructions',
|
'common.settings.instructions': 'Instructions',
|
||||||
'common.settings.language': 'Language',
|
'common.settings.language': 'Language',
|
||||||
|
'common.settings.language.tips':
|
||||||
|
'Set the display language for the interface.',
|
||||||
'common.delete.confirm':
|
'common.delete.confirm':
|
||||||
'Are you sure you want to delete the selected {type}?',
|
'Are you sure you want to delete the selected {type}?',
|
||||||
'common.delete.single.confirm':
|
'common.delete.single.confirm':
|
||||||
@@ -223,7 +232,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',
|
||||||
@@ -245,6 +253,11 @@ export default {
|
|||||||
'common.appearance.tips': 'Default follows system preference.',
|
'common.appearance.tips': 'Default follows system preference.',
|
||||||
'common.button.forgotpassword': 'Forgot password?',
|
'common.button.forgotpassword': 'Forgot password?',
|
||||||
'common.appearance.theme': 'Theme',
|
'common.appearance.theme': 'Theme',
|
||||||
|
'common.appearance.description':
|
||||||
|
'Customize how the interface looks on your device.',
|
||||||
|
'common.security': 'Security',
|
||||||
|
'common.security.description':
|
||||||
|
'Manage the password used to sign in to your account.',
|
||||||
'common.page.wentwrong': 'Something went wrong.',
|
'common.page.wentwrong': 'Something went wrong.',
|
||||||
'common.page.refresh.tips':
|
'common.page.refresh.tips':
|
||||||
'The page may need to be updated. Try refreshing it!',
|
'The page may need to be updated. Try refreshing it!',
|
||||||
@@ -255,6 +268,10 @@ export default {
|
|||||||
'common.login.auth': 'Authenticating...',
|
'common.login.auth': 'Authenticating...',
|
||||||
'common.login.auth.failed': 'Authentication failed',
|
'common.login.auth.failed': 'Authentication failed',
|
||||||
'common.login.password': 'Log in with Password',
|
'common.login.password': 'Log in with Password',
|
||||||
|
'common.login.username.holder': 'Please enter username',
|
||||||
|
'common.login.password.holder': 'Please enter password',
|
||||||
|
'common.login.newpassword.holder': 'Please enter new password',
|
||||||
|
'common.login.confirm.holder': 'Please enter password again',
|
||||||
'common.external.login': 'Log in with {type}',
|
'common.external.login': 'Log in with {type}',
|
||||||
'common.sso.noConfig':
|
'common.sso.noConfig':
|
||||||
'Single sign-on is not enabled on this system. Please contact your administrator.',
|
'Single sign-on is not enabled on this system. Please contact your administrator.',
|
||||||
@@ -281,5 +298,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': 'Max {count}'
|
'common.remaining': 'Remaining {count}',
|
||||||
|
'common.max': 'Max {count}',
|
||||||
|
'common.max.count': '{label} Count',
|
||||||
|
'common.validate.group': 'Please complete the {group} configuration',
|
||||||
|
'common.preferences': 'Preferences'
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -13,12 +13,26 @@ export default {
|
|||||||
'gpuservice.template.command.placeholder':
|
'gpuservice.template.command.placeholder':
|
||||||
'Separate arguments with spaces; wrap arguments containing spaces in quotes, e.g.: /bin/bash -c "echo hello world"',
|
'Separate arguments with spaces; wrap arguments containing spaces in quotes, e.g.: /bin/bash -c "echo hello world"',
|
||||||
'gpuservice.template.mountPath': 'Mount Path',
|
'gpuservice.template.mountPath': 'Mount Path',
|
||||||
|
'gpuservice.template.mountPath.tips':
|
||||||
|
'The default mount path for the storage volume when creating an instance from this template. Useful for persisting data that needs to be retained while the instance is running.',
|
||||||
'gpuservice.template.containerDisk': 'Container Disk (GB)',
|
'gpuservice.template.containerDisk': 'Container Disk (GB)',
|
||||||
|
'gpuservice.template.containerDisk.tips':
|
||||||
|
'The size of the container system disk.',
|
||||||
'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 +43,51 @@ 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.delete.tips':
|
||||||
|
'Deleting an SSH Public Key will not revoke access for existing attached Instances. To remove access, edit those Instances separately.',
|
||||||
|
'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',
|
||||||
@@ -45,22 +103,51 @@ export default {
|
|||||||
'gpuservice.instance.templates': 'Instance Templates',
|
'gpuservice.instance.templates': 'Instance Templates',
|
||||||
'gpuservice.instance.section.storage': 'Storage',
|
'gpuservice.instance.section.storage': 'Storage',
|
||||||
'gpuservice.instance.type.required': 'Please select an instance type',
|
'gpuservice.instance.type.required': 'Please select an instance type',
|
||||||
|
'gpuservice.instance.type.noAvailable': 'No instance type available',
|
||||||
'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.cpuCount.max':
|
||||||
|
'Please select at most {count} CPU core(s)',
|
||||||
|
'gpuservice.instance.cpuCount.min':
|
||||||
|
'Please select at least {count} CPU core(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': 'VRAM',
|
||||||
'gpuservice.instance.ram': 'RAM',
|
'gpuservice.instance.ram': 'RAM',
|
||||||
'gpuservice.instance.search.type.placeholder':
|
'gpuservice.instance.os': 'OS',
|
||||||
'Search by name, VRAM, memory or vCPU',
|
'gpuservice.instance.arch': 'Arch',
|
||||||
|
'gpuservice.instance.disk': 'Disk',
|
||||||
|
'gpuservice.table.count': 'Count',
|
||||||
|
'gpuservice.instance.disk.system': 'System Disk',
|
||||||
|
'gpuservice.instance.disk.ephemeral': 'Ephemeral Storage',
|
||||||
|
'gpuservice.instance.disk.persistent': 'Persistent Storage',
|
||||||
|
'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',
|
||||||
@@ -72,11 +159,26 @@ export default {
|
|||||||
'gpuservice.storage.capacity': 'Capacity',
|
'gpuservice.storage.capacity': 'Capacity',
|
||||||
'gpuservice.storage.accessMode': 'Access Mode',
|
'gpuservice.storage.accessMode': 'Access Mode',
|
||||||
'gpuservice.storage.persistent': 'Persistent',
|
'gpuservice.storage.persistent': 'Persistent',
|
||||||
'gpuservice.storage.temporary': 'Temporary',
|
'gpuservice.storage.temporary': 'Ephemeral',
|
||||||
'gpuservice.storage.persistentVolume': 'Persistent Volume',
|
'gpuservice.storage.persistentVolume': 'Persistent',
|
||||||
'gpuservice.storage.persistentVolume.required':
|
'gpuservice.storage.temporary.tips':
|
||||||
'Please select a persistent volume',
|
'Data is cleared when the instance stops.',
|
||||||
'gpuservice.storage.tempCapacity': 'Storage Capacity (GB)',
|
'gpuservice.storage.persistentVolume.tips':
|
||||||
|
'Data persists across instance restarts. Persistent volumes remain intact after instance termination and can be shared by multiple instances.',
|
||||||
|
'gpuservice.storage.persistentVolume.required': 'Please select a storage',
|
||||||
|
'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.",
|
||||||
|
'gpuservice.form.storage.select': 'Select Storage',
|
||||||
|
'gpuservice.creator': 'Creator',
|
||||||
|
'gpuservice.owner.global': 'Global',
|
||||||
|
'gpuservice.template.group.yours': 'Your Templates',
|
||||||
|
'gpuservice.template.group.global': 'Global Templates'
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export default {
|
|||||||
'menu.playground.text2images': 'Image',
|
'menu.playground.text2images': 'Image',
|
||||||
'menu.playground.video': 'Video',
|
'menu.playground.video': 'Video',
|
||||||
'menu.compare': 'Compare',
|
'menu.compare': 'Compare',
|
||||||
'menu.models': 'Models',
|
'menu.models': 'Model Service',
|
||||||
'menu.models.modelList': 'Deploy & Manage',
|
'menu.models.modelList': 'Deploy & Manage',
|
||||||
'menu.models.modelCatalog': 'Catalog',
|
'menu.models.modelCatalog': 'Catalog',
|
||||||
'menu.models.catalog': 'Model Catalog',
|
'menu.models.catalog': 'Model Catalog',
|
||||||
@@ -27,23 +27,28 @@ export default {
|
|||||||
'menu.users': 'Users',
|
'menu.users': 'Users',
|
||||||
'menu.resources.workers': 'Workers',
|
'menu.resources.workers': 'Workers',
|
||||||
'menu.resources.gpus': 'GPUs',
|
'menu.resources.gpus': 'GPUs',
|
||||||
'menu.resources.modelfiles': 'Model Files',
|
'menu.models.modelfiles': 'Model Files',
|
||||||
'menu.accessControl': 'Access Control',
|
'menu.accessControl': 'Access Control',
|
||||||
'menu.accessControl.apikeys': 'API Keys',
|
'menu.accessControl.apikeys': 'API Keys',
|
||||||
'menu.accessControl.users': 'Users',
|
'menu.accessControl.users': 'Users',
|
||||||
'menu.profile': 'Profile',
|
'menu.accessControl.organizations': 'Organizations',
|
||||||
|
'menu.profile': 'Preferences',
|
||||||
'menu.login': 'Login',
|
'menu.login': 'Login',
|
||||||
'menu.usage': 'Usage',
|
'menu.usage': 'Usage',
|
||||||
|
'menu.usage.usage': 'Usage',
|
||||||
|
'menu.billingAndUsage': 'Usage & Billing',
|
||||||
|
'menu.billingAndUsage.usage': 'Usage',
|
||||||
|
'menu.billingAndUsage.billing': 'Billing',
|
||||||
'menu.404': '404',
|
'menu.404': '404',
|
||||||
'menu.clusterManagement': 'Cluster Management',
|
'menu.resources.clusters': 'Clusters',
|
||||||
'menu.clusterManagement.clusters': 'Clusters',
|
'menu.resources.credentials': 'Cloud Credentials',
|
||||||
'menu.clusterManagement.credentials': 'Cloud Credentials',
|
'menu.resources.clusterDetail': 'Cluster Detail',
|
||||||
'menu.clusterManagement.clusterDetail': 'Cluster Detail',
|
'menu.resources.clusterCreate': 'Create Cluster',
|
||||||
'menu.clusterManagement.clusterCreate': 'Create Cluster',
|
'menu.models.backendsList': 'Inference Backends',
|
||||||
'menu.resources.backendsList': 'Inference Backends',
|
|
||||||
'menu.gpuService': 'GPU Service',
|
'menu.gpuService': 'GPU Service',
|
||||||
'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 Adapters',
|
||||||
|
'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'
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -36,9 +36,12 @@ export default {
|
|||||||
'noresult.catalog.nofound': 'No matching models found.',
|
'noresult.catalog.nofound': 'No matching models found.',
|
||||||
'noresult.resources.cluster':
|
'noresult.resources.cluster':
|
||||||
'No clusters available. Add a cluster to get started.',
|
'No clusters available. Add a cluster to get started.',
|
||||||
|
'noresult.resources.k8sCluster':
|
||||||
|
'No clusters available. Add a Kubernetes cluster to get started.',
|
||||||
'noresult.resources.worker':
|
'noresult.resources.worker':
|
||||||
'No workers available. Add a worker to get started.',
|
'No workers available. Add a worker to get started.',
|
||||||
'noresult.resources.gotocluster': 'Create Your First Cluster',
|
'noresult.resources.gotocluster': 'Create Your First Cluster',
|
||||||
|
'noresult.resources.addk8scluster': 'Add a Kubernetes Cluster',
|
||||||
'noresult.resources.gotoworker': 'Add Worker',
|
'noresult.resources.gotoworker': 'Add Worker',
|
||||||
'noresult.benchmark.title': 'No Benchmarks',
|
'noresult.benchmark.title': 'No Benchmarks',
|
||||||
'noresult.benchmark.subTitle': 'No benchmarks have been added yet.',
|
'noresult.benchmark.subTitle': 'No benchmarks have been added yet.',
|
||||||
@@ -64,5 +67,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.'
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
export default {
|
||||||
|
'organizations.upsell.title': 'Organizations are an Enterprise feature',
|
||||||
|
'organizations.upsell.subtitle':
|
||||||
|
'Multi-tenancy lets you isolate users, resources, and quotas across teams. Upgrade to GPUStack Enterprise to manage organizations.',
|
||||||
|
'organizations.upsell.featuresTitle': 'What you get in Enterprise',
|
||||||
|
'organizations.upsell.feature.orgs':
|
||||||
|
'Create organizations to group users and isolate workloads',
|
||||||
|
'organizations.upsell.feature.members':
|
||||||
|
'Manage members and roles per organization',
|
||||||
|
'organizations.upsell.feature.quotas':
|
||||||
|
'Set resource and token quotas per organization',
|
||||||
|
'organizations.upsell.feature.isolation':
|
||||||
|
'Scope API keys, model deployments, and resources per organization',
|
||||||
|
'organizations.upsell.cta': 'Learn about Enterprise'
|
||||||
|
};
|
||||||
@@ -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',
|
||||||
|
|||||||
@@ -17,9 +17,15 @@ export default {
|
|||||||
'usage.table.user.apiKeysUsed': 'API Keys Used',
|
'usage.table.user.apiKeysUsed': 'API Keys Used',
|
||||||
'usage.table.lastActive': 'Last Active',
|
'usage.table.lastActive': 'Last Active',
|
||||||
'usage.filter.granularity': 'Granularity',
|
'usage.filter.granularity': 'Granularity',
|
||||||
|
'usage.filter.granularity.hour': 'Hour',
|
||||||
'usage.filter.granularity.day': 'Day',
|
'usage.filter.granularity.day': 'Day',
|
||||||
'usage.filter.granularity.week': 'Week',
|
'usage.filter.granularity.week': 'Week',
|
||||||
'usage.filter.granularity.month': 'Month',
|
'usage.filter.granularity.month': 'Month',
|
||||||
|
'usage.tabs.summary': 'Summary',
|
||||||
|
'usage.tabs.tokens': 'Tokens',
|
||||||
|
'usage.tabs.gpuInstances': 'GPU Instances',
|
||||||
|
'usage.tabs.storage': 'Storage',
|
||||||
|
'usage.tabs.resourceEvents': 'Resource Events',
|
||||||
'usage.tabs.models': 'Models',
|
'usage.tabs.models': 'Models',
|
||||||
'usage.tabs.apikeys': 'API Keys',
|
'usage.tabs.apikeys': 'API Keys',
|
||||||
'usage.tabs.users': 'User',
|
'usage.tabs.users': 'User',
|
||||||
@@ -32,5 +38,70 @@ export default {
|
|||||||
'usage.chart.cached': 'Cached',
|
'usage.chart.cached': 'Cached',
|
||||||
'usage.chart.uncached': 'Uncached',
|
'usage.chart.uncached': 'Uncached',
|
||||||
'usage.chart.inputTokensCached': 'Input Tokens (Cached/Uncached)',
|
'usage.chart.inputTokensCached': 'Input Tokens (Cached/Uncached)',
|
||||||
'usage.table.inputTokensCached': 'Input Tokens Cached'
|
'usage.table.inputTokensCached': 'Input Tokens Cached',
|
||||||
|
|
||||||
|
// --- Resource usage: shared metrics & units ---
|
||||||
|
'usage.metric.tokens': 'Tokens',
|
||||||
|
'usage.metric.input': 'Input',
|
||||||
|
'usage.metric.output': 'Output',
|
||||||
|
'usage.metric.gpuHours': 'GPU Hours',
|
||||||
|
'usage.metric.instanceHours': 'Instance Hours',
|
||||||
|
'usage.metric.gbDays': 'GB-Days',
|
||||||
|
'usage.metric.gbHours': 'GB-Hours',
|
||||||
|
'usage.metric.activeUsers': 'Active Users',
|
||||||
|
'usage.metric.activeInstances': 'Active Instances',
|
||||||
|
'usage.metric.activeStorage': 'Active Storage',
|
||||||
|
'usage.metric.activeVolumes': 'Active Volumes',
|
||||||
|
'usage.metric.storageTypes': 'Storage Types',
|
||||||
|
'usage.metric.gpuHours.tip':
|
||||||
|
'Instance running time weighted by GPU count: an instance with N GPUs running for H hours counts as N × H GPU-hours. Equal to Instance Hours when every instance uses a single GPU.',
|
||||||
|
'usage.metric.instanceHours.tip':
|
||||||
|
'Total running time summed across all instances, regardless of how many GPUs each uses. One instance running for 2 hours = 2 instance-hours.',
|
||||||
|
'usage.metric.gbDays.tip':
|
||||||
|
'Storage capacity integrated over time, in GB × days: 10 GB kept for 5 days = 50 GB-days. (= GB-Hours ÷ 24)',
|
||||||
|
'usage.metric.gbHours.tip':
|
||||||
|
'Storage capacity integrated over time, in GB × hours: 10 GB kept for 5 hours = 50 GB-hours.',
|
||||||
|
|
||||||
|
// --- Resource usage: common table / labels ---
|
||||||
|
'usage.common.noData': 'No data',
|
||||||
|
'usage.common.unknown': 'unknown',
|
||||||
|
'usage.table.date': 'Date',
|
||||||
|
'usage.table.name': 'Name',
|
||||||
|
'usage.table.user': 'User',
|
||||||
|
'usage.table.users': 'Users',
|
||||||
|
'usage.table.type': 'Type',
|
||||||
|
'usage.table.instance': 'Instance',
|
||||||
|
'usage.table.instanceType': 'Instance Type',
|
||||||
|
'usage.table.instanceTypes': 'Instance Types',
|
||||||
|
'usage.table.instances': 'Instances',
|
||||||
|
'usage.table.capacity': 'Capacity',
|
||||||
|
'usage.export.tableNamed': 'Export Table Data — {name}',
|
||||||
|
|
||||||
|
// --- Summary tab ---
|
||||||
|
'usage.summary.compute': 'Compute',
|
||||||
|
'usage.summary.tokensOverTime': 'Tokens over time',
|
||||||
|
'usage.summary.gpuHoursOverTime': 'GPU Hours over time',
|
||||||
|
'usage.summary.gbDaysOverTime': 'GB-Days over time',
|
||||||
|
|
||||||
|
// --- GPU Instances / Storage filters ---
|
||||||
|
'usage.filter.instance': 'Filter by instance',
|
||||||
|
'usage.filter.storage': 'Filter by storage',
|
||||||
|
|
||||||
|
// --- Resource events ---
|
||||||
|
'usage.events.resourceType': 'Resource type',
|
||||||
|
'usage.events.eventType': 'Event type',
|
||||||
|
'usage.events.resourceName': 'Filter by name',
|
||||||
|
'usage.events.col.time': 'Time',
|
||||||
|
'usage.events.col.resource': 'Resource',
|
||||||
|
'usage.events.col.event': 'Event',
|
||||||
|
'usage.events.col.message': 'Message',
|
||||||
|
'usage.events.resource.gpuInstance': 'GPU Instance',
|
||||||
|
'usage.events.resource.cpuInstance': 'CPU Instance',
|
||||||
|
'usage.events.type.created': 'Created',
|
||||||
|
'usage.events.type.deleted': 'Deleted',
|
||||||
|
'usage.events.type.started': 'Started',
|
||||||
|
'usage.events.type.stopped': 'Stopped',
|
||||||
|
'usage.events.type.updated': 'Updated',
|
||||||
|
'usage.events.type.attached': 'Attached',
|
||||||
|
'usage.events.type.detached': 'Detached'
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ export default {
|
|||||||
'users.password.modify.title': 'Modify Password',
|
'users.password.modify.title': 'Modify Password',
|
||||||
'users.password.modify.description':
|
'users.password.modify.description':
|
||||||
"For your account's security, please change your initial password.",
|
"For your account's security, please change your initial password.",
|
||||||
|
'users.password.modify.tips':
|
||||||
|
'Regularly updating your password helps keep your account secure.',
|
||||||
'users.password.confirm': 'Confirm New Password',
|
'users.password.confirm': 'Confirm New Password',
|
||||||
'users.password.confirm.empty': 'Please confirm the new password.',
|
'users.password.confirm.empty': 'Please confirm the new password.',
|
||||||
'users.password.confirm.error': 'The two passwords entered do not match.',
|
'users.password.confirm.error': 'The two passwords entered do not match.',
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ export default {
|
|||||||
'vendor.hygon': 'Hygon',
|
'vendor.hygon': 'Hygon',
|
||||||
'vendor.moorthreads': 'Moore Threads',
|
'vendor.moorthreads': 'Moore Threads',
|
||||||
'vendor.iluvatar': 'Iluvatar',
|
'vendor.iluvatar': 'Iluvatar',
|
||||||
'vendor.metax': 'Metax',
|
'vendor.metax': 'MetaX',
|
||||||
'vendor.cambricon': 'Cambricon',
|
'vendor.cambricon': 'Cambricon',
|
||||||
'vendor.thead': 'T-Head PPU'
|
'vendor.thead': 'T-Head PPU'
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export default {
|
|||||||
'ai.provider.ollama': 'Ollama',
|
'ai.provider.ollama': 'Ollama',
|
||||||
'ai.provider.openai': 'OpenAI',
|
'ai.provider.openai': 'OpenAI',
|
||||||
'ai.provider.openrouter': 'OpenRouter',
|
'ai.provider.openrouter': 'OpenRouter',
|
||||||
'ai.provider.qwen': 'Qwen',
|
'ai.provider.qwen': 'Alibaba Cloud Model Studio',
|
||||||
'ai.provider.spark': 'Spark',
|
'ai.provider.spark': 'Spark',
|
||||||
'ai.provider.stepfun': 'StepFun',
|
'ai.provider.stepfun': 'StepFun',
|
||||||
'ai.provider.together-ai': 'TogetherAI',
|
'ai.provider.together-ai': 'TogetherAI',
|
||||||
|
|||||||
@@ -23,7 +23,9 @@ export default {
|
|||||||
'apikeys.accessScope.inference': 'Inference APIs',
|
'apikeys.accessScope.inference': 'Inference APIs',
|
||||||
'apikeys.access.permissions': 'Access Permissions',
|
'apikeys.access.permissions': 'Access Permissions',
|
||||||
'apikeys.type.auto': 'Auto-generated',
|
'apikeys.type.auto': 'Auto-generated',
|
||||||
'apikeys.type.custom': 'Custom'
|
'apikeys.type.custom': 'Custom',
|
||||||
|
'apikeys.button.ipConfig': 'IP Access Control',
|
||||||
|
'quotaLimits.button.title': 'Quota Limit'
|
||||||
};
|
};
|
||||||
|
|
||||||
// ========== To-Do: Translate Keys (Remove After Translation) ==========
|
// ========== To-Do: Translate Keys (Remove After Translation) ==========
|
||||||
|
|||||||
@@ -23,6 +23,14 @@ 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.flagFormat': 'フラグ形式',
|
||||||
|
'backend.form.flagFormat.tips':
|
||||||
|
'オプションとその値を連結する形式。空欄の場合は各パラメータを入力されたまま保持し、形式を統一しません。',
|
||||||
|
'backend.form.flagFormat.space': 'スペース区切り (--key value)',
|
||||||
|
'backend.form.flagFormat.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',
|
||||||
|
|||||||
@@ -37,8 +37,8 @@ export default {
|
|||||||
'Stress test for long-context handling. Evaluates KV cache behavior, memory usage, and backend stability.',
|
'Stress test for long-context handling. Evaluates KV cache behavior, memory usage, and backend stability.',
|
||||||
'benchmark.form.profile.heavy.tips':
|
'benchmark.form.profile.heavy.tips':
|
||||||
'Decode-heavy generation benchmark. Measures sustained decoding speed and output token throughput.',
|
'Decode-heavy generation benchmark. Measures sustained decoding speed and output token throughput.',
|
||||||
'benchmark.table.filter.bygpu': 'Filter by GPU',
|
'benchmark.table.filter.bygpu': 'GPU 検索',
|
||||||
'benchmark.table.filter.bymodel': 'Filter by Model',
|
'benchmark.table.filter.bymodel': 'モデル検索',
|
||||||
'benchmark.table.filter.bydataset': 'Filter by Dataset',
|
'benchmark.table.filter.bydataset': 'Filter by Dataset',
|
||||||
'benchmark.table.filter.byProfile': 'Filter by Profile',
|
'benchmark.table.filter.byProfile': 'Filter by Profile',
|
||||||
'benchmark.table.avg': 'Avg',
|
'benchmark.table.avg': 'Avg',
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
export default {
|
||||||
|
'billing.upsell.title': 'Billing is an Enterprise feature',
|
||||||
|
'billing.upsell.subtitle':
|
||||||
|
'Track spend, generate invoices, and enforce budgets across teams. Upgrade to GPUStack Enterprise to manage billing.',
|
||||||
|
'billing.upsell.featuresTitle': 'What you get in Enterprise',
|
||||||
|
'billing.upsell.feature.usage':
|
||||||
|
'See cost breakdowns by organization, user, and model',
|
||||||
|
'billing.upsell.feature.invoices':
|
||||||
|
'Generate invoices and export billing reports',
|
||||||
|
'billing.upsell.feature.budgets':
|
||||||
|
'Set budgets and spending limits with alerts',
|
||||||
|
'billing.upsell.feature.chargeback':
|
||||||
|
'Attribute and charge back usage to teams and projects',
|
||||||
|
'billing.upsell.cta': 'Learn about Enterprise'
|
||||||
|
};
|
||||||
@@ -39,9 +39,11 @@ export default {
|
|||||||
'clusters.workerpool.batchSize.desc':
|
'clusters.workerpool.batchSize.desc':
|
||||||
'Number of workers created simultaneously in the Worker pool',
|
'Number of workers created simultaneously in the Worker pool',
|
||||||
'clusters.create.addworker.tips':
|
'clusters.create.addworker.tips':
|
||||||
'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> 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,8 +69,14 @@ 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.subtitle':
|
||||||
|
'複数の GPU ベンダーを選択するか、CPU クラスター専用の場合は選択不要です',
|
||||||
'clusters.addworker.checkEnv': 'Check Environment',
|
'clusters.addworker.checkEnv': 'Check Environment',
|
||||||
|
'clusters.addworker.checkEnv.cpuOnlyTips':
|
||||||
|
'以下のコマンドを使用して、Kubernetes クラスターに少なくとも 1 つのレディーノードがあることを確認してください。CPU クラスターを登録しています。',
|
||||||
'clusters.addworker.specifyArgs': 'Specify Arguments',
|
'clusters.addworker.specifyArgs': 'Specify Arguments',
|
||||||
|
'clusters.addworker.dtkVersion': 'DTK バージョン',
|
||||||
'clusters.addworker.runCommand': 'Run Command',
|
'clusters.addworker.runCommand': 'Run Command',
|
||||||
'clusters.addworker.specifyWorkerIP': 'Worker IP',
|
'clusters.addworker.specifyWorkerIP': 'Worker IP',
|
||||||
'clusters.addworker.detectWorkerIP': 'Worker IP を自動検出',
|
'clusters.addworker.detectWorkerIP': 'Worker IP を自動検出',
|
||||||
@@ -107,6 +115,8 @@ export default {
|
|||||||
'{count} new workers have been added to the cluster.',
|
'{count} new workers have been added to the cluster.',
|
||||||
'clusters.create.serverUrl': 'GPUStack Server URL',
|
'clusters.create.serverUrl': 'GPUStack Server URL',
|
||||||
'clusters.create.workerConfig': 'Worker Configuration',
|
'clusters.create.workerConfig': 'Worker Configuration',
|
||||||
|
'clusters.edit.k8sOptions.changed.tip':
|
||||||
|
'Kubernetes オプションを変更しました。変更を有効にするには、対象クラスターで登録コマンドを再実行してください。',
|
||||||
'clusters.addworker.containerName': 'Worker Container Name',
|
'clusters.addworker.containerName': 'Worker Container Name',
|
||||||
'clusters.addworker.containerName.tips':
|
'clusters.addworker.containerName.tips':
|
||||||
'Specify a name for the worker container.',
|
'Specify a name for the worker container.',
|
||||||
@@ -134,7 +144,7 @@ export default {
|
|||||||
'clusters.addworker.theadNotes-02':
|
'clusters.addworker.theadNotes-02':
|
||||||
'T-Head PPU uses the Container Device Interface (CDI) for device injection and requires the <span class="bold-text">/var/run/cdi</span> directory to be available for CDI generation.',
|
'T-Head PPU uses the Container Device Interface (CDI) for device injection and requires the <span class="bold-text">/var/run/cdi</span> directory to be available for CDI generation.',
|
||||||
'clusters.addworker.nvidiaNotes':
|
'clusters.addworker.nvidiaNotes':
|
||||||
'The built-in inference backends in GPUStack v2.1 require <span class="bold-text">CUDA 12.6+</span>. Please ensure your NVIDIA driver version is <span class="bold-text">560</span> or newer.',
|
'The built-in inference backends in GPUStack require <span class="bold-text">CUDA 12.8+</span>. Please ensure your NVIDIA driver version is <span class="bold-text">570</span> or newer.',
|
||||||
'clusters.volume.title': 'Volume Mounts',
|
'clusters.volume.title': 'Volume Mounts',
|
||||||
'clusters.volume.name': 'Volume Name',
|
'clusters.volume.name': 'Volume Name',
|
||||||
'clusters.volume.mountPath': 'Container Path',
|
'clusters.volume.mountPath': 'Container Path',
|
||||||
@@ -158,7 +168,35 @@ 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.systemDefaultContainerRegistry.title': 'Default Container Registry',
|
||||||
|
'clusters.systemDefaultContainerRegistry.tip':
|
||||||
|
'Default registry used to resolve GPUStack images for this cluster. Falls back to the server default when unset.',
|
||||||
|
'clusters.k8sOptions.title': 'Kubernetes Deployment Options',
|
||||||
|
'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.operatorImage.title': 'Operator Image',
|
||||||
|
'clusters.operatorImage.tip':
|
||||||
|
'Override for the GPUStack Operator container image. Leave empty to use the server default.',
|
||||||
|
'clusters.namespace.title': 'Namespace',
|
||||||
|
'clusters.namespace.tip':
|
||||||
|
'Kubernetes namespace the cluster’s manifests render into. Leave empty to use gpustack-system.',
|
||||||
|
'clusters.clusterType.title': 'Cluster Type',
|
||||||
|
'clusters.modelService.title': 'Model Service',
|
||||||
|
'clusters.modelService.tip':
|
||||||
|
'For LLM inference and API serving — e.g. exposing model APIs and token-based services.',
|
||||||
|
'clusters.gpuInstances.title': 'GPU Service',
|
||||||
|
'clusters.gpuInstances.tip':
|
||||||
|
'For on-demand GPU compute — e.g. interactive development, training jobs, or custom environments.',
|
||||||
|
'clusters.gpuInstances.staticAddress': 'GPU Service Static Access Address',
|
||||||
|
'clusters.gpuInstances.staticAddress.tip':
|
||||||
|
'Static address the operator uses to access GPU instances in this cluster (e.g. a LoadBalancer VIP). Optional.'
|
||||||
};
|
};
|
||||||
|
|
||||||
// ========== To-Do: Translate Keys (Remove After Translation) ==========
|
// ========== To-Do: Translate Keys (Remove After Translation) ==========
|
||||||
|
|||||||
@@ -46,22 +46,28 @@ export default {
|
|||||||
'common.button.enabled': '有効',
|
'common.button.enabled': '有効',
|
||||||
'common.button.disabled': '無効',
|
'common.button.disabled': '無効',
|
||||||
'common.button.upgrade': 'アップグレード',
|
'common.button.upgrade': 'アップグレード',
|
||||||
|
'common.enterprise.feature': 'Available in GPUStack Enterprise',
|
||||||
'common.input.holder': '入力してください',
|
'common.input.holder': '入力してください',
|
||||||
'common.validate.value': '{name} の値は必須です',
|
'common.validate.value': '{name} の値は必須です',
|
||||||
'common.button.edit': '編集',
|
'common.button.edit': '編集',
|
||||||
'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.creator': '作成者',
|
||||||
'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} 名称',
|
||||||
'common.search.name.placeholder': '名前でフィルタ',
|
'common.search.name.placeholder': '名前でフィルタ',
|
||||||
'common.search.id.placeholder': 'IDでフィルタ',
|
'common.search.id.placeholder': 'IDでフィルタ',
|
||||||
'common.filter.byId': 'IDでフィルタ',
|
'common.filter.byId': 'IDでフィルタ',
|
||||||
|
'common.filter.byCreator': '作成者でフィルタ',
|
||||||
'common.table.type': 'タイプ',
|
'common.table.type': 'タイプ',
|
||||||
'common.table.default': 'デフォルト値',
|
'common.table.default': 'デフォルト値',
|
||||||
'common.copy.success': 'コピー成功!',
|
'common.copy.success': 'コピー成功!',
|
||||||
@@ -164,6 +170,7 @@ export default {
|
|||||||
'common.time.hour': '時間',
|
'common.time.hour': '時間',
|
||||||
'common.time.minute': '分',
|
'common.time.minute': '分',
|
||||||
'common.issue.report': '問題を報告',
|
'common.issue.report': '問題を報告',
|
||||||
|
'common.github.star.tooltip': 'GitHub でスターをつける',
|
||||||
'common.social.discord': 'Discordに参加',
|
'common.social.discord': 'Discordに参加',
|
||||||
'common.table.mark': 'コメント',
|
'common.table.mark': 'コメント',
|
||||||
'common.table.rollback.mark': 'ロールバックコメント',
|
'common.table.rollback.mark': 'ロールバックコメント',
|
||||||
@@ -192,6 +199,7 @@ export default {
|
|||||||
'common.table.user': 'ユーザー',
|
'common.table.user': 'ユーザー',
|
||||||
'common.settings.instructions': '手順',
|
'common.settings.instructions': '手順',
|
||||||
'common.settings.language': '言語',
|
'common.settings.language': '言語',
|
||||||
|
'common.settings.language.tips': 'インターフェースの表示言語を設定します。',
|
||||||
'common.delete.confirm': '選択した {type} を削除してもよろしいですか?',
|
'common.delete.confirm': '選択した {type} を削除してもよろしいですか?',
|
||||||
'common.delete.single.confirm':
|
'common.delete.single.confirm':
|
||||||
'<span style="font-size: 13px;font-weight: 700">{name}</span> を削除してもよろしいですか?',
|
'<span style="font-size: 13px;font-weight: 700">{name}</span> を削除してもよろしいですか?',
|
||||||
@@ -223,7 +231,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': 'なし',
|
||||||
@@ -245,6 +252,11 @@ export default {
|
|||||||
'common.appearance.tips': 'Default follows system preference.',
|
'common.appearance.tips': 'Default follows system preference.',
|
||||||
'common.button.forgotpassword': 'Forgot password?',
|
'common.button.forgotpassword': 'Forgot password?',
|
||||||
'common.appearance.theme': 'Theme',
|
'common.appearance.theme': 'Theme',
|
||||||
|
'common.appearance.description':
|
||||||
|
'デバイス上でのインターフェースの表示をカスタマイズします。',
|
||||||
|
'common.security': 'セキュリティ',
|
||||||
|
'common.security.description':
|
||||||
|
'アカウントへのログインに使用するパスワードを管理します。',
|
||||||
'common.page.wentwrong': 'Something went wrong.',
|
'common.page.wentwrong': 'Something went wrong.',
|
||||||
'common.page.refresh.tips':
|
'common.page.refresh.tips':
|
||||||
'The page may need to be updated. Try refreshing it!',
|
'The page may need to be updated. Try refreshing it!',
|
||||||
@@ -255,6 +267,10 @@ export default {
|
|||||||
'common.login.auth': 'Authenticating...',
|
'common.login.auth': 'Authenticating...',
|
||||||
'common.login.auth.failed': 'Authentication failed',
|
'common.login.auth.failed': 'Authentication failed',
|
||||||
'common.login.password': 'Log in with Password',
|
'common.login.password': 'Log in with Password',
|
||||||
|
'common.login.username.holder': 'Please enter username',
|
||||||
|
'common.login.password.holder': 'Please enter password',
|
||||||
|
'common.login.newpassword.holder': 'Please enter new password',
|
||||||
|
'common.login.confirm.holder': 'Please enter password again',
|
||||||
'common.external.login': 'Log in with {type}',
|
'common.external.login': 'Log in with {type}',
|
||||||
'common.sso.noConfig':
|
'common.sso.noConfig':
|
||||||
'Single sign-on is not enabled on this system. Please contact your administrator.',
|
'Single sign-on is not enabled on this system. Please contact your administrator.',
|
||||||
@@ -281,7 +297,11 @@ 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.max.count': '{label} 数',
|
||||||
|
'common.validate.group': 'Please complete the {group} configuration',
|
||||||
|
'common.preferences': 'Preferences'
|
||||||
};
|
};
|
||||||
|
|
||||||
// ========== 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',
|
||||||
|
|||||||
@@ -13,11 +13,25 @@ export default {
|
|||||||
'gpuservice.template.command.placeholder':
|
'gpuservice.template.command.placeholder':
|
||||||
'引数はスペースで区切り、スペースを含む引数は引用符で囲んでください。例:/bin/bash -c "echo hello world"',
|
'引数はスペースで区切り、スペースを含む引数は引用符で囲んでください。例:/bin/bash -c "echo hello world"',
|
||||||
'gpuservice.template.mountPath': 'マウントパス',
|
'gpuservice.template.mountPath': 'マウントパス',
|
||||||
|
'gpuservice.template.mountPath.tips':
|
||||||
|
'このテンプレートからインスタンスを作成する際に、ストレージボリュームがデフォルトでマウントされるパスです。インスタンスの実行中に保持する必要があるデータの永続化に使用できます。',
|
||||||
'gpuservice.template.containerDisk': 'コンテナディスク (GB)',
|
'gpuservice.template.containerDisk': 'コンテナディスク (GB)',
|
||||||
|
'gpuservice.template.containerDisk.tips':
|
||||||
|
'コンテナシステムディスクのサイズです。',
|
||||||
'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 +41,52 @@ 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.delete.tips':
|
||||||
|
'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 インスタンス',
|
||||||
@@ -43,22 +102,51 @@ export default {
|
|||||||
'gpuservice.instance.templates': 'インスタンステンプレート',
|
'gpuservice.instance.templates': 'インスタンステンプレート',
|
||||||
'gpuservice.instance.section.storage': 'ストレージボリューム',
|
'gpuservice.instance.section.storage': 'ストレージボリューム',
|
||||||
'gpuservice.instance.type.required': 'インスタンスタイプを選択してください',
|
'gpuservice.instance.type.required': 'インスタンスタイプを選択してください',
|
||||||
|
'gpuservice.instance.type.noAvailable':
|
||||||
|
'利用可能なインスタンスタイプがありません',
|
||||||
'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.cpuCount.max':
|
||||||
|
'最大 {count} 個の CPU コアを選択してください',
|
||||||
|
'gpuservice.instance.cpuCount.min':
|
||||||
|
'少なくとも {count} 個の CPU コアを選択してください',
|
||||||
|
'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': 'VRAM',
|
||||||
'gpuservice.instance.ram': 'RAM',
|
'gpuservice.instance.ram': 'RAM',
|
||||||
'gpuservice.instance.search.type.placeholder':
|
'gpuservice.instance.os': 'OS',
|
||||||
'名前、VRAM、メモリまたは vCPU で検索',
|
'gpuservice.instance.arch': 'アーキテクチャ',
|
||||||
|
'gpuservice.instance.disk': 'ディスク',
|
||||||
|
'gpuservice.table.count': '数量',
|
||||||
|
'gpuservice.instance.disk.system': 'システムディスク',
|
||||||
|
'gpuservice.instance.disk.ephemeral': '一時ストレージ',
|
||||||
|
'gpuservice.instance.disk.persistent': '永続ストレージ',
|
||||||
|
'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': 'ストレージを編集',
|
||||||
@@ -71,10 +159,26 @@ export default {
|
|||||||
'gpuservice.storage.accessMode': 'アクセスモード',
|
'gpuservice.storage.accessMode': 'アクセスモード',
|
||||||
'gpuservice.storage.persistent': '永続',
|
'gpuservice.storage.persistent': '永続',
|
||||||
'gpuservice.storage.temporary': '一時',
|
'gpuservice.storage.temporary': '一時',
|
||||||
'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 instance restarts. Persistent volumes remain intact after instance termination and can be shared by multiple instances.',
|
||||||
|
'gpuservice.form.storage.select': 'ストレージを選択',
|
||||||
|
'gpuservice.creator': '作成者',
|
||||||
|
'gpuservice.owner.global': 'グローバル',
|
||||||
|
'gpuservice.template.group.yours': '自分のテンプレート',
|
||||||
|
'gpuservice.template.group.global': 'グローバルテンプレート'
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export default {
|
|||||||
'menu.playground.text2images': '画像生成',
|
'menu.playground.text2images': '画像生成',
|
||||||
'menu.playground.video': '動画',
|
'menu.playground.video': '動画',
|
||||||
'menu.compare': '比較',
|
'menu.compare': '比較',
|
||||||
'menu.models': 'モデル',
|
'menu.models': 'モデルサービス',
|
||||||
'menu.models.modelList': 'デプロイと管理',
|
'menu.models.modelList': 'デプロイと管理',
|
||||||
'menu.models.modelCatalog': 'カタログ',
|
'menu.models.modelCatalog': 'カタログ',
|
||||||
'menu.models.catalog': 'モデルカタログ',
|
'menu.models.catalog': 'モデルカタログ',
|
||||||
@@ -23,28 +23,33 @@ export default {
|
|||||||
'menu.resources': 'リソース',
|
'menu.resources': 'リソース',
|
||||||
'menu.apikeys': 'APIキー',
|
'menu.apikeys': 'APIキー',
|
||||||
'menu.users': 'ユーザー',
|
'menu.users': 'ユーザー',
|
||||||
'menu.profile': 'プロフィール',
|
'menu.profile': 'Preferences',
|
||||||
'menu.login': 'ログイン',
|
'menu.login': 'ログイン',
|
||||||
'menu.usage': '使用状況',
|
'menu.usage': '使用状況',
|
||||||
|
'menu.usage.usage': '使用状況',
|
||||||
|
'menu.billingAndUsage': '使用状況と請求',
|
||||||
|
'menu.billingAndUsage.usage': '使用状況',
|
||||||
|
'menu.billingAndUsage.billing': '請求',
|
||||||
'menu.404': '404',
|
'menu.404': '404',
|
||||||
'menu.settings': 'Settings',
|
'menu.settings': 'Settings',
|
||||||
'menu.resources.workers': 'Workers',
|
'menu.resources.workers': 'Workers',
|
||||||
'menu.resources.gpus': 'GPUs',
|
'menu.resources.gpus': 'GPUs',
|
||||||
'menu.resources.modelfiles': 'Model Files',
|
'menu.models.modelfiles': 'Model Files',
|
||||||
'menu.accessControl': 'Access Control',
|
'menu.accessControl': 'Access Control',
|
||||||
'menu.accessControl.apikeys': 'API Keys',
|
'menu.accessControl.apikeys': 'API Keys',
|
||||||
'menu.accessControl.users': 'Users',
|
'menu.accessControl.users': 'Users',
|
||||||
'menu.clusterManagement': 'Cluster Management',
|
'menu.accessControl.organizations': 'Organizations',
|
||||||
'menu.clusterManagement.clusters': 'Clusters',
|
'menu.resources.clusters': 'Clusters',
|
||||||
'menu.clusterManagement.credentials': 'Cloud Credentials',
|
'menu.resources.credentials': 'Cloud Credentials',
|
||||||
'menu.models.userModels': 'My Models',
|
'menu.models.userModels': 'My Models',
|
||||||
'menu.clusterManagement.clusterDetail': 'Cluster Detail',
|
'menu.resources.clusterDetail': 'Cluster Detail',
|
||||||
'menu.clusterManagement.clusterCreate': 'Create Cluster',
|
'menu.resources.clusterCreate': 'Create Cluster',
|
||||||
'menu.resources.backendsList': 'Inference Backends',
|
'menu.models.backendsList': 'Inference Backends',
|
||||||
'menu.gpuService': 'GPU Service',
|
'menu.gpuService': 'GPU Service',
|
||||||
'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'
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -52,7 +57,7 @@ export default {
|
|||||||
// 1. 'menu.models.deployment': 'Deployment',
|
// 1. 'menu.models.deployment': 'Deployment',
|
||||||
// 2. 'menu.resources.workers': 'Workers',
|
// 2. 'menu.resources.workers': 'Workers',
|
||||||
// 3. 'menu.resources.gpus': 'GPUs',
|
// 3. 'menu.resources.gpus': 'GPUs',
|
||||||
// 4. 'menu.resources.modelfiles': 'Model Files',
|
// 4. 'menu.models.modelfiles': 'Model Files',
|
||||||
// 5. 'menu.accessControl': 'Access Control',
|
// 5. 'menu.accessControl': 'Access Control',
|
||||||
// 6. 'menu.accessControl.apikeys': 'API Keys',
|
// 6. 'menu.accessControl.apikeys': 'API Keys',
|
||||||
// 7. 'menu.accessControl.users': 'Users',
|
// 7. 'menu.accessControl.users': 'Users',
|
||||||
@@ -62,7 +67,7 @@ export default {
|
|||||||
// 11. 'menu.models.userModels': 'My Models'
|
// 11. 'menu.models.userModels': 'My Models'
|
||||||
// 12. 'menu.clusterManagement.clusterDetail': 'Cluster Detail',
|
// 12. 'menu.clusterManagement.clusterDetail': 'Cluster Detail',
|
||||||
// 13. 'menu.clusterManagement.clusterCreate': 'Create Cluster',
|
// 13. 'menu.clusterManagement.clusterCreate': 'Create Cluster',
|
||||||
// 14. 'menu.resources.backendsList': 'Inference Backends',
|
// 14. 'menu.models.backendsList': 'Inference Backends',
|
||||||
// 15. 'menu.models.benchmark': 'Benchmarks',
|
// 15. 'menu.models.benchmark': 'Benchmarks',
|
||||||
// 15. 'menu.models.provider': 'Provider',
|
// 15. 'menu.models.provider': 'Provider',
|
||||||
// 15. 'menu.models.providers': 'Provider',
|
// 15. 'menu.models.providers': 'Provider',
|
||||||
|
|||||||
@@ -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 Adapters',
|
||||||
|
'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 ==========
|
||||||
|
|||||||
@@ -36,9 +36,12 @@ export default {
|
|||||||
'noresult.catalog.nofound': 'No matching models found.',
|
'noresult.catalog.nofound': 'No matching models found.',
|
||||||
'noresult.resources.cluster':
|
'noresult.resources.cluster':
|
||||||
'No clusters available. Add a cluster to get started.',
|
'No clusters available. Add a cluster to get started.',
|
||||||
|
'noresult.resources.k8sCluster':
|
||||||
|
'No clusters available. Add a Kubernetes cluster to get started.',
|
||||||
'noresult.resources.worker':
|
'noresult.resources.worker':
|
||||||
'No workers available. Add a worker to get started.',
|
'No workers available. Add a worker to get started.',
|
||||||
'noresult.resources.gotocluster': 'Create Your First Cluster',
|
'noresult.resources.gotocluster': 'Create Your First Cluster',
|
||||||
|
'noresult.resources.addk8scluster': 'Add a Kubernetes Cluster',
|
||||||
'noresult.resources.gotoworker': 'Add Worker',
|
'noresult.resources.gotoworker': 'Add Worker',
|
||||||
'noresult.benchmark.title': 'No Benchmarks',
|
'noresult.benchmark.title': 'No Benchmarks',
|
||||||
'noresult.benchmark.subTitle': 'No benchmarks have been added yet.',
|
'noresult.benchmark.subTitle': 'No benchmarks have been added yet.',
|
||||||
@@ -66,5 +69,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 公開鍵が見つかりません。'
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
export default {
|
||||||
|
'organizations.upsell.title': 'Organizations are an Enterprise feature',
|
||||||
|
'organizations.upsell.subtitle':
|
||||||
|
'Multi-tenancy lets you isolate users, resources, and quotas across teams. Upgrade to GPUStack Enterprise to manage organizations.',
|
||||||
|
'organizations.upsell.featuresTitle': 'What you get in Enterprise',
|
||||||
|
'organizations.upsell.feature.orgs':
|
||||||
|
'Create organizations to group users and isolate workloads',
|
||||||
|
'organizations.upsell.feature.members':
|
||||||
|
'Manage members and roles per organization',
|
||||||
|
'organizations.upsell.feature.quotas':
|
||||||
|
'Set resource and token quotas per organization',
|
||||||
|
'organizations.upsell.feature.isolation':
|
||||||
|
'Scope API keys, model deployments, and resources per organization',
|
||||||
|
'organizations.upsell.cta': 'Learn about Enterprise'
|
||||||
|
};
|
||||||
@@ -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',
|
||||||
|
|||||||
@@ -17,9 +17,15 @@ export default {
|
|||||||
'usage.table.user.apiKeysUsed': 'API Keys Used',
|
'usage.table.user.apiKeysUsed': 'API Keys Used',
|
||||||
'usage.table.lastActive': 'Last Active',
|
'usage.table.lastActive': 'Last Active',
|
||||||
'usage.filter.granularity': 'Granularity',
|
'usage.filter.granularity': 'Granularity',
|
||||||
|
'usage.filter.granularity.hour': '時間',
|
||||||
'usage.filter.granularity.day': 'Day',
|
'usage.filter.granularity.day': 'Day',
|
||||||
'usage.filter.granularity.week': 'Week',
|
'usage.filter.granularity.week': 'Week',
|
||||||
'usage.filter.granularity.month': 'Month',
|
'usage.filter.granularity.month': 'Month',
|
||||||
|
'usage.tabs.summary': '概要',
|
||||||
|
'usage.tabs.tokens': 'トークン',
|
||||||
|
'usage.tabs.gpuInstances': 'GPU インスタンス',
|
||||||
|
'usage.tabs.storage': 'ストレージ',
|
||||||
|
'usage.tabs.resourceEvents': 'リソースイベント',
|
||||||
'usage.tabs.models': 'Models',
|
'usage.tabs.models': 'Models',
|
||||||
'usage.tabs.apikeys': 'API Keys',
|
'usage.tabs.apikeys': 'API Keys',
|
||||||
'usage.tabs.users': 'User',
|
'usage.tabs.users': 'User',
|
||||||
@@ -32,5 +38,70 @@ export default {
|
|||||||
'usage.chart.cached': 'Cached',
|
'usage.chart.cached': 'Cached',
|
||||||
'usage.chart.uncached': 'Uncached',
|
'usage.chart.uncached': 'Uncached',
|
||||||
'usage.chart.inputTokensCached': 'Input Tokens (Cached/Uncached)',
|
'usage.chart.inputTokensCached': 'Input Tokens (Cached/Uncached)',
|
||||||
'usage.table.inputTokensCached': 'Input Tokens Cached'
|
'usage.table.inputTokensCached': 'Input Tokens Cached',
|
||||||
|
|
||||||
|
// --- Resource usage: shared metrics & units ---
|
||||||
|
'usage.metric.tokens': 'トークン数',
|
||||||
|
'usage.metric.input': '入力',
|
||||||
|
'usage.metric.output': '出力',
|
||||||
|
'usage.metric.gpuHours': 'GPU 時間',
|
||||||
|
'usage.metric.instanceHours': 'インスタンス時間',
|
||||||
|
'usage.metric.gbDays': 'GB·日',
|
||||||
|
'usage.metric.gbHours': 'GB·時間',
|
||||||
|
'usage.metric.activeUsers': 'アクティブユーザー',
|
||||||
|
'usage.metric.activeInstances': 'アクティブインスタンス',
|
||||||
|
'usage.metric.activeStorage': 'アクティブストレージ',
|
||||||
|
'usage.metric.activeVolumes': 'アクティブボリューム',
|
||||||
|
'usage.metric.storageTypes': 'ストレージタイプ',
|
||||||
|
'usage.metric.gpuHours.tip':
|
||||||
|
'インスタンスの稼働時間を GPU 数で重み付けした値:N 個の GPU を使用するインスタンスが H 時間稼働すると N × H GPU 時間としてカウントされます。すべてのインスタンスが単一の GPU を使用する場合はインスタンス時間と等しくなります。',
|
||||||
|
'usage.metric.instanceHours.tip':
|
||||||
|
'GPU の数に関係なく、すべてのインスタンスの稼働時間を合計した値。1 つのインスタンスが 2 時間稼働 = 2 インスタンス時間。',
|
||||||
|
'usage.metric.gbDays.tip':
|
||||||
|
'ストレージ容量を時間で積分した値(GB × 日):10 GB を 5 日間保持 = 50 GB·日。(= GB·時間 ÷ 24)',
|
||||||
|
'usage.metric.gbHours.tip':
|
||||||
|
'ストレージ容量を時間で積分した値(GB × 時間):10 GB を 5 時間保持 = 50 GB·時間。',
|
||||||
|
|
||||||
|
// --- Resource usage: common table / labels ---
|
||||||
|
'usage.common.noData': 'データがありません',
|
||||||
|
'usage.common.unknown': '不明',
|
||||||
|
'usage.table.date': '日付',
|
||||||
|
'usage.table.name': '名前',
|
||||||
|
'usage.table.user': 'ユーザー',
|
||||||
|
'usage.table.users': 'ユーザー',
|
||||||
|
'usage.table.type': 'タイプ',
|
||||||
|
'usage.table.instance': 'インスタンス',
|
||||||
|
'usage.table.instanceType': 'インスタンスタイプ',
|
||||||
|
'usage.table.instanceTypes': 'インスタンスタイプ',
|
||||||
|
'usage.table.instances': 'インスタンス',
|
||||||
|
'usage.table.capacity': '容量',
|
||||||
|
'usage.export.tableNamed': 'テーブルデータをエクスポート — {name}',
|
||||||
|
|
||||||
|
// --- Summary tab ---
|
||||||
|
'usage.summary.compute': 'コンピュート',
|
||||||
|
'usage.summary.tokensOverTime': 'トークン数の推移',
|
||||||
|
'usage.summary.gpuHoursOverTime': 'GPU 時間の推移',
|
||||||
|
'usage.summary.gbDaysOverTime': 'GB·日の推移',
|
||||||
|
|
||||||
|
// --- GPU Instances / Storage filters ---
|
||||||
|
'usage.filter.instance': 'インスタンスで絞り込み',
|
||||||
|
'usage.filter.storage': 'ストレージで絞り込み',
|
||||||
|
|
||||||
|
// --- Resource events ---
|
||||||
|
'usage.events.resourceType': 'リソースタイプ',
|
||||||
|
'usage.events.eventType': 'イベントタイプ',
|
||||||
|
'usage.events.resourceName': '名前で絞り込み',
|
||||||
|
'usage.events.col.time': '時刻',
|
||||||
|
'usage.events.col.resource': 'リソース',
|
||||||
|
'usage.events.col.event': 'イベント',
|
||||||
|
'usage.events.col.message': 'メッセージ',
|
||||||
|
'usage.events.resource.gpuInstance': 'GPU インスタンス',
|
||||||
|
'usage.events.resource.cpuInstance': 'CPU インスタンス',
|
||||||
|
'usage.events.type.created': '作成済み',
|
||||||
|
'usage.events.type.deleted': '削除済み',
|
||||||
|
'usage.events.type.started': '開始',
|
||||||
|
'usage.events.type.stopped': '停止',
|
||||||
|
'usage.events.type.updated': '更新済み',
|
||||||
|
'usage.events.type.attached': 'アタッチ済み',
|
||||||
|
'usage.events.type.detached': 'デタッチ済み'
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -29,6 +29,8 @@ export default {
|
|||||||
'users.password.modify.title': 'パスワードを変更',
|
'users.password.modify.title': 'パスワードを変更',
|
||||||
'users.password.modify.description':
|
'users.password.modify.description':
|
||||||
'アカウントのセキュリティのため、初期パスワードを変更してください。',
|
'アカウントのセキュリティのため、初期パスワードを変更してください。',
|
||||||
|
'users.password.modify.tips':
|
||||||
|
'パスワードを定期的に更新すると、アカウントの安全を保てます。',
|
||||||
'users.password.confirm': '新しいパスワードを確認',
|
'users.password.confirm': '新しいパスワードを確認',
|
||||||
'users.password.confirm.empty': '新しいパスワードを確認してください。',
|
'users.password.confirm.empty': '新しいパスワードを確認してください。',
|
||||||
'users.password.confirm.error': '入力された2つのパスワードが一致しません。',
|
'users.password.confirm.error': '入力された2つのパスワードが一致しません。',
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ export default {
|
|||||||
'vendor.hygon': 'Hygon',
|
'vendor.hygon': 'Hygon',
|
||||||
'vendor.moorthreads': 'Moore Threads',
|
'vendor.moorthreads': 'Moore Threads',
|
||||||
'vendor.iluvatar': 'Iluvatar',
|
'vendor.iluvatar': 'Iluvatar',
|
||||||
'vendor.metax': 'Metax',
|
'vendor.metax': 'MetaX',
|
||||||
'vendor.cambricon': 'Cambricon',
|
'vendor.cambricon': 'Cambricon',
|
||||||
'vendor.thead': 'T-Head PPU'
|
'vendor.thead': 'T-Head PPU'
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export default {
|
|||||||
'ai.provider.ollama': 'Ollama',
|
'ai.provider.ollama': 'Ollama',
|
||||||
'ai.provider.openai': 'OpenAI',
|
'ai.provider.openai': 'OpenAI',
|
||||||
'ai.provider.openrouter': 'OpenRouter',
|
'ai.provider.openrouter': 'OpenRouter',
|
||||||
'ai.provider.qwen': 'Qwen',
|
'ai.provider.qwen': 'Alibaba Cloud Model Studio',
|
||||||
'ai.provider.spark': 'Spark',
|
'ai.provider.spark': 'Spark',
|
||||||
'ai.provider.stepfun': 'StepFun',
|
'ai.provider.stepfun': 'StepFun',
|
||||||
'ai.provider.together-ai': 'TogetherAI',
|
'ai.provider.together-ai': 'TogetherAI',
|
||||||
|
|||||||
@@ -23,7 +23,9 @@ export default {
|
|||||||
'apikeys.accessScope.inference': 'Inference APIs',
|
'apikeys.accessScope.inference': 'Inference APIs',
|
||||||
'apikeys.access.permissions': 'Access Permissions',
|
'apikeys.access.permissions': 'Access Permissions',
|
||||||
'apikeys.type.auto': 'Auto-generated',
|
'apikeys.type.auto': 'Auto-generated',
|
||||||
'apikeys.type.custom': 'Custom'
|
'apikeys.type.custom': 'Custom',
|
||||||
|
'apikeys.button.ipConfig': 'IP Access Control',
|
||||||
|
'quotaLimits.button.title': 'Quota Limit'
|
||||||
};
|
};
|
||||||
|
|
||||||
// ========== To-Do: Translate Keys (Remove After Translation) ==========
|
// ========== To-Do: Translate Keys (Remove After Translation) ==========
|
||||||
|
|||||||
@@ -23,6 +23,14 @@ 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.flagFormat': 'Формат флага',
|
||||||
|
'backend.form.flagFormat.tips':
|
||||||
|
'Формат соединения опции и её значения. Оставьте пустым, чтобы сохранить каждый параметр в исходном виде, без приведения к единому формату.',
|
||||||
|
'backend.form.flagFormat.space': 'Разделение пробелом (--key value)',
|
||||||
|
'backend.form.flagFormat.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': 'Версии не добавлены',
|
||||||
|
|||||||
@@ -37,8 +37,8 @@ export default {
|
|||||||
'Stress test for long-context handling. Evaluates KV cache behavior, memory usage, and backend stability.',
|
'Stress test for long-context handling. Evaluates KV cache behavior, memory usage, and backend stability.',
|
||||||
'benchmark.form.profile.heavy.tips':
|
'benchmark.form.profile.heavy.tips':
|
||||||
'Decode-heavy generation benchmark. Measures sustained decoding speed and output token throughput.',
|
'Decode-heavy generation benchmark. Measures sustained decoding speed and output token throughput.',
|
||||||
'benchmark.table.filter.bygpu': 'Filter by GPU',
|
'benchmark.table.filter.bygpu': 'Поиск GPU',
|
||||||
'benchmark.table.filter.bymodel': 'Filter by Model',
|
'benchmark.table.filter.bymodel': 'Поиск модели',
|
||||||
'benchmark.table.filter.bydataset': 'Filter by Dataset',
|
'benchmark.table.filter.bydataset': 'Filter by Dataset',
|
||||||
'benchmark.table.filter.byProfile': 'Filter by Profile',
|
'benchmark.table.filter.byProfile': 'Filter by Profile',
|
||||||
'benchmark.table.avg': 'Avg',
|
'benchmark.table.avg': 'Avg',
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
export default {
|
||||||
|
'billing.upsell.title': 'Billing is an Enterprise feature',
|
||||||
|
'billing.upsell.subtitle':
|
||||||
|
'Track spend, generate invoices, and enforce budgets across teams. Upgrade to GPUStack Enterprise to manage billing.',
|
||||||
|
'billing.upsell.featuresTitle': 'What you get in Enterprise',
|
||||||
|
'billing.upsell.feature.usage':
|
||||||
|
'See cost breakdowns by organization, user, and model',
|
||||||
|
'billing.upsell.feature.invoices':
|
||||||
|
'Generate invoices and export billing reports',
|
||||||
|
'billing.upsell.feature.budgets':
|
||||||
|
'Set budgets and spending limits with alerts',
|
||||||
|
'billing.upsell.feature.chargeback':
|
||||||
|
'Attribute and charge back usage to teams and projects',
|
||||||
|
'billing.upsell.cta': 'Learn about Enterprise'
|
||||||
|
};
|
||||||
@@ -39,9 +39,11 @@ export default {
|
|||||||
'clusters.workerpool.batchSize.desc':
|
'clusters.workerpool.batchSize.desc':
|
||||||
'Количество воркеров, создаваемых одновременно в пуле воркеров',
|
'Количество воркеров, создаваемых одновременно в пуле воркеров',
|
||||||
'clusters.create.addworker.tips':
|
'clusters.create.addworker.tips':
|
||||||
'Пожалуйста, убедитесь, что выполнены <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 и зарегистрировать этот кластер.',
|
||||||
'cluster.create.checkEnv.tips':
|
'cluster.create.checkEnv.tips':
|
||||||
'Используйте следующую команду для проверки готовности окружения',
|
'Используйте следующую команду для проверки готовности окружения',
|
||||||
'clusters.create.register.tips':
|
'clusters.create.register.tips':
|
||||||
@@ -67,8 +69,14 @@ 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.subtitle':
|
||||||
|
'Вы можете выбрать несколько производителей GPU или не выбирать для кластера только с CPU',
|
||||||
'clusters.addworker.checkEnv': 'Проверить окружение',
|
'clusters.addworker.checkEnv': 'Проверить окружение',
|
||||||
|
'clusters.addworker.checkEnv.cpuOnlyTips':
|
||||||
|
'Используйте следующую команду, чтобы убедиться, что в кластере Kubernetes есть хотя бы один готовый узел. Вы регистрируете кластер только с CPU.',
|
||||||
'clusters.addworker.specifyArgs': 'Указать аргументы',
|
'clusters.addworker.specifyArgs': 'Указать аргументы',
|
||||||
|
'clusters.addworker.dtkVersion': 'Версия DTK',
|
||||||
'clusters.addworker.runCommand': 'Выполнить команду',
|
'clusters.addworker.runCommand': 'Выполнить команду',
|
||||||
'clusters.addworker.specifyWorkerIP': 'Указать IP воркера',
|
'clusters.addworker.specifyWorkerIP': 'Указать IP воркера',
|
||||||
'clusters.addworker.detectWorkerIP': 'Автоматически определить IP воркера',
|
'clusters.addworker.detectWorkerIP': 'Автоматически определить IP воркера',
|
||||||
@@ -107,6 +115,8 @@ export default {
|
|||||||
'{count} новых воркеров были добавлены в кластер.',
|
'{count} новых воркеров были добавлены в кластер.',
|
||||||
'clusters.create.serverUrl': 'URL сервера GPUStack',
|
'clusters.create.serverUrl': 'URL сервера GPUStack',
|
||||||
'clusters.create.workerConfig': 'Конфигурация воркера',
|
'clusters.create.workerConfig': 'Конфигурация воркера',
|
||||||
|
'clusters.edit.k8sOptions.changed.tip':
|
||||||
|
'Вы изменили параметры Kubernetes. Чтобы изменения вступили в силу, повторно выполните команду регистрации в целевом кластере.',
|
||||||
'clusters.addworker.containerName': 'Имя контейнера воркера',
|
'clusters.addworker.containerName': 'Имя контейнера воркера',
|
||||||
'clusters.addworker.containerName.tips':
|
'clusters.addworker.containerName.tips':
|
||||||
'Укажите имя для контейнера воркера.',
|
'Укажите имя для контейнера воркера.',
|
||||||
@@ -135,7 +145,7 @@ export default {
|
|||||||
'clusters.addworker.theadNotes-02':
|
'clusters.addworker.theadNotes-02':
|
||||||
'T-Head PPU uses the Container Device Interface (CDI) for device injection and requires the <span class="bold-text">/var/run/cdi</span> directory to be available for CDI generation.',
|
'T-Head PPU uses the Container Device Interface (CDI) for device injection and requires the <span class="bold-text">/var/run/cdi</span> directory to be available for CDI generation.',
|
||||||
'clusters.addworker.nvidiaNotes':
|
'clusters.addworker.nvidiaNotes':
|
||||||
'The built-in inference backends in GPUStack v2.1 require <span class="bold-text">CUDA 12.6+</span>. Please ensure your NVIDIA driver version is <span class="bold-text">560</span> or newer.',
|
'The built-in inference backends in GPUStack require <span class="bold-text">CUDA 12.8+</span>. Please ensure your NVIDIA driver version is <span class="bold-text">570</span> or newer.',
|
||||||
'clusters.volume.title': 'Volume Mounts',
|
'clusters.volume.title': 'Volume Mounts',
|
||||||
'clusters.volume.name': 'Volume Name',
|
'clusters.volume.name': 'Volume Name',
|
||||||
'clusters.volume.mountPath': 'Container Path',
|
'clusters.volume.mountPath': 'Container Path',
|
||||||
@@ -159,7 +169,35 @@ 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.systemDefaultContainerRegistry.title': 'Default Container Registry',
|
||||||
|
'clusters.systemDefaultContainerRegistry.tip':
|
||||||
|
'Default registry used to resolve GPUStack images for this cluster. Falls back to the server default when unset.',
|
||||||
|
'clusters.k8sOptions.title': 'Kubernetes Deployment Options',
|
||||||
|
'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.operatorImage.title': 'Operator Image',
|
||||||
|
'clusters.operatorImage.tip':
|
||||||
|
'Override for the GPUStack Operator container image. Leave empty to use the server default.',
|
||||||
|
'clusters.namespace.title': 'Namespace',
|
||||||
|
'clusters.namespace.tip':
|
||||||
|
'Kubernetes namespace the cluster’s manifests render into. Leave empty to use gpustack-system.',
|
||||||
|
'clusters.clusterType.title': 'Cluster Type',
|
||||||
|
'clusters.modelService.title': 'Model Service',
|
||||||
|
'clusters.modelService.tip':
|
||||||
|
'For LLM inference and API serving — e.g. exposing model APIs and token-based services.',
|
||||||
|
'clusters.gpuInstances.title': 'GPU Service',
|
||||||
|
'clusters.gpuInstances.tip':
|
||||||
|
'For on-demand GPU compute — e.g. interactive development, training jobs, or custom environments.',
|
||||||
|
'clusters.gpuInstances.staticAddress': 'GPU Service Static Access Address',
|
||||||
|
'clusters.gpuInstances.staticAddress.tip':
|
||||||
|
'Static address the operator uses to access GPU instances in this cluster (e.g. a LoadBalancer VIP). Optional.'
|
||||||
};
|
};
|
||||||
|
|
||||||
// ========== To-Do: Translate Keys (Remove After Translation) ==========
|
// ========== To-Do: Translate Keys (Remove After Translation) ==========
|
||||||
|
|||||||
@@ -46,22 +46,28 @@ export default {
|
|||||||
'common.button.enabled': 'Активно',
|
'common.button.enabled': 'Активно',
|
||||||
'common.button.disabled': 'Отключено',
|
'common.button.disabled': 'Отключено',
|
||||||
'common.button.upgrade': 'Обновить',
|
'common.button.upgrade': 'Обновить',
|
||||||
|
'common.enterprise.feature': 'Available in GPUStack Enterprise',
|
||||||
'common.input.holder': 'Введите значение',
|
'common.input.holder': 'Введите значение',
|
||||||
'common.validate.value': 'Поле {name} обязательно',
|
'common.validate.value': 'Поле {name} обязательно',
|
||||||
'common.button.edit': 'Редактировать',
|
'common.button.edit': 'Редактировать',
|
||||||
'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.creator': 'Создатель',
|
||||||
'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}',
|
||||||
'common.search.name.placeholder': 'Фильтр по названию',
|
'common.search.name.placeholder': 'Фильтр по названию',
|
||||||
'common.search.id.placeholder': 'Фильтр по ID',
|
'common.search.id.placeholder': 'Фильтр по ID',
|
||||||
'common.filter.byId': 'Фильтр по ID',
|
'common.filter.byId': 'Фильтр по ID',
|
||||||
|
'common.filter.byCreator': 'Фильтр по создателю',
|
||||||
'common.table.type': 'Тип',
|
'common.table.type': 'Тип',
|
||||||
'common.table.default': 'Значение по умолчанию',
|
'common.table.default': 'Значение по умолчанию',
|
||||||
'common.copy.success': 'Скопировано!',
|
'common.copy.success': 'Скопировано!',
|
||||||
@@ -161,6 +167,7 @@ export default {
|
|||||||
'common.time.hour': 'Час',
|
'common.time.hour': 'Час',
|
||||||
'common.time.minute': 'Минута',
|
'common.time.minute': 'Минута',
|
||||||
'common.issue.report': 'Сообщить о проблеме',
|
'common.issue.report': 'Сообщить о проблеме',
|
||||||
|
'common.github.star.tooltip': 'Поставьте нам звезду на GitHub',
|
||||||
'common.social.discord': 'Присоединиться к Discord',
|
'common.social.discord': 'Присоединиться к Discord',
|
||||||
'common.table.mark': 'Комментарий',
|
'common.table.mark': 'Комментарий',
|
||||||
'common.table.rollback.mark': 'Комментарий к откату',
|
'common.table.rollback.mark': 'Комментарий к откату',
|
||||||
@@ -190,6 +197,7 @@ export default {
|
|||||||
'common.table.user': 'Пользователь',
|
'common.table.user': 'Пользователь',
|
||||||
'common.settings.instructions': 'Инструкции',
|
'common.settings.instructions': 'Инструкции',
|
||||||
'common.settings.language': 'Язык',
|
'common.settings.language': 'Язык',
|
||||||
|
'common.settings.language.tips': 'Задайте язык отображения интерфейса.',
|
||||||
'common.delete.confirm': 'Вы уверены, что хотите удалить выбранный {type}?',
|
'common.delete.confirm': 'Вы уверены, что хотите удалить выбранный {type}?',
|
||||||
'common.delete.single.confirm':
|
'common.delete.single.confirm':
|
||||||
'Вы уверены, что хотите удалить <span style="font-size: 13px;font-weight: 700">{name}</span>?',
|
'Вы уверены, что хотите удалить <span style="font-size: 13px;font-weight: 700">{name}</span>?',
|
||||||
@@ -221,7 +229,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': 'Нет',
|
||||||
@@ -244,6 +251,11 @@ export default {
|
|||||||
'common.appearance.tips': 'По умолчанию соответствует системным настройкам.',
|
'common.appearance.tips': 'По умолчанию соответствует системным настройкам.',
|
||||||
'common.button.forgotpassword': 'Забыли пароль?',
|
'common.button.forgotpassword': 'Забыли пароль?',
|
||||||
'common.appearance.theme': 'Тема',
|
'common.appearance.theme': 'Тема',
|
||||||
|
'common.appearance.description':
|
||||||
|
'Настройте внешний вид интерфейса на вашем устройстве.',
|
||||||
|
'common.security': 'Безопасность',
|
||||||
|
'common.security.description':
|
||||||
|
'Управляйте паролем для входа в учётную запись.',
|
||||||
'common.page.wentwrong': 'Что-то пошло не так.',
|
'common.page.wentwrong': 'Что-то пошло не так.',
|
||||||
'common.page.refresh.tips':
|
'common.page.refresh.tips':
|
||||||
'Страница может нуждаться в обновлении. Попробуйте обновить её!',
|
'Страница может нуждаться в обновлении. Попробуйте обновить её!',
|
||||||
@@ -254,11 +266,15 @@ export default {
|
|||||||
'common.login.auth': 'Аутентификация...',
|
'common.login.auth': 'Аутентификация...',
|
||||||
'common.login.auth.failed': 'Ошибка аутентификации',
|
'common.login.auth.failed': 'Ошибка аутентификации',
|
||||||
'common.login.password': 'Войти с паролем',
|
'common.login.password': 'Войти с паролем',
|
||||||
|
'common.login.username.holder': 'Введите имя пользователя',
|
||||||
|
'common.login.password.holder': 'Введите пароль',
|
||||||
|
'common.login.newpassword.holder': 'Введите новый пароль',
|
||||||
|
'common.login.confirm.holder': 'Введите пароль ещё раз',
|
||||||
'common.external.login': 'Войти через {type}',
|
'common.external.login': 'Войти через {type}',
|
||||||
'common.sso.noConfig':
|
'common.sso.noConfig':
|
||||||
'Единый вход не настроен в этой системе. Пожалуйста, обратитесь к администратору.',
|
'Единый вход не настроен в этой системе. Пожалуйста, обратитесь к администратору.',
|
||||||
'common.button.edit.item': 'Редактировать {name}',
|
'common.button.edit.item': 'Редактировать {name}',
|
||||||
'common.button.copy.item': 'Duplicate {name}',
|
'common.button.copy.item': 'Дублировать {name}',
|
||||||
'common.button.terminal': 'Терминал',
|
'common.button.terminal': 'Терминал',
|
||||||
'common.button.addItem': 'Добавить элемент',
|
'common.button.addItem': 'Добавить элемент',
|
||||||
'common.help.default': 'По умолчанию: {content}',
|
'common.help.default': 'По умолчанию: {content}',
|
||||||
@@ -280,7 +296,11 @@ 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.max.count': 'Количество {label}',
|
||||||
|
'common.validate.group': 'Please complete the {group} configuration',
|
||||||
|
'common.preferences': 'Preferences'
|
||||||
};
|
};
|
||||||
|
|
||||||
// ========== 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': 'Выбрать модели',
|
||||||
|
|||||||
@@ -14,11 +14,26 @@ export default {
|
|||||||
'gpuservice.template.command.placeholder':
|
'gpuservice.template.command.placeholder':
|
||||||
'Разделяйте аргументы пробелами; аргументы с пробелами заключайте в кавычки, например: /bin/bash -c "echo hello world"',
|
'Разделяйте аргументы пробелами; аргументы с пробелами заключайте в кавычки, например: /bin/bash -c "echo hello world"',
|
||||||
'gpuservice.template.mountPath': 'Путь монтирования',
|
'gpuservice.template.mountPath': 'Путь монтирования',
|
||||||
|
'gpuservice.template.mountPath.tips':
|
||||||
|
'Путь, по которому том хранилища монтируется по умолчанию при создании экземпляра из этого шаблона. Может использоваться для сохранения данных, которые нужно сохранить во время работы экземпляра.',
|
||||||
'gpuservice.template.containerDisk': 'Диск контейнера (GB)',
|
'gpuservice.template.containerDisk': 'Диск контейнера (GB)',
|
||||||
|
'gpuservice.template.containerDisk.tips':
|
||||||
|
'Размер системного диска контейнера.',
|
||||||
'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 +44,53 @@ 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.delete.tips':
|
||||||
|
'Удаление открытого ключа 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,22 +106,46 @@ export default {
|
|||||||
'gpuservice.instance.templates': 'Шаблоны экземпляров',
|
'gpuservice.instance.templates': 'Шаблоны экземпляров',
|
||||||
'gpuservice.instance.section.storage': 'Том хранилища',
|
'gpuservice.instance.section.storage': 'Том хранилища',
|
||||||
'gpuservice.instance.type.required': 'Выберите тип экземпляра',
|
'gpuservice.instance.type.required': 'Выберите тип экземпляра',
|
||||||
|
'gpuservice.instance.type.noAvailable': 'Нет доступных типов экземпляров',
|
||||||
'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.cpuCount.max': 'Выберите максимум {count} ядер CPU',
|
||||||
|
'gpuservice.instance.cpuCount.min': 'Выберите минимум {count} ядер CPU',
|
||||||
|
'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': 'VRAM',
|
||||||
'gpuservice.instance.ram': 'RAM',
|
'gpuservice.instance.ram': 'RAM',
|
||||||
'gpuservice.instance.search.type.placeholder':
|
'gpuservice.instance.os': 'ОС',
|
||||||
'Поиск по имени, VRAM, памяти или vCPU',
|
'gpuservice.instance.arch': 'Архитектура',
|
||||||
|
'gpuservice.instance.disk': 'Диск',
|
||||||
|
'gpuservice.table.count': 'Количество',
|
||||||
|
'gpuservice.instance.disk.system': 'Системный диск',
|
||||||
|
'gpuservice.instance.disk.ephemeral': 'Временное хранилище',
|
||||||
|
'gpuservice.instance.disk.persistent': 'Постоянное хранилище',
|
||||||
|
'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': 'Редактировать хранилище',
|
||||||
@@ -73,9 +158,24 @@ export default {
|
|||||||
'gpuservice.storage.accessMode': 'Режим доступа',
|
'gpuservice.storage.accessMode': 'Режим доступа',
|
||||||
'gpuservice.storage.persistent': 'Постоянное',
|
'gpuservice.storage.persistent': 'Постоянное',
|
||||||
'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 instance restarts. Persistent volumes remain intact after instance termination and can be shared by multiple instances.',
|
||||||
|
'gpuservice.form.storage.select': 'Выберите хранилище',
|
||||||
|
'gpuservice.creator': 'Создатель',
|
||||||
|
'gpuservice.owner.global': 'Глобальный',
|
||||||
|
'gpuservice.template.group.yours': 'Ваши шаблоны',
|
||||||
|
'gpuservice.template.group.global': 'Глобальные шаблоны'
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export default {
|
|||||||
'menu.playground.text2images': 'Генерация изображений',
|
'menu.playground.text2images': 'Генерация изображений',
|
||||||
'menu.playground.video': 'Видео',
|
'menu.playground.video': 'Видео',
|
||||||
'menu.compare': 'Сравнение',
|
'menu.compare': 'Сравнение',
|
||||||
'menu.models': 'Модели',
|
'menu.models': 'Сервисы моделей',
|
||||||
'menu.models.modelList': 'Развертывание и управление',
|
'menu.models.modelList': 'Развертывание и управление',
|
||||||
'menu.models.modelCatalog': 'Каталог',
|
'menu.models.modelCatalog': 'Каталог',
|
||||||
'menu.models.catalog': 'Каталог моделей',
|
'menu.models.catalog': 'Каталог моделей',
|
||||||
@@ -23,28 +23,33 @@ export default {
|
|||||||
'menu.resources': 'Ресурсы',
|
'menu.resources': 'Ресурсы',
|
||||||
'menu.apikeys': 'API-ключи',
|
'menu.apikeys': 'API-ключи',
|
||||||
'menu.users': 'Пользователи',
|
'menu.users': 'Пользователи',
|
||||||
'menu.profile': 'Профиль',
|
'menu.profile': 'Preferences',
|
||||||
'menu.login': 'Авторизация',
|
'menu.login': 'Авторизация',
|
||||||
'menu.usage': 'Использование',
|
'menu.usage': 'Использование',
|
||||||
|
'menu.usage.usage': 'Использование',
|
||||||
|
'menu.billingAndUsage': 'Использование и биллинг',
|
||||||
|
'menu.billingAndUsage.usage': 'Использование',
|
||||||
|
'menu.billingAndUsage.billing': 'Биллинг',
|
||||||
'menu.404': 'Ошибка 404',
|
'menu.404': 'Ошибка 404',
|
||||||
'menu.resources.workers': 'Воркеры',
|
'menu.resources.workers': 'Воркеры',
|
||||||
'menu.resources.gpus': 'GPUs',
|
'menu.resources.gpus': 'GPUs',
|
||||||
'menu.resources.modelfiles': 'Файлы модлей',
|
'menu.models.modelfiles': 'Файлы модлей',
|
||||||
'menu.accessControl': 'Управление доступом',
|
'menu.accessControl': 'Управление доступом',
|
||||||
'menu.accessControl.apikeys': 'API Ключи',
|
'menu.accessControl.apikeys': 'API Ключи',
|
||||||
'menu.accessControl.users': 'Пользователи',
|
'menu.accessControl.users': 'Пользователи',
|
||||||
'menu.clusterManagement': 'Управление кластерами',
|
'menu.accessControl.organizations': 'Организации',
|
||||||
'menu.clusterManagement.clusters': 'Кластеры',
|
'menu.resources.clusters': 'Кластеры',
|
||||||
'menu.clusterManagement.credentials': 'Облачные аккаунты',
|
'menu.resources.credentials': 'Облачные аккаунты',
|
||||||
'menu.models.userModels': 'Мои модели',
|
'menu.models.userModels': 'Мои модели',
|
||||||
'menu.clusterManagement.clusterDetail': 'Детали кластера',
|
'menu.resources.clusterDetail': 'Детали кластера',
|
||||||
'menu.clusterManagement.clusterCreate': 'Создать кластер',
|
'menu.resources.clusterCreate': 'Создать кластер',
|
||||||
'menu.resources.backendsList': 'Бэкенды запуска',
|
'menu.models.backendsList': 'Бэкенды запуска',
|
||||||
'menu.settings': 'Settings',
|
'menu.settings': 'Settings',
|
||||||
'menu.gpuService': 'GPU Service',
|
'menu.gpuService': 'GPU Service',
|
||||||
'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 Adapters',
|
||||||
|
'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 ==========
|
||||||
|
|||||||
@@ -37,9 +37,12 @@ export default {
|
|||||||
'noresult.catalog.nofound': 'Подходящие модели не найдены.',
|
'noresult.catalog.nofound': 'Подходящие модели не найдены.',
|
||||||
'noresult.resources.cluster':
|
'noresult.resources.cluster':
|
||||||
'No clusters available. Add a cluster to get started.',
|
'No clusters available. Add a cluster to get started.',
|
||||||
|
'noresult.resources.k8sCluster':
|
||||||
|
'No clusters available. Add a Kubernetes cluster to get started.',
|
||||||
'noresult.resources.worker':
|
'noresult.resources.worker':
|
||||||
'No workers available. Add a worker to get started.',
|
'No workers available. Add a worker to get started.',
|
||||||
'noresult.resources.gotocluster': 'Create Your First Cluster',
|
'noresult.resources.gotocluster': 'Create Your First Cluster',
|
||||||
|
'noresult.resources.addk8scluster': 'Add a Kubernetes Cluster',
|
||||||
'noresult.resources.gotoworker': 'Add Worker',
|
'noresult.resources.gotoworker': 'Add Worker',
|
||||||
'noresult.benchmark.title': 'No Benchmarks',
|
'noresult.benchmark.title': 'No Benchmarks',
|
||||||
'noresult.benchmark.subTitle': 'No benchmarks have been added yet.',
|
'noresult.benchmark.subTitle': 'No benchmarks have been added yet.',
|
||||||
@@ -65,7 +68,15 @@ 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) ==========
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
export default {
|
||||||
|
'organizations.upsell.title': 'Organizations are an Enterprise feature',
|
||||||
|
'organizations.upsell.subtitle':
|
||||||
|
'Multi-tenancy lets you isolate users, resources, and quotas across teams. Upgrade to GPUStack Enterprise to manage organizations.',
|
||||||
|
'organizations.upsell.featuresTitle': 'What you get in Enterprise',
|
||||||
|
'organizations.upsell.feature.orgs':
|
||||||
|
'Create organizations to group users and isolate workloads',
|
||||||
|
'organizations.upsell.feature.members':
|
||||||
|
'Manage members and roles per organization',
|
||||||
|
'organizations.upsell.feature.quotas':
|
||||||
|
'Set resource and token quotas per organization',
|
||||||
|
'organizations.upsell.feature.isolation':
|
||||||
|
'Scope API keys, model deployments, and resources per organization',
|
||||||
|
'organizations.upsell.cta': 'Learn about Enterprise'
|
||||||
|
};
|
||||||
@@ -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',
|
||||||
|
|||||||