Compare commits
242
Commits
gpu-service
..
test
| 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 |
@@ -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`.
|
||||
+1
-1
@@ -13,6 +13,6 @@
|
||||
.swc
|
||||
.DS_Store
|
||||
.idea
|
||||
.claude
|
||||
.claude/settings.local.json
|
||||
/dist.zip
|
||||
.cache
|
||||
+1
-2
@@ -3,7 +3,6 @@ node_modules
|
||||
.umi-production
|
||||
public/static/*.js
|
||||
public/static/*.css
|
||||
src/components/icon-font/iconfont/iconfont.js
|
||||
src/components/icon-font/iconfont/*.css
|
||||
src/components/iconfont/
|
||||
|
||||
|
||||
|
||||
+1
-1
@@ -3,5 +3,5 @@ module.exports = {
|
||||
rules: {
|
||||
'selector-class-pattern': null
|
||||
},
|
||||
ignoreFiles: ['public/static/*.css']
|
||||
ignoreFiles: ['public/static/*.css', 'src/components/iconfont/iconfont.css']
|
||||
};
|
||||
|
||||
@@ -1,265 +0,0 @@
|
||||
# React State and Request Patterns
|
||||
|
||||
These guidelines define preferred patterns for request handling, state updates, and side-effect management in React applications.
|
||||
|
||||
The primary goal is to keep data flow explicit, predictable, maintainable, and performant while avoiding unnecessary rerenders and effect-driven logic.
|
||||
|
||||
---
|
||||
|
||||
## 1. Avoid Effect-Driven Requests
|
||||
|
||||
Do not use request functions themselves as dependencies in `useEffect`.
|
||||
|
||||
Avoid patterns like:
|
||||
|
||||
```ts
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, [fetchData]);
|
||||
```
|
||||
|
||||
Requests should be triggered explicitly by user actions or lifecycle entry points.
|
||||
|
||||
---
|
||||
|
||||
## 2. Form Requests Should Be Action-Driven
|
||||
|
||||
For form-related requests (such as loading `Select` options):
|
||||
|
||||
- Fetch data when the form is opened for the first time.
|
||||
- If later requests depend on user interactions, trigger them directly inside the interaction handler.
|
||||
- Do not rely on `useEffect` dependency changes to trigger requests.
|
||||
|
||||
Recommended:
|
||||
|
||||
```ts
|
||||
const handleOnChange = (value) => {
|
||||
fetchData(value);
|
||||
};
|
||||
```
|
||||
|
||||
Avoid:
|
||||
|
||||
```ts
|
||||
useEffect(() => {
|
||||
fetchData(value);
|
||||
}, [value]);
|
||||
```
|
||||
|
||||
The action itself should control the request.
|
||||
|
||||
---
|
||||
|
||||
## 3. Update Related States Together
|
||||
|
||||
If a single action updates multiple related states:
|
||||
|
||||
- Do not synchronize them through `useEffect`
|
||||
- Do not derive them indirectly through `useMemo`
|
||||
|
||||
Instead, update all related states directly inside the action handler.
|
||||
|
||||
Recommended:
|
||||
|
||||
```ts
|
||||
const handleOnChange = (value) => {
|
||||
setState1(...);
|
||||
setState2(...);
|
||||
buildState(...);
|
||||
};
|
||||
```
|
||||
|
||||
Avoid implicit state synchronization chains.
|
||||
|
||||
---
|
||||
|
||||
## 4. Group Strongly Related State
|
||||
|
||||
If multiple states are always updated together:
|
||||
|
||||
- Do not split them into multiple `useState` calls.
|
||||
- Prefer a single state object.
|
||||
|
||||
Recommended:
|
||||
|
||||
```ts
|
||||
const [state, setState] = useState({
|
||||
state1: ...,
|
||||
state2: ...,
|
||||
state3: ...,
|
||||
});
|
||||
```
|
||||
|
||||
This reduces unnecessary rerenders and keeps state transitions predictable.
|
||||
|
||||
---
|
||||
|
||||
## 5. Prefer Explicit State Flow
|
||||
|
||||
Avoid chaining business logic through multiple `useEffect` hooks.
|
||||
|
||||
Keep:
|
||||
|
||||
- request execution
|
||||
- state updates
|
||||
- derived calculations
|
||||
|
||||
close to the triggering action whenever possible.
|
||||
|
||||
Prefer:
|
||||
|
||||
```ts
|
||||
const handleAction = () => {
|
||||
fetchData();
|
||||
setTableData(...);
|
||||
setSelectedRow(...);
|
||||
};
|
||||
```
|
||||
|
||||
Over:
|
||||
|
||||
```ts
|
||||
useEffect(() => {
|
||||
buildTable();
|
||||
}, [data]);
|
||||
|
||||
useEffect(() => {
|
||||
updateSelection();
|
||||
}, [tableData]);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Avoid Premature Memoization
|
||||
|
||||
Do not use `useMemo` or `useCallback` unless there is a confirmed rendering or computation bottleneck.
|
||||
|
||||
Overusing memoization:
|
||||
|
||||
- increases complexity
|
||||
- makes state flow harder to understand
|
||||
- may introduce stale dependency issues
|
||||
|
||||
Prefer simple and explicit logic first.
|
||||
|
||||
Optimize only when necessary.
|
||||
|
||||
---
|
||||
|
||||
## 7. Keep Request Logic Predictable
|
||||
|
||||
A user interaction should clearly show:
|
||||
|
||||
- what request is triggered
|
||||
- which states are updated
|
||||
- how the UI changes
|
||||
|
||||
Avoid indirect update chains caused by dependency-driven effects.
|
||||
|
||||
The code should make the request and update flow easy to trace.
|
||||
|
||||
---
|
||||
|
||||
## 8. Prefer Action-Driven Architecture
|
||||
|
||||
Prefer:
|
||||
|
||||
- action-driven updates
|
||||
- explicit handlers
|
||||
- localized state transitions
|
||||
|
||||
Over:
|
||||
|
||||
- effect-driven synchronization
|
||||
- cross-hook implicit updates
|
||||
- reactive chains between states
|
||||
|
||||
The triggering action should remain the primary source of truth for UI updates.
|
||||
|
||||
---
|
||||
|
||||
# Form
|
||||
|
||||
Form-specific patterns that build on the rules above. The theme: keep cascading selections (pick A → derive B → write form) on a single, predictable path.
|
||||
|
||||
## 1. No Fallback for Derived Selection
|
||||
|
||||
When "pick A then auto-pick B", match by rule and return `undefined` if no match — let the corresponding form field stay empty.
|
||||
|
||||
Do not silently fall back to `list[0]` or another default. A fallback hides data issues and tricks the user into thinking they have a valid selection.
|
||||
|
||||
```ts
|
||||
const findB = (key, list) =>
|
||||
key ? list.find((x) => x.key === key) : undefined;
|
||||
```
|
||||
|
||||
For form fields, prefer clearing with `undefined` over `''`. With Ant Design, `undefined` restores the placeholder; `''` is treated as a real value.
|
||||
|
||||
## 2. Async Race Protection
|
||||
|
||||
For fetches triggered by a lifecycle entry (e.g., modal open), tag each invocation with a session ref. Discard stale results if the session has rotated (the modal was closed and re-opened) by the time the response arrives.
|
||||
|
||||
```ts
|
||||
const sessionRef = useRef(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) {
|
||||
sessionRef.current += 1;
|
||||
return;
|
||||
}
|
||||
const session = ++sessionRef.current;
|
||||
Promise.all([fetchA(), fetchB()]).then(([as, bs]) => {
|
||||
if (sessionRef.current !== session) return;
|
||||
applySelection(as.items[0], findB(as.items[0].key, bs.items));
|
||||
});
|
||||
}, [open]);
|
||||
```
|
||||
|
||||
## 3. Reference Template
|
||||
|
||||
A typical form with two cascading selectors backed by a single shared state:
|
||||
|
||||
```ts
|
||||
type Selection = { a?: string; b?: number };
|
||||
|
||||
const [selection, setSelection] = useState<Selection>({});
|
||||
const sessionRef = useRef(0);
|
||||
|
||||
const findB = (key, list) =>
|
||||
key ? list.find((x) => x.key === key) : undefined;
|
||||
|
||||
// Single atomic write: state + form together.
|
||||
const applySelection = (a, b) => {
|
||||
setSelection({ a: a.name, b: b?.id });
|
||||
form.current?.setFieldsValue({
|
||||
field: b?.field,
|
||||
spec: { ...currentSpec, ...b?.spec }
|
||||
});
|
||||
};
|
||||
|
||||
// Trigger 1: modal opened
|
||||
useEffect(() => {
|
||||
if (!open) {
|
||||
sessionRef.current++;
|
||||
setSelection({});
|
||||
return;
|
||||
}
|
||||
const session = ++sessionRef.current;
|
||||
Promise.all([fetchA(), fetchB()]).then(([as, bs]) => {
|
||||
if (sessionRef.current !== session) return;
|
||||
const first = as.items[0];
|
||||
applySelection(first, findB(first.key, bs.items));
|
||||
});
|
||||
}, [open]);
|
||||
|
||||
// Trigger 2: user picks A
|
||||
const handleAChange = (a) => {
|
||||
applySelection(a, findB(a.key, listB));
|
||||
};
|
||||
|
||||
// Trigger 3: user picks B
|
||||
const handleBChange = (b) => {
|
||||
setSelection((prev) => ({ ...prev, b: b.id }));
|
||||
form.current?.setFieldsValue({ ...b.fields });
|
||||
};
|
||||
```
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
+99
-62
@@ -140,15 +140,6 @@ const baseRoutes = [
|
||||
access: 'canSeeOrgAdmin',
|
||||
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',
|
||||
path: '/models/providers',
|
||||
@@ -179,6 +170,26 @@ const baseRoutes = [
|
||||
access: 'canSeeOrgAdmin',
|
||||
hideInMenu: true,
|
||||
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',
|
||||
path: '/gpu-service',
|
||||
key: 'gpuService',
|
||||
access: 'canSeeGpuService',
|
||||
routes: [
|
||||
{
|
||||
path: '/gpu-service',
|
||||
@@ -223,7 +235,12 @@ const baseRoutes = [
|
||||
path: '/gpu-service/storage-types',
|
||||
key: 'gpuServiceStorageTypes',
|
||||
icon: 'icon-storage-outlined',
|
||||
access: 'canSeeAdmin',
|
||||
// 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',
|
||||
defaultIcon: 'icon-storage-outlined',
|
||||
component: './gpu-service/storage-types'
|
||||
@@ -249,6 +266,16 @@ const baseRoutes = [
|
||||
path: '/resources',
|
||||
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',
|
||||
path: '/resources/workers',
|
||||
@@ -268,67 +295,63 @@ const baseRoutes = [
|
||||
component: './resources/components/gpus'
|
||||
},
|
||||
{
|
||||
name: 'backendsList',
|
||||
path: '/resources/backends',
|
||||
key: 'backendsList',
|
||||
icon: 'icon-backend',
|
||||
selectedIcon: 'icon-backend-filled',
|
||||
defaultIcon: 'icon-backend',
|
||||
access: 'canSeeOrgAdmin',
|
||||
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: 'credentials',
|
||||
path: '/resources/credentials',
|
||||
key: 'credentials',
|
||||
icon: 'icon-credential-outline',
|
||||
selectedIcon: 'icon-credential-filled',
|
||||
defaultIcon: 'icon-credential-outline',
|
||||
component: './cluster-management/credentials'
|
||||
},
|
||||
{
|
||||
name: 'clusterDetail',
|
||||
path: '/cluster-management/clusters/detail',
|
||||
path: '/resources/clusters/detail',
|
||||
key: 'clusterDetail',
|
||||
icon: 'icon-cluster2-outline',
|
||||
selectedIcon: 'icon-cluster2-filled',
|
||||
defaultIcon: 'icon-cluster2-outline',
|
||||
hideInMenu: true,
|
||||
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',
|
||||
path: '/cluster-management/credentials',
|
||||
key: 'credentials',
|
||||
icon: 'icon-credential-outline',
|
||||
selectedIcon: 'icon-credential-filled',
|
||||
defaultIcon: 'icon-credential-outline',
|
||||
component: './cluster-management/credentials'
|
||||
name: 'usage',
|
||||
path: '/usage/overview',
|
||||
key: 'usage',
|
||||
icon: 'icon-usage-outlined',
|
||||
selectedIcon: 'icon-usage-filled',
|
||||
defaultIcon: 'icon-usage-outlined',
|
||||
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'
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -341,6 +364,20 @@ const baseRoutes = [
|
||||
path: '/access-control',
|
||||
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',
|
||||
path: '/access-control/users',
|
||||
@@ -374,8 +411,8 @@ const baseRoutes = [
|
||||
},
|
||||
{
|
||||
name: 'profile',
|
||||
path: '/profile',
|
||||
key: 'profile',
|
||||
path: '/preferences',
|
||||
key: 'preferences',
|
||||
hideInMenu: true,
|
||||
component: './profile',
|
||||
icon: 'User'
|
||||
|
||||
+24
-10
@@ -1,13 +1,27 @@
|
||||
const child_process = require('child_process');
|
||||
import { execSync } from 'child_process';
|
||||
|
||||
export const getBranchInfo = () => {
|
||||
const latestCommit = child_process
|
||||
.execSync('git rev-parse HEAD')
|
||||
.toString()
|
||||
.trim();
|
||||
const versionTag = child_process
|
||||
.execSync(`git tag --contains ${latestCommit}`)
|
||||
.toString()
|
||||
.trim();
|
||||
return { version: versionTag || '', commitId: latestCommit.slice(0, 7) };
|
||||
// git may be absent (source archive, bare container) or this tree may
|
||||
// not be a git checkout. Swallow the failure and fall back to the env
|
||||
// overrides below — losing build info shouldn't fail the build.
|
||||
let latestCommit = '';
|
||||
let versionTag = '';
|
||||
try {
|
||||
latestCommit = execSync('git rev-parse HEAD').toString().trim();
|
||||
versionTag = execSync(`git tag --contains ${latestCommit}`)
|
||||
.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)
|
||||
};
|
||||
};
|
||||
|
||||
+2
-1
@@ -14,7 +14,8 @@ export default defineConfig([
|
||||
'dist',
|
||||
'src/.umi/',
|
||||
'src/.umi-production/',
|
||||
'src/.umi-test/'
|
||||
'src/.umi-test/',
|
||||
'src/components/iconfont/'
|
||||
]),
|
||||
{
|
||||
files: ['**/*.{ts,tsx,js,jsx}'],
|
||||
|
||||
+1
-1
@@ -17,7 +17,7 @@
|
||||
"@ant-design/pro-components": "3.1.0-0",
|
||||
"@antv/g6": "^5.0.51",
|
||||
"@braintree/sanitize-url": "^7.1.1",
|
||||
"@gpustack/core-ui": "^1.0.16",
|
||||
"@gpustack/core-ui": "^1.0.32",
|
||||
"@huggingface/gguf": "^0.1.7",
|
||||
"@huggingface/hub": "^0.15.1",
|
||||
"@huggingface/tasks": "^0.11.6",
|
||||
|
||||
@@ -5,6 +5,8 @@ export default (api: IApi) => {
|
||||
const info = JSON.parse(process.env.VERSION || '{}');
|
||||
const env = process.env.NODE_ENV;
|
||||
|
||||
$('html').attr('lang', 'en');
|
||||
|
||||
$('html').attr('data-env', env);
|
||||
|
||||
$('html').attr(
|
||||
|
||||
Generated
+119
-119
@@ -24,8 +24,8 @@ importers:
|
||||
specifier: ^7.1.1
|
||||
version: 7.1.2
|
||||
'@gpustack/core-ui':
|
||||
specifier: ^1.0.16
|
||||
version: 1.0.16(czdvzceysqw7iv6pct2ucnb23e)
|
||||
specifier: ^1.0.32
|
||||
version: 1.0.32(czdvzceysqw7iv6pct2ucnb23e)
|
||||
'@huggingface/gguf':
|
||||
specifier: ^0.1.7
|
||||
version: 0.1.18
|
||||
@@ -49,7 +49,7 @@ importers:
|
||||
version: 4.17.24
|
||||
'@umijs/max':
|
||||
specifier: ^4.6.15
|
||||
version: 4.6.51(@babel/core@7.29.0)(@types/node@25.6.2)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(lightningcss@1.22.1)(prettier@3.8.3)(rc-field-form@2.7.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.29.0)(react@18.3.1))(terser@5.48.0)(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14))
|
||||
version: 4.6.51(@babel/core@7.23.6)(@types/node@25.6.2)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(lightningcss@1.22.1)(prettier@3.8.3)(rc-field-form@2.7.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.23.6)(react@18.3.1))(terser@5.48.0)(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14))
|
||||
'@xterm/addon-fit':
|
||||
specifier: ^0.10.0
|
||||
version: 0.10.0(@xterm/xterm@5.5.0)
|
||||
@@ -109,7 +109,7 @@ importers:
|
||||
version: 3.3.0
|
||||
jotai:
|
||||
specifier: ^2.8.4
|
||||
version: 2.20.0(@babel/core@7.29.0)(@babel/template@7.28.6)(@types/react@18.3.28)(react@18.3.1)
|
||||
version: 2.20.0(@babel/core@7.23.6)(@babel/template@7.28.6)(@types/react@18.3.28)(react@18.3.1)
|
||||
js-yaml:
|
||||
specifier: ^4.1.0
|
||||
version: 4.1.1
|
||||
@@ -205,7 +205,7 @@ importers:
|
||||
version: 6.4.1(css-to-react-native@3.2.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
umi-presets-pro:
|
||||
specifier: ^2.0.3
|
||||
version: 2.0.3(@babel/core@7.29.0)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(antd@6.3.7(date-fns@2.30.0)(moment@2.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(chokidar@3.6.0)(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(encoding@0.1.13)(rc-field-form@2.7.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3)(umi@4.6.51(@babel/core@7.29.0)(@types/node@25.6.2)(@types/react@18.3.28)(eslint@9.39.4(jiti@2.7.0))(lightningcss@1.22.1)(prettier@3.8.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.29.0)(react@18.3.1))(stylelint@14.8.2)(terser@5.48.0)(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14)))
|
||||
version: 2.0.3(@babel/core@7.23.6)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(antd@6.3.7(date-fns@2.30.0)(moment@2.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(chokidar@3.6.0)(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(encoding@0.1.13)(rc-field-form@2.7.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3)(umi@4.6.51(@babel/core@7.23.6)(@types/node@25.6.2)(@types/react@18.3.28)(eslint@9.39.4(jiti@2.7.0))(lightningcss@1.22.1)(prettier@3.8.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.23.6)(react@18.3.1))(stylelint@14.8.2)(terser@5.48.0)(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14)))
|
||||
wavesurfer.js:
|
||||
specifier: ^7.8.8
|
||||
version: 7.12.6
|
||||
@@ -233,10 +233,10 @@ importers:
|
||||
version: 1.0.1
|
||||
'@umijs/plugins':
|
||||
specifier: ^4.4.11
|
||||
version: 4.6.51(@babel/core@7.29.0)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(antd@6.3.7(date-fns@2.30.0)(moment@2.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(rc-field-form@2.7.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
version: 4.6.51(@babel/core@7.23.6)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(antd@6.3.7(date-fns@2.30.0)(moment@2.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(rc-field-form@2.7.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
babel-plugin-named-asset-import:
|
||||
specifier: ^0.3.8
|
||||
version: 0.3.8(@babel/core@7.29.0)
|
||||
version: 0.3.8(@babel/core@7.23.6)
|
||||
case-sensitive-paths-webpack-plugin:
|
||||
specifier: ^2.4.0
|
||||
version: 2.4.0
|
||||
@@ -1484,24 +1484,24 @@ packages:
|
||||
resolution: {integrity: sha512-KWk80UPIzPmUg+P0rKh6TqspRw0G6eux1PuJr+zz47ftMaZ9QDwbGzHZbtzWkl5hgayM/qrKRutllRC7D/vVXQ==, tarball: https://registry.npmjs.org/@formatjs/intl-utils/-/intl-utils-2.3.0.tgz}
|
||||
deprecated: the package is rather renamed to @formatjs/ecma-abstract with some changes in functionality (primarily selectUnit is removed and we don't plan to make any further changes to this package
|
||||
|
||||
'@gpustack/core-ui@1.0.16':
|
||||
resolution: {integrity: sha512-wFKDv7X0FXRmAmZPt4WKYV0+aGHcx82/3EZSJkrut/49XQd5XdrqKaimGtlFWFCNEWqsouwzs4uqaC7JFqfOkQ==, tarball: https://registry.npmjs.org/@gpustack/core-ui/-/core-ui-1.0.16.tgz}
|
||||
'@gpustack/core-ui@1.0.32':
|
||||
resolution: {integrity: sha512-kGTazoqbK2KyZgOP6gmQaRxTiQVfF2IKLGDXjJq6w6BbmJgALXFJA2v2ROjAbjEVyfTdBzyYeXfPo/JgISpMNw==, tarball: https://registry.npmjs.org/@gpustack/core-ui/-/core-ui-1.0.32.tgz}
|
||||
peerDependencies:
|
||||
'@ant-design/icons': '>=6.0.0'
|
||||
'@ant-design/icons': ^6.1.0
|
||||
'@ant-design/pro-components': 3.1.0-0
|
||||
'@monaco-editor/react': ^4.6.0
|
||||
ahooks: '>=3.0.0'
|
||||
antd: '>=6.0.0'
|
||||
antd-style: '>=3.0.0'
|
||||
axios: '>=1.8.0'
|
||||
echarts: '>=5.0.0'
|
||||
ahooks: ^3.8.5
|
||||
antd: ^6.3.3
|
||||
antd-style: ^3.6.2
|
||||
axios: ^1.8.2
|
||||
echarts: ^5.5.1
|
||||
file-saver: ^2.0.5
|
||||
monaco-editor: ^0.30.1
|
||||
monaco-yaml: ^4.0.0
|
||||
overlayscrollbars-react: ^0.5.6
|
||||
react: '>=18.0.0'
|
||||
react-dom: '>=18.0.0'
|
||||
styled-components: '>=6.0.0'
|
||||
react: ^18.2.0
|
||||
react-dom: ^18.2.0
|
||||
styled-components: ^6.1.15
|
||||
|
||||
'@hono/node-server@1.19.14':
|
||||
resolution: {integrity: sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==, tarball: https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.14.tgz}
|
||||
@@ -9257,14 +9257,14 @@ snapshots:
|
||||
'@radix-ui/popper': 0.0.10
|
||||
react: 18.3.1
|
||||
|
||||
'@alita/plugins@3.5.5(@babel/core@7.29.0)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(antd@6.3.7(date-fns@2.30.0)(moment@2.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(rc-field-form@2.7.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
|
||||
'@alita/plugins@3.5.5(@babel/core@7.23.6)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(antd@6.3.7(date-fns@2.30.0)(moment@2.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(rc-field-form@2.7.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
|
||||
dependencies:
|
||||
'@alita/babel-transform-jsx-class': 0.0.2
|
||||
'@alita/inspx': 0.0.2(react@18.3.1)
|
||||
'@alita/request': 3.1.2
|
||||
'@alita/types': 3.1.2
|
||||
'@umijs/bundler-utils': 4.4.11
|
||||
'@umijs/plugins': 4.4.11(@babel/core@7.29.0)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(antd@6.3.7(date-fns@2.30.0)(moment@2.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(rc-field-form@2.7.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
'@umijs/plugins': 4.4.11(@babel/core@7.23.6)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(antd@6.3.7(date-fns@2.30.0)(moment@2.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(rc-field-form@2.7.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
'@umijs/utils': 4.4.11
|
||||
ahooks: 3.9.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
antd-mobile-alita: 2.3.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
@@ -10134,90 +10134,90 @@ snapshots:
|
||||
dependencies:
|
||||
'@babel/types': 7.29.0
|
||||
|
||||
'@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.29.0)':
|
||||
'@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.23.6)':
|
||||
dependencies:
|
||||
'@babel/core': 7.29.0
|
||||
'@babel/core': 7.23.6
|
||||
'@babel/helper-plugin-utils': 7.28.6
|
||||
|
||||
'@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.29.0)':
|
||||
'@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.23.6)':
|
||||
dependencies:
|
||||
'@babel/core': 7.29.0
|
||||
'@babel/core': 7.23.6
|
||||
'@babel/helper-plugin-utils': 7.28.6
|
||||
|
||||
'@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.29.0)':
|
||||
'@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.23.6)':
|
||||
dependencies:
|
||||
'@babel/core': 7.29.0
|
||||
'@babel/core': 7.23.6
|
||||
'@babel/helper-plugin-utils': 7.28.6
|
||||
|
||||
'@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.29.0)':
|
||||
'@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.23.6)':
|
||||
dependencies:
|
||||
'@babel/core': 7.29.0
|
||||
'@babel/core': 7.23.6
|
||||
'@babel/helper-plugin-utils': 7.28.6
|
||||
|
||||
'@babel/plugin-syntax-import-attributes@7.28.6(@babel/core@7.29.0)':
|
||||
'@babel/plugin-syntax-import-attributes@7.28.6(@babel/core@7.23.6)':
|
||||
dependencies:
|
||||
'@babel/core': 7.29.0
|
||||
'@babel/core': 7.23.6
|
||||
'@babel/helper-plugin-utils': 7.28.6
|
||||
|
||||
'@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.29.0)':
|
||||
'@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.23.6)':
|
||||
dependencies:
|
||||
'@babel/core': 7.29.0
|
||||
'@babel/core': 7.23.6
|
||||
'@babel/helper-plugin-utils': 7.28.6
|
||||
|
||||
'@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.29.0)':
|
||||
'@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.23.6)':
|
||||
dependencies:
|
||||
'@babel/core': 7.29.0
|
||||
'@babel/core': 7.23.6
|
||||
'@babel/helper-plugin-utils': 7.28.6
|
||||
|
||||
'@babel/plugin-syntax-jsx@7.28.6(@babel/core@7.29.0)':
|
||||
'@babel/plugin-syntax-jsx@7.28.6(@babel/core@7.23.6)':
|
||||
dependencies:
|
||||
'@babel/core': 7.29.0
|
||||
'@babel/core': 7.23.6
|
||||
'@babel/helper-plugin-utils': 7.28.6
|
||||
|
||||
'@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.29.0)':
|
||||
'@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.23.6)':
|
||||
dependencies:
|
||||
'@babel/core': 7.29.0
|
||||
'@babel/core': 7.23.6
|
||||
'@babel/helper-plugin-utils': 7.28.6
|
||||
|
||||
'@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.29.0)':
|
||||
'@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.23.6)':
|
||||
dependencies:
|
||||
'@babel/core': 7.29.0
|
||||
'@babel/core': 7.23.6
|
||||
'@babel/helper-plugin-utils': 7.28.6
|
||||
|
||||
'@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.29.0)':
|
||||
'@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.23.6)':
|
||||
dependencies:
|
||||
'@babel/core': 7.29.0
|
||||
'@babel/core': 7.23.6
|
||||
'@babel/helper-plugin-utils': 7.28.6
|
||||
|
||||
'@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.29.0)':
|
||||
'@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.23.6)':
|
||||
dependencies:
|
||||
'@babel/core': 7.29.0
|
||||
'@babel/core': 7.23.6
|
||||
'@babel/helper-plugin-utils': 7.28.6
|
||||
|
||||
'@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.29.0)':
|
||||
'@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.23.6)':
|
||||
dependencies:
|
||||
'@babel/core': 7.29.0
|
||||
'@babel/core': 7.23.6
|
||||
'@babel/helper-plugin-utils': 7.28.6
|
||||
|
||||
'@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.29.0)':
|
||||
'@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.23.6)':
|
||||
dependencies:
|
||||
'@babel/core': 7.29.0
|
||||
'@babel/core': 7.23.6
|
||||
'@babel/helper-plugin-utils': 7.28.6
|
||||
|
||||
'@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.29.0)':
|
||||
'@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.23.6)':
|
||||
dependencies:
|
||||
'@babel/core': 7.29.0
|
||||
'@babel/core': 7.23.6
|
||||
'@babel/helper-plugin-utils': 7.28.6
|
||||
|
||||
'@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.29.0)':
|
||||
'@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.23.6)':
|
||||
dependencies:
|
||||
'@babel/core': 7.29.0
|
||||
'@babel/core': 7.23.6
|
||||
'@babel/helper-plugin-utils': 7.28.6
|
||||
|
||||
'@babel/plugin-transform-modules-commonjs@7.23.3(@babel/core@7.29.0)':
|
||||
'@babel/plugin-transform-modules-commonjs@7.23.3(@babel/core@7.23.6)':
|
||||
dependencies:
|
||||
'@babel/core': 7.29.0
|
||||
'@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0)
|
||||
'@babel/core': 7.23.6
|
||||
'@babel/helper-module-transforms': 7.28.6(@babel/core@7.23.6)
|
||||
'@babel/helper-plugin-utils': 7.28.6
|
||||
'@babel/helper-simple-access': 7.27.1
|
||||
transitivePeerDependencies:
|
||||
@@ -10808,7 +10808,7 @@ snapshots:
|
||||
|
||||
'@formatjs/intl-utils@2.3.0': {}
|
||||
|
||||
'@gpustack/core-ui@1.0.16(czdvzceysqw7iv6pct2ucnb23e)':
|
||||
'@gpustack/core-ui@1.0.32(czdvzceysqw7iv6pct2ucnb23e)':
|
||||
dependencies:
|
||||
'@ant-design/icons': 6.2.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
'@ant-design/pro-components': 3.1.0-0(antd@6.3.7(date-fns@2.30.0)(moment@2.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
@@ -11829,7 +11829,7 @@ snapshots:
|
||||
|
||||
'@types/history@5.0.0':
|
||||
dependencies:
|
||||
history: 4.10.1
|
||||
history: 5.3.0
|
||||
|
||||
'@types/hoist-non-react-statics@3.3.7(@types/react@18.3.28)':
|
||||
dependencies:
|
||||
@@ -11915,7 +11915,7 @@ snapshots:
|
||||
'@types/history': 4.7.11
|
||||
'@types/react': 18.3.29
|
||||
'@types/react-router': 5.1.20
|
||||
redux: 3.7.2
|
||||
redux: 4.2.1
|
||||
|
||||
'@types/react-router@5.1.20':
|
||||
dependencies:
|
||||
@@ -12281,11 +12281,11 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@umijs/bundler-utoopack@4.6.51(styled-jsx@5.1.7(@babel/core@7.29.0)(react@18.3.1))(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14))':
|
||||
'@umijs/bundler-utoopack@4.6.51(styled-jsx@5.1.7(@babel/core@7.23.6)(react@18.3.1))(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14))':
|
||||
dependencies:
|
||||
'@umijs/bundler-utils': 4.6.51
|
||||
'@umijs/bundler-webpack': 4.6.51(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14))
|
||||
'@utoo/pack': 1.4.3(less-loader@11.1.0(less@4.1.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14)))(less@4.1.3)(postcss@8.5.14)(resolve-url-loader@5.0.0)(sass-loader@13.2.0(sass@1.54.0)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14)))(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.29.0)(react@18.3.1))
|
||||
'@utoo/pack': 1.4.3(less-loader@11.1.0(less@4.1.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14)))(less@4.1.3)(postcss@8.5.14)(resolve-url-loader@5.0.0)(sass-loader@13.2.0(sass@1.54.0)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14)))(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.23.6)(react@18.3.1))
|
||||
compression: 1.8.1
|
||||
connect-history-api-fallback: 2.0.0
|
||||
cors: 2.8.6
|
||||
@@ -12553,14 +12553,14 @@ snapshots:
|
||||
- supports-color
|
||||
- typescript
|
||||
|
||||
'@umijs/max@4.6.51(@babel/core@7.29.0)(@types/node@25.6.2)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(lightningcss@1.22.1)(prettier@3.8.3)(rc-field-form@2.7.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.29.0)(react@18.3.1))(terser@5.48.0)(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14))':
|
||||
'@umijs/max@4.6.51(@babel/core@7.23.6)(@types/node@25.6.2)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(lightningcss@1.22.1)(prettier@3.8.3)(rc-field-form@2.7.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.23.6)(react@18.3.1))(terser@5.48.0)(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14))':
|
||||
dependencies:
|
||||
'@umijs/lint': 4.6.51(eslint@8.35.0)(stylelint@14.8.2)(typescript@5.9.3)
|
||||
'@umijs/plugins': 4.6.51(@babel/core@7.29.0)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(antd@4.24.16(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(rc-field-form@2.7.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
'@umijs/plugins': 4.6.51(@babel/core@7.23.6)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(antd@4.24.16(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(rc-field-form@2.7.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
antd: 4.24.16(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
eslint: 8.35.0
|
||||
stylelint: 14.8.2
|
||||
umi: 4.6.51(@babel/core@7.29.0)(@types/node@25.6.2)(@types/react@18.3.28)(eslint@8.35.0)(lightningcss@1.22.1)(prettier@3.8.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.29.0)(react@18.3.1))(stylelint@14.8.2)(terser@5.48.0)(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14))
|
||||
umi: 4.6.51(@babel/core@7.23.6)(@types/node@25.6.2)(@types/react@18.3.28)(eslint@8.35.0)(lightningcss@1.22.1)(prettier@3.8.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.23.6)(react@18.3.1))(stylelint@14.8.2)(terser@5.48.0)(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14))
|
||||
transitivePeerDependencies:
|
||||
- '@babel/core'
|
||||
- '@rspack/core'
|
||||
@@ -12642,7 +12642,7 @@ snapshots:
|
||||
dependencies:
|
||||
tsx: 3.12.2
|
||||
|
||||
'@umijs/plugins@4.4.11(@babel/core@7.29.0)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(antd@6.3.7(date-fns@2.30.0)(moment@2.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(rc-field-form@2.7.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
|
||||
'@umijs/plugins@4.4.11(@babel/core@7.23.6)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(antd@6.3.7(date-fns@2.30.0)(moment@2.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(rc-field-form@2.7.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
|
||||
dependencies:
|
||||
'@ahooksjs/use-request': 2.8.15(react@18.3.1)
|
||||
'@ant-design/antd-theme-variable': 1.0.0
|
||||
@@ -12657,7 +12657,7 @@ snapshots:
|
||||
antd-dayjs-webpack-plugin: 1.0.6(dayjs@1.11.20)
|
||||
axios: 0.27.2
|
||||
babel-plugin-import: 1.13.8
|
||||
babel-plugin-styled-components: 2.1.4(@babel/core@7.29.0)(styled-components@6.1.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))
|
||||
babel-plugin-styled-components: 2.1.4(@babel/core@7.23.6)(styled-components@6.1.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))
|
||||
dayjs: 1.11.20
|
||||
dva-core: 2.0.4(redux@4.2.1)
|
||||
dva-immer: 1.0.2(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))
|
||||
@@ -12687,7 +12687,7 @@ snapshots:
|
||||
- react-native
|
||||
- supports-color
|
||||
|
||||
'@umijs/plugins@4.6.51(@babel/core@7.29.0)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(antd@4.24.16(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(rc-field-form@2.7.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
|
||||
'@umijs/plugins@4.6.51(@babel/core@7.23.6)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(antd@4.24.16(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(rc-field-form@2.7.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
|
||||
dependencies:
|
||||
'@ahooksjs/use-request': 2.8.15(react@18.3.1)
|
||||
'@ant-design/antd-theme-variable': 1.0.0
|
||||
@@ -12702,7 +12702,7 @@ snapshots:
|
||||
antd-dayjs-webpack-plugin: 1.0.6(dayjs@1.11.20)
|
||||
axios: 0.27.2
|
||||
babel-plugin-import: 1.13.8
|
||||
babel-plugin-styled-components: 2.1.4(@babel/core@7.29.0)(styled-components@6.1.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))
|
||||
babel-plugin-styled-components: 2.1.4(@babel/core@7.23.6)(styled-components@6.1.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))
|
||||
dayjs: 1.11.20
|
||||
dva-core: 2.0.4(redux@4.2.1)
|
||||
dva-immer: 1.0.2(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))
|
||||
@@ -12732,7 +12732,7 @@ snapshots:
|
||||
- react-native
|
||||
- supports-color
|
||||
|
||||
'@umijs/plugins@4.6.51(@babel/core@7.29.0)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(antd@6.3.7(date-fns@2.30.0)(moment@2.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(rc-field-form@2.7.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
|
||||
'@umijs/plugins@4.6.51(@babel/core@7.23.6)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(antd@6.3.7(date-fns@2.30.0)(moment@2.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(rc-field-form@2.7.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
|
||||
dependencies:
|
||||
'@ahooksjs/use-request': 2.8.15(react@18.3.1)
|
||||
'@ant-design/antd-theme-variable': 1.0.0
|
||||
@@ -12747,7 +12747,7 @@ snapshots:
|
||||
antd-dayjs-webpack-plugin: 1.0.6(dayjs@1.11.20)
|
||||
axios: 0.27.2
|
||||
babel-plugin-import: 1.13.8
|
||||
babel-plugin-styled-components: 2.1.4(@babel/core@7.29.0)(styled-components@6.1.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))
|
||||
babel-plugin-styled-components: 2.1.4(@babel/core@7.23.6)(styled-components@6.1.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))
|
||||
dayjs: 1.11.20
|
||||
dva-core: 2.0.4(redux@4.2.1)
|
||||
dva-immer: 1.0.2(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))
|
||||
@@ -12777,7 +12777,7 @@ snapshots:
|
||||
- react-native
|
||||
- supports-color
|
||||
|
||||
'@umijs/preset-umi@4.6.51(@types/node@25.6.2)(@types/react@18.3.28)(lightningcss@1.22.1)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.29.0)(react@18.3.1))(terser@5.48.0)(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14))':
|
||||
'@umijs/preset-umi@4.6.51(@types/node@25.6.2)(@types/react@18.3.28)(lightningcss@1.22.1)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.23.6)(react@18.3.1))(terser@5.48.0)(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14))':
|
||||
dependencies:
|
||||
'@iconify/utils': 2.1.1
|
||||
'@stagewise/toolbar': 0.6.2
|
||||
@@ -12787,7 +12787,7 @@ snapshots:
|
||||
'@umijs/bundler-esbuild': 4.6.51
|
||||
'@umijs/bundler-mako': 0.11.10(postcss@8.5.14)(sass@1.54.0)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14))
|
||||
'@umijs/bundler-utils': 4.6.51
|
||||
'@umijs/bundler-utoopack': 4.6.51(styled-jsx@5.1.7(@babel/core@7.29.0)(react@18.3.1))(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14))
|
||||
'@umijs/bundler-utoopack': 4.6.51(styled-jsx@5.1.7(@babel/core@7.23.6)(react@18.3.1))(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14))
|
||||
'@umijs/bundler-vite': 4.6.51(@types/node@25.6.2)(lightningcss@1.22.1)(postcss@8.5.14)(rollup@3.30.0)(sass@1.54.0)(terser@5.48.0)
|
||||
'@umijs/bundler-webpack': 4.6.51(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14))
|
||||
'@umijs/core': 4.6.51
|
||||
@@ -12870,13 +12870,13 @@ snapshots:
|
||||
react-helmet-async: 1.3.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
react-router-dom: 6.3.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
|
||||
'@umijs/request-record@1.1.4(umi@4.6.51(@babel/core@7.29.0)(@types/node@25.6.2)(@types/react@18.3.28)(eslint@9.39.4(jiti@2.7.0))(lightningcss@1.22.1)(prettier@3.8.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.29.0)(react@18.3.1))(stylelint@14.8.2)(terser@5.48.0)(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14)))':
|
||||
'@umijs/request-record@1.1.4(umi@4.6.51(@babel/core@7.23.6)(@types/node@25.6.2)(@types/react@18.3.28)(eslint@9.39.4(jiti@2.7.0))(lightningcss@1.22.1)(prettier@3.8.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.23.6)(react@18.3.1))(stylelint@14.8.2)(terser@5.48.0)(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14)))':
|
||||
dependencies:
|
||||
chokidar: 3.6.0
|
||||
express: 4.22.1
|
||||
lodash: 4.18.1
|
||||
prettier: 2.8.8
|
||||
umi: 4.6.51(@babel/core@7.29.0)(@types/node@25.6.2)(@types/react@18.3.28)(eslint@9.39.4(jiti@2.7.0))(lightningcss@1.22.1)(prettier@3.8.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.29.0)(react@18.3.1))(stylelint@14.8.2)(terser@5.48.0)(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14))
|
||||
umi: 4.6.51(@babel/core@7.23.6)(@types/node@25.6.2)(@types/react@18.3.28)(eslint@9.39.4(jiti@2.7.0))(lightningcss@1.22.1)(prettier@3.8.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.23.6)(react@18.3.1))(stylelint@14.8.2)(terser@5.48.0)(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14))
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
@@ -12892,13 +12892,13 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@umijs/test@4.6.51(@babel/core@7.29.0)':
|
||||
'@umijs/test@4.6.51(@babel/core@7.23.6)':
|
||||
dependencies:
|
||||
'@babel/plugin-transform-modules-commonjs': 7.23.3(@babel/core@7.29.0)
|
||||
'@babel/plugin-transform-modules-commonjs': 7.23.3(@babel/core@7.23.6)
|
||||
'@jest/types': 27.5.1
|
||||
'@umijs/bundler-utils': 4.6.51
|
||||
'@umijs/utils': 4.6.51
|
||||
babel-jest: 29.7.0(@babel/core@7.29.0)
|
||||
babel-jest: 29.7.0(@babel/core@7.23.6)
|
||||
esbuild: 0.21.4
|
||||
identity-obj-proxy: 3.0.0
|
||||
isomorphic-unfetch: 4.0.2
|
||||
@@ -13018,7 +13018,7 @@ snapshots:
|
||||
'@utoo/pack-win32-x64-msvc@1.4.3':
|
||||
optional: true
|
||||
|
||||
'@utoo/pack@1.4.3(less-loader@11.1.0(less@4.1.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14)))(less@4.1.3)(postcss@8.5.14)(resolve-url-loader@5.0.0)(sass-loader@13.2.0(sass@1.54.0)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14)))(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.29.0)(react@18.3.1))':
|
||||
'@utoo/pack@1.4.3(less-loader@11.1.0(less@4.1.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14)))(less@4.1.3)(postcss@8.5.14)(resolve-url-loader@5.0.0)(sass-loader@13.2.0(sass@1.54.0)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14)))(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.23.6)(react@18.3.1))':
|
||||
dependencies:
|
||||
'@babel/code-frame': 7.22.5
|
||||
'@hono/node-server': 1.19.14(hono@4.12.18)
|
||||
@@ -13039,7 +13039,7 @@ snapshots:
|
||||
sass-loader: 13.2.0(sass@1.54.0)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14))
|
||||
semver: 7.8.0
|
||||
send: 0.17.1
|
||||
styled-jsx: 5.1.7(@babel/core@7.29.0)(react@18.3.1)
|
||||
styled-jsx: 5.1.7(@babel/core@7.23.6)(react@18.3.1)
|
||||
ws: 8.20.0
|
||||
optionalDependencies:
|
||||
'@utoo/pack-darwin-arm64': 1.4.3
|
||||
@@ -13555,13 +13555,13 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- debug
|
||||
|
||||
babel-jest@29.7.0(@babel/core@7.29.0):
|
||||
babel-jest@29.7.0(@babel/core@7.23.6):
|
||||
dependencies:
|
||||
'@babel/core': 7.29.0
|
||||
'@babel/core': 7.23.6
|
||||
'@jest/transform': 29.7.0
|
||||
'@types/babel__core': 7.20.5
|
||||
babel-plugin-istanbul: 6.1.1
|
||||
babel-preset-jest: 29.6.3(@babel/core@7.29.0)
|
||||
babel-preset-jest: 29.6.3(@babel/core@7.23.6)
|
||||
chalk: 4.1.2
|
||||
graceful-fs: 4.2.11
|
||||
slash: 3.0.0
|
||||
@@ -13601,9 +13601,9 @@ snapshots:
|
||||
cosmiconfig: 7.1.0
|
||||
resolve: 1.22.12
|
||||
|
||||
babel-plugin-named-asset-import@0.3.8(@babel/core@7.29.0):
|
||||
babel-plugin-named-asset-import@0.3.8(@babel/core@7.23.6):
|
||||
dependencies:
|
||||
'@babel/core': 7.29.0
|
||||
'@babel/core': 7.23.6
|
||||
|
||||
babel-plugin-react-compiler@0.0.0-experimental-c23de8d-20240515:
|
||||
dependencies:
|
||||
@@ -13615,11 +13615,11 @@ snapshots:
|
||||
zod: 3.25.76
|
||||
zod-validation-error: 2.1.0(zod@3.25.76)
|
||||
|
||||
babel-plugin-styled-components@2.1.4(@babel/core@7.29.0)(styled-components@6.1.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)):
|
||||
babel-plugin-styled-components@2.1.4(@babel/core@7.23.6)(styled-components@6.1.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)):
|
||||
dependencies:
|
||||
'@babel/helper-annotate-as-pure': 7.27.3
|
||||
'@babel/helper-module-imports': 7.28.6
|
||||
'@babel/plugin-syntax-jsx': 7.28.6(@babel/core@7.29.0)
|
||||
'@babel/plugin-syntax-jsx': 7.28.6(@babel/core@7.23.6)
|
||||
lodash: 4.18.1
|
||||
picomatch: 2.3.2
|
||||
styled-components: 6.1.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
@@ -13627,30 +13627,30 @@ snapshots:
|
||||
- '@babel/core'
|
||||
- supports-color
|
||||
|
||||
babel-preset-current-node-syntax@1.2.0(@babel/core@7.29.0):
|
||||
babel-preset-current-node-syntax@1.2.0(@babel/core@7.23.6):
|
||||
dependencies:
|
||||
'@babel/core': 7.29.0
|
||||
'@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.29.0)
|
||||
'@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.29.0)
|
||||
'@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.29.0)
|
||||
'@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.29.0)
|
||||
'@babel/plugin-syntax-import-attributes': 7.28.6(@babel/core@7.29.0)
|
||||
'@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.29.0)
|
||||
'@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.29.0)
|
||||
'@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.29.0)
|
||||
'@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.29.0)
|
||||
'@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.29.0)
|
||||
'@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.29.0)
|
||||
'@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.29.0)
|
||||
'@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.29.0)
|
||||
'@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.29.0)
|
||||
'@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.29.0)
|
||||
'@babel/core': 7.23.6
|
||||
'@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.23.6)
|
||||
'@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.23.6)
|
||||
'@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.23.6)
|
||||
'@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.23.6)
|
||||
'@babel/plugin-syntax-import-attributes': 7.28.6(@babel/core@7.23.6)
|
||||
'@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.23.6)
|
||||
'@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.23.6)
|
||||
'@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.23.6)
|
||||
'@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.23.6)
|
||||
'@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.23.6)
|
||||
'@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.23.6)
|
||||
'@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.23.6)
|
||||
'@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.23.6)
|
||||
'@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.23.6)
|
||||
'@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.23.6)
|
||||
|
||||
babel-preset-jest@29.6.3(@babel/core@7.29.0):
|
||||
babel-preset-jest@29.6.3(@babel/core@7.23.6):
|
||||
dependencies:
|
||||
'@babel/core': 7.29.0
|
||||
'@babel/core': 7.23.6
|
||||
babel-plugin-jest-hoist: 29.6.3
|
||||
babel-preset-current-node-syntax: 1.2.0(@babel/core@7.29.0)
|
||||
babel-preset-current-node-syntax: 1.2.0(@babel/core@7.23.6)
|
||||
|
||||
babel-runtime-jsx-plus@0.1.5: {}
|
||||
|
||||
@@ -16410,9 +16410,9 @@ snapshots:
|
||||
|
||||
jiti@2.7.0: {}
|
||||
|
||||
jotai@2.20.0(@babel/core@7.29.0)(@babel/template@7.28.6)(@types/react@18.3.28)(react@18.3.1):
|
||||
jotai@2.20.0(@babel/core@7.23.6)(@babel/template@7.28.6)(@types/react@18.3.28)(react@18.3.1):
|
||||
optionalDependencies:
|
||||
'@babel/core': 7.29.0
|
||||
'@babel/core': 7.23.6
|
||||
'@babel/template': 7.28.6
|
||||
'@types/react': 18.3.28
|
||||
react: 18.3.1
|
||||
@@ -19805,12 +19805,12 @@ snapshots:
|
||||
css-to-react-native: 3.2.0
|
||||
react-dom: 18.3.1(react@18.3.1)
|
||||
|
||||
styled-jsx@5.1.7(@babel/core@7.29.0)(react@18.3.1):
|
||||
styled-jsx@5.1.7(@babel/core@7.23.6)(react@18.3.1):
|
||||
dependencies:
|
||||
client-only: 0.0.1
|
||||
react: 18.3.1
|
||||
optionalDependencies:
|
||||
'@babel/core': 7.29.0
|
||||
'@babel/core': 7.23.6
|
||||
|
||||
stylelint-config-recommended@7.0.0(stylelint@14.8.2):
|
||||
dependencies:
|
||||
@@ -20164,12 +20164,12 @@ snapshots:
|
||||
|
||||
ua-parser-js@0.7.41: {}
|
||||
|
||||
umi-presets-pro@2.0.3(@babel/core@7.29.0)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(antd@6.3.7(date-fns@2.30.0)(moment@2.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(chokidar@3.6.0)(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(encoding@0.1.13)(rc-field-form@2.7.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3)(umi@4.6.51(@babel/core@7.29.0)(@types/node@25.6.2)(@types/react@18.3.28)(eslint@9.39.4(jiti@2.7.0))(lightningcss@1.22.1)(prettier@3.8.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.29.0)(react@18.3.1))(stylelint@14.8.2)(terser@5.48.0)(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14))):
|
||||
umi-presets-pro@2.0.3(@babel/core@7.23.6)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(antd@6.3.7(date-fns@2.30.0)(moment@2.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(chokidar@3.6.0)(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(encoding@0.1.13)(rc-field-form@2.7.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3)(umi@4.6.51(@babel/core@7.23.6)(@types/node@25.6.2)(@types/react@18.3.28)(eslint@9.39.4(jiti@2.7.0))(lightningcss@1.22.1)(prettier@3.8.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.23.6)(react@18.3.1))(stylelint@14.8.2)(terser@5.48.0)(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14))):
|
||||
dependencies:
|
||||
'@alita/plugins': 3.5.5(@babel/core@7.29.0)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(antd@6.3.7(date-fns@2.30.0)(moment@2.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(rc-field-form@2.7.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
'@alita/plugins': 3.5.5(@babel/core@7.23.6)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(antd@6.3.7(date-fns@2.30.0)(moment@2.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(rc-field-form@2.7.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
'@umijs/max-plugin-openapi': 2.0.3(chokidar@3.6.0)(encoding@0.1.13)(typescript@5.9.3)
|
||||
'@umijs/plugins': 4.6.51(@babel/core@7.29.0)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(antd@6.3.7(date-fns@2.30.0)(moment@2.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(rc-field-form@2.7.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
'@umijs/request-record': 1.1.4(umi@4.6.51(@babel/core@7.29.0)(@types/node@25.6.2)(@types/react@18.3.28)(eslint@9.39.4(jiti@2.7.0))(lightningcss@1.22.1)(prettier@3.8.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.29.0)(react@18.3.1))(stylelint@14.8.2)(terser@5.48.0)(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14)))
|
||||
'@umijs/plugins': 4.6.51(@babel/core@7.23.6)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(antd@6.3.7(date-fns@2.30.0)(moment@2.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(rc-field-form@2.7.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
'@umijs/request-record': 1.1.4(umi@4.6.51(@babel/core@7.23.6)(@types/node@25.6.2)(@types/react@18.3.28)(eslint@9.39.4(jiti@2.7.0))(lightningcss@1.22.1)(prettier@3.8.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.23.6)(react@18.3.1))(stylelint@14.8.2)(terser@5.48.0)(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14)))
|
||||
swagger-ui-dist: 4.19.1
|
||||
transitivePeerDependencies:
|
||||
- '@babel/core'
|
||||
@@ -20193,17 +20193,17 @@ snapshots:
|
||||
isomorphic-fetch: 2.2.1
|
||||
qs: 6.15.1
|
||||
|
||||
umi@4.6.51(@babel/core@7.29.0)(@types/node@25.6.2)(@types/react@18.3.28)(eslint@8.35.0)(lightningcss@1.22.1)(prettier@3.8.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.29.0)(react@18.3.1))(stylelint@14.8.2)(terser@5.48.0)(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14)):
|
||||
umi@4.6.51(@babel/core@7.23.6)(@types/node@25.6.2)(@types/react@18.3.28)(eslint@8.35.0)(lightningcss@1.22.1)(prettier@3.8.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.23.6)(react@18.3.1))(stylelint@14.8.2)(terser@5.48.0)(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14)):
|
||||
dependencies:
|
||||
'@babel/runtime': 7.23.6
|
||||
'@umijs/bundler-utils': 4.6.51
|
||||
'@umijs/bundler-webpack': 4.6.51(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14))
|
||||
'@umijs/core': 4.6.51
|
||||
'@umijs/lint': 4.6.51(eslint@8.35.0)(stylelint@14.8.2)(typescript@5.9.3)
|
||||
'@umijs/preset-umi': 4.6.51(@types/node@25.6.2)(@types/react@18.3.28)(lightningcss@1.22.1)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.29.0)(react@18.3.1))(terser@5.48.0)(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14))
|
||||
'@umijs/preset-umi': 4.6.51(@types/node@25.6.2)(@types/react@18.3.28)(lightningcss@1.22.1)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.23.6)(react@18.3.1))(terser@5.48.0)(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14))
|
||||
'@umijs/renderer-react': 4.6.51(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
'@umijs/server': 4.6.51
|
||||
'@umijs/test': 4.6.51(@babel/core@7.29.0)
|
||||
'@umijs/test': 4.6.51(@babel/core@7.23.6)
|
||||
'@umijs/utils': 4.6.51
|
||||
prettier-plugin-organize-imports: 3.2.4(prettier@3.8.3)(typescript@5.9.3)
|
||||
prettier-plugin-packagejson: 2.4.3(prettier@3.8.3)
|
||||
@@ -20247,17 +20247,17 @@ snapshots:
|
||||
- webpack-hot-middleware
|
||||
- webpack-plugin-serve
|
||||
|
||||
umi@4.6.51(@babel/core@7.29.0)(@types/node@25.6.2)(@types/react@18.3.28)(eslint@9.39.4(jiti@2.7.0))(lightningcss@1.22.1)(prettier@3.8.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.29.0)(react@18.3.1))(stylelint@14.8.2)(terser@5.48.0)(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14)):
|
||||
umi@4.6.51(@babel/core@7.23.6)(@types/node@25.6.2)(@types/react@18.3.28)(eslint@9.39.4(jiti@2.7.0))(lightningcss@1.22.1)(prettier@3.8.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.23.6)(react@18.3.1))(stylelint@14.8.2)(terser@5.48.0)(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14)):
|
||||
dependencies:
|
||||
'@babel/runtime': 7.23.6
|
||||
'@umijs/bundler-utils': 4.6.51
|
||||
'@umijs/bundler-webpack': 4.6.51(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14))
|
||||
'@umijs/core': 4.6.51
|
||||
'@umijs/lint': 4.6.51(eslint@9.39.4(jiti@2.7.0))(stylelint@14.8.2)(typescript@5.9.3)
|
||||
'@umijs/preset-umi': 4.6.51(@types/node@25.6.2)(@types/react@18.3.28)(lightningcss@1.22.1)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.29.0)(react@18.3.1))(terser@5.48.0)(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14))
|
||||
'@umijs/preset-umi': 4.6.51(@types/node@25.6.2)(@types/react@18.3.28)(lightningcss@1.22.1)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.23.6)(react@18.3.1))(terser@5.48.0)(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14))
|
||||
'@umijs/renderer-react': 4.6.51(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
'@umijs/server': 4.6.51
|
||||
'@umijs/test': 4.6.51(@babel/core@7.29.0)
|
||||
'@umijs/test': 4.6.51(@babel/core@7.23.6)
|
||||
'@umijs/utils': 4.6.51
|
||||
prettier-plugin-organize-imports: 3.2.4(prettier@3.8.3)(typescript@5.9.3)
|
||||
prettier-plugin-packagejson: 2.4.3(prettier@3.8.3)
|
||||
|
||||
+22
-1
@@ -1,6 +1,10 @@
|
||||
import { applyAccessExtensions } from './access.extensions';
|
||||
|
||||
export default (initialState: { currentUser?: Global.UserInfo }) => {
|
||||
export default (initialState: {
|
||||
currentUser?: Global.UserInfo;
|
||||
hasKubernetesCluster?: boolean;
|
||||
hasResourceEvents?: boolean;
|
||||
}) => {
|
||||
const isPlatformAdmin = !!(
|
||||
initialState &&
|
||||
initialState.currentUser &&
|
||||
@@ -11,6 +15,16 @@ export default (initialState: { currentUser?: Global.UserInfo }) => {
|
||||
initialState.currentUser &&
|
||||
!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:
|
||||
// * `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
|
||||
// (Dashboard, Resources, Models, Cluster Management). Defaults
|
||||
// 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
|
||||
// specific org context (member / group management). Defaults to
|
||||
// `false`; extensions widen when both an org is selected AND
|
||||
@@ -27,6 +46,8 @@ export default (initialState: { currentUser?: Global.UserInfo }) => {
|
||||
return applyAccessExtensions({
|
||||
canSeeAdmin: isPlatformAdmin,
|
||||
canSeeOrgAdmin: isPlatformAdmin,
|
||||
canSeeGpuService:
|
||||
isPlatformAdmin || hasKubernetesCluster !== false || hasResourceEvents,
|
||||
canManageCurrentOrg: false,
|
||||
canSeeUser,
|
||||
canDelete: true,
|
||||
|
||||
+55
-3
@@ -1,8 +1,9 @@
|
||||
import { userSettingsHelperAtom } from '@/atoms/settings';
|
||||
import { GPUStackVersionAtom, UpdateCheckAtom } from '@/atoms/user';
|
||||
import { GPUStackVersionAtom, UpdateCheckAtom, userAtom } from '@/atoms/user';
|
||||
import { setAtomStorage } from '@/atoms/utils';
|
||||
import { DEFAULT_ENTER_PAGE, GPUSTACK_API_BASE_URL } from '@/config/settings';
|
||||
import { COLOR_PRIMARY } from '@/config/theme/constants';
|
||||
import { getGPUStackPlugin } from '@/plugins';
|
||||
import { enterprisePluginReady } from '@/plugins/enterprise-ready';
|
||||
import { GPUStackPluginManager } from '@/plugins/manager';
|
||||
import { requestConfig } from '@/request-config';
|
||||
@@ -13,6 +14,11 @@ import {
|
||||
} from '@/services/profile/apis';
|
||||
import { fetchSystemConfig } from '@/services/system/query-system-config';
|
||||
import { isOnline } from '@/utils';
|
||||
import {
|
||||
markInitialStateProbed,
|
||||
probeAccessFlags
|
||||
} from '@/utils/access-probes';
|
||||
import { installTenantFetch } from '@/utils/install-fetch';
|
||||
import {
|
||||
IS_FIRST_LOGIN,
|
||||
readState,
|
||||
@@ -22,6 +28,8 @@ import '@gpustack/core-ui/style.css';
|
||||
import { RequestConfig, history, request as umiRequest } from '@umijs/max';
|
||||
import { message } from 'antd';
|
||||
|
||||
installTenantFetch();
|
||||
|
||||
// only for the first login and access from http://localhost
|
||||
|
||||
const checkDefaultPage = async (userInfo: any) => {
|
||||
@@ -39,6 +47,8 @@ export async function getInitialState(): Promise<{
|
||||
fetchUserInfo: () => Promise<Global.UserInfo>;
|
||||
currentUser?: Global.UserInfo;
|
||||
pluginData?: Record<string, any>;
|
||||
hasKubernetesCluster?: boolean;
|
||||
hasResourceEvents?: boolean;
|
||||
}> {
|
||||
const { location } = history;
|
||||
|
||||
@@ -83,6 +93,36 @@ export async function getInitialState(): Promise<{
|
||||
getUpdateCheck();
|
||||
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;
|
||||
} catch (error: any) {
|
||||
const data = error?.response?.data;
|
||||
@@ -122,12 +162,24 @@ export async function getInitialState(): Promise<{
|
||||
getAppVersionInfo();
|
||||
|
||||
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);
|
||||
return {
|
||||
fetchUserInfo,
|
||||
currentUser: userInfo,
|
||||
pluginData
|
||||
pluginData,
|
||||
...accessFlags
|
||||
};
|
||||
}
|
||||
return {
|
||||
|
||||
@@ -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 {
|
||||
border-inline: none;
|
||||
border-radius: 0;
|
||||
padding-inline-end: 0;
|
||||
padding-block-end: 8px;
|
||||
}
|
||||
|
||||
|
||||
@@ -63,6 +63,13 @@ export const fromClusterCreationAtom = atom(false);
|
||||
export const clusterSessionAtom = atom<{
|
||||
firstAddWorker: 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);
|
||||
|
||||
export const clusterDetailAtom = atom<ClusterListItem | null>(null);
|
||||
|
||||
@@ -1,10 +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);
|
||||
|
||||
export const addSSHKeyPageAtom = atom<{ create: boolean }>({
|
||||
create: false
|
||||
});
|
||||
@@ -55,3 +55,10 @@ export const userSettingsHelperAtom = atom(
|
||||
}
|
||||
);
|
||||
export const hideModalTemporarilyAtom = atom<boolean>(false);
|
||||
|
||||
export const collapsedMenuGroupsAtom = atomWithStorage<string[]>(
|
||||
'collapsedMenuGroups',
|
||||
[],
|
||||
undefined,
|
||||
{ getOnInit: true }
|
||||
);
|
||||
|
||||
+50
-10
@@ -3,6 +3,15 @@ import { atomWithStorage } from 'jotai/utils';
|
||||
|
||||
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<{
|
||||
version: string;
|
||||
git_commit: string;
|
||||
@@ -23,10 +32,7 @@ export const UpdateCheckAtom = atom<{
|
||||
latest_version: ''
|
||||
});
|
||||
|
||||
export const initialPasswordAtom = atomWithStorage<string>(
|
||||
'initialPassword',
|
||||
''
|
||||
);
|
||||
export const initialPasswordAtom = atom<string>('');
|
||||
|
||||
// Namespace the server creates for an Org's resources on each Kubernetes
|
||||
// cluster. The format must match the backend's ``get_namespace_name``
|
||||
@@ -81,23 +87,57 @@ const getStoredCurrentOrgId = (): number | null => {
|
||||
// cluster-owner fallback.
|
||||
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;
|
||||
// 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);
|
||||
for (const key of ORG_CACHE_KEYS) {
|
||||
try {
|
||||
const raw = localStorage.getItem(key);
|
||||
if (!raw) continue;
|
||||
const list = JSON.parse(raw) as Array<{ id: number; name?: string }>;
|
||||
const list = JSON.parse(raw) as CachedOrg[];
|
||||
if (!Array.isArray(list)) continue;
|
||||
const match = list.find((item) => String(item?.id) === target);
|
||||
if (match?.name) return `gpustack-${match.name}`;
|
||||
if (match) return match;
|
||||
} catch {
|
||||
// ignore malformed cache; continue checking other keys
|
||||
}
|
||||
}
|
||||
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());
|
||||
};
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Binary file not shown.
Binary file not shown.
Vendored
+6
@@ -92,6 +92,12 @@ declare namespace Global {
|
||||
interface InitialStateType {
|
||||
fetchUserInfo: () => Promise<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 };
|
||||
|
||||
@@ -54,7 +54,8 @@ export default {
|
||||
itemHoverColor: 'rgba(0,0,0,1)',
|
||||
itemColor: 'rgba(0,0,0,1)',
|
||||
itemHoverBg: 'rgb(24 25 27)',
|
||||
itemActiveBg: 'rgb(24 25 27)'
|
||||
itemActiveBg: 'rgb(24 25 27)',
|
||||
menuItemSelectedBg: '#292929'
|
||||
},
|
||||
Progress: {
|
||||
lineBorderRadius: 4
|
||||
@@ -108,6 +109,7 @@ export default {
|
||||
fontSize: 14,
|
||||
motion: true,
|
||||
colorFill: '#0A0A0A',
|
||||
colorBgBase: '#0A0A0A'
|
||||
colorBgBase: '#0A0A0A',
|
||||
menuItemSelectedBg: '#292929'
|
||||
}
|
||||
};
|
||||
|
||||
@@ -56,7 +56,8 @@ export default {
|
||||
itemHoverColor: 'rgba(0,0,0,1)',
|
||||
itemColor: 'rgba(0,0,0,1)',
|
||||
itemHoverBg: 'rgba(0,0,0,0.04)',
|
||||
itemActiveBg: 'rgba(0,0,0,0.04)'
|
||||
itemActiveBg: 'rgba(0,0,0,0.04)',
|
||||
menuItemSelectedBg: '#e8eaed'
|
||||
},
|
||||
Progress: {
|
||||
lineBorderRadius: 3
|
||||
|
||||
+26
-39
@@ -171,8 +171,6 @@ body {
|
||||
}
|
||||
|
||||
.ant-table .ant-table-container table {
|
||||
// border-spacing: 0 20px;
|
||||
|
||||
.ant-table-thead th.ant-table-column-sort {
|
||||
background-color: transparent;
|
||||
|
||||
@@ -241,8 +239,8 @@ body {
|
||||
height: 100%;
|
||||
|
||||
.ant-pro-sider-footer {
|
||||
padding-block: 2px 0;
|
||||
padding-left: 6px;
|
||||
padding-block: 4px 0;
|
||||
padding-left: 8px;
|
||||
}
|
||||
|
||||
.ant-pro-sider-actions-list-item {
|
||||
@@ -259,46 +257,26 @@ body {
|
||||
border-block-end: none;
|
||||
}
|
||||
|
||||
.ant-pro-sider-logo-collapsed {
|
||||
padding-left: 12px;
|
||||
cursor: e-resize;
|
||||
|
||||
.collapse-wrap {
|
||||
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-logo {
|
||||
.collapse-btn {
|
||||
color: var(--ant-color-text-tertiary);
|
||||
:hover {
|
||||
color: var(--ant-color-text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ant-pro-sider .ant-layout-sider-children {
|
||||
// border-right: 1px solid var(--ant-color-split);
|
||||
.ant-pro-sider-logo-collapsed {
|
||||
padding-left: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-table-content table {
|
||||
.ant-table-tbody {
|
||||
.ant-table-row {
|
||||
border-radius: var(--table-td-radius);
|
||||
.ant-table .ant-table-tbody {
|
||||
.ant-table-row {
|
||||
border-radius: var(--table-td-radius);
|
||||
|
||||
> td {
|
||||
background-color: unset;
|
||||
border-bottom: 1px solid var(--ant-color-split);
|
||||
}
|
||||
> td {
|
||||
border-bottom: 1px solid var(--ant-color-split);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -347,7 +325,6 @@ body {
|
||||
.ant-pro-layout-container {
|
||||
overflow-x: auto;
|
||||
min-height: 100vh;
|
||||
// background-color: var(--ant-color-bg-container);
|
||||
}
|
||||
|
||||
.ant-pro-sider {
|
||||
@@ -661,10 +638,15 @@ body {
|
||||
inset: 53px auto auto 0 !important;
|
||||
border-radius: 0 0 var(--border-radius-base) var(--border-radius-base);
|
||||
border-top: none;
|
||||
max-height: 240px;
|
||||
min-height: 100px;
|
||||
.ant-cascader-menus {
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
&.gpu-selector {
|
||||
.ant-cascader-menu {
|
||||
max-height: 400px;
|
||||
height: unset;
|
||||
}
|
||||
.ant-cascader-menu:last-child {
|
||||
flex: auto;
|
||||
|
||||
@@ -812,6 +794,8 @@ body {
|
||||
}
|
||||
|
||||
.ant-pro-sider-logo + div {
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
&::-webkit-scrollbar {
|
||||
width: 0;
|
||||
}
|
||||
@@ -867,3 +851,6 @@ body {
|
||||
padding-right: 8px;
|
||||
padding-block: 8px;
|
||||
}
|
||||
.ant-select-multiple .ant-select-content {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
+113
-35
@@ -1,54 +1,132 @@
|
||||
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');
|
||||
|
||||
const DEFAULT_BRAND_HUE = 211; // brand color
|
||||
const COOL_HUE_START = 180;
|
||||
const COOL_HUE_END = 280;
|
||||
// Linear-style minimal/neutral palette: a TIGHT blue -> violet band in OKLCH
|
||||
// (perceptually uniform), kept moderate-chroma so fills read as clean and gentle
|
||||
// rather than candy-colored. We deliberately stay narrow and DON'T fan out to
|
||||
// green/magenta — in this aesthetic series are separated by LIGHTNESS, not by
|
||||
// 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 GOLDEN_RATIO_CONJUGATE = 0.618033988749895;
|
||||
|
||||
function colorStringToHue(input?: string): number | undefined {
|
||||
if (!input) return undefined;
|
||||
const color = toHsl(input);
|
||||
if (!color || typeof color.h !== 'number') return undefined;
|
||||
return color.h;
|
||||
}
|
||||
|
||||
export default function useCoolColors() {
|
||||
// const brandHue = useMemo(
|
||||
// () => colorStringToHue(userSettings.colorPrimary) ?? DEFAULT_BRAND_HUE,
|
||||
// [userSettings.colorPrimary]
|
||||
// );
|
||||
/**
|
||||
* Vivid, distinct cool accents — for places that need a handful of "primary"
|
||||
* colors, one per card/section (e.g. the summary trend cards), NOT a stacked
|
||||
* multi-series palette. Every color is anchor-quality (bright + saturated) and
|
||||
* 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 function useCoolAccents() {
|
||||
useMode(modeRgb);
|
||||
useMode(modeHsl);
|
||||
useMode(modeOklch);
|
||||
|
||||
const brandHue = DEFAULT_BRAND_HUE;
|
||||
const { isDarkTheme } = useUserSettings();
|
||||
|
||||
return useMemoizedFn((count: number): string[] => {
|
||||
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++) {
|
||||
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) {
|
||||
hue = brandHue - 5;
|
||||
} else {
|
||||
const offset = (i * GOLDEN_RATIO_CONJUGATE) % 1;
|
||||
hue = COOL_HUE_START + offset * COOL_HUE_RANGE;
|
||||
export default function useCoolColors() {
|
||||
useMode(modeRgb);
|
||||
useMode(modeOklch);
|
||||
|
||||
if (Math.abs(hue - brandHue) < 5) {
|
||||
hue = (hue + 10) % COOL_HUE_END;
|
||||
}
|
||||
}
|
||||
const { isDarkTheme } = useUserSettings();
|
||||
|
||||
const s = i === 0 ? 100 : 75 + (i % 3) * 5;
|
||||
const l = i === 0 ? 50 : 55 + (i % 2) * 5;
|
||||
return useMemoizedFn((count: number): string[] => {
|
||||
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;
|
||||
|
||||
@@ -18,6 +18,7 @@ export function useQueryDataList<
|
||||
Response = Array<ListItem>
|
||||
>(option: {
|
||||
key: string;
|
||||
manual?: boolean;
|
||||
responseType?: 'array' | 'object';
|
||||
fetchList: (
|
||||
params: Params,
|
||||
@@ -38,6 +39,7 @@ export function useQueryDataList<
|
||||
fetchList,
|
||||
getLabel,
|
||||
getValue,
|
||||
manual = true,
|
||||
responseType = 'array',
|
||||
errorMsg
|
||||
} = option;
|
||||
@@ -70,7 +72,7 @@ export function useQueryDataList<
|
||||
return responseType === 'array' ? res.items || [] : res;
|
||||
},
|
||||
{
|
||||
manual: true,
|
||||
manual: manual,
|
||||
debounceWait: option.debounceWait || 300,
|
||||
onSuccess: () => {},
|
||||
onError: (error) => {
|
||||
@@ -105,16 +107,18 @@ export function useQueryDataList<
|
||||
export function useQueryData<Detail, Params = any>(option: {
|
||||
key: string;
|
||||
delay?: number;
|
||||
manual?: boolean;
|
||||
fetchDetail: (params: Params, options?: any) => Promise<Detail>;
|
||||
getData?: (response: Detail, params?: any) => any;
|
||||
errorMsg?: string;
|
||||
}): {
|
||||
loading: boolean;
|
||||
detailData: Detail;
|
||||
manual?: boolean;
|
||||
cancelRequest: () => void;
|
||||
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 [detailData, setDetailData] = useState<Detail>({} as Detail);
|
||||
|
||||
@@ -142,7 +146,7 @@ export function useQueryData<Detail, Params = any>(option: {
|
||||
return res;
|
||||
},
|
||||
{
|
||||
manual: true,
|
||||
manual: manual,
|
||||
onSuccess: () => {},
|
||||
onError: (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 };
|
||||
}
|
||||
@@ -37,6 +37,7 @@ export default function useTableFetch<T>(
|
||||
key?: (typeof PaginationKey)[keyof typeof PaginationKey];
|
||||
fetchAPI: (params: any, options?: any) => Promise<Global.PageResponse<T>>;
|
||||
deleteAPI?: (id: number, params?: any) => Promise<any>;
|
||||
afterDelete?: (id?: number | number[]) => void;
|
||||
contentForDelete?: string;
|
||||
defaultData?: any[];
|
||||
events?: EventsType[];
|
||||
@@ -48,6 +49,7 @@ export default function useTableFetch<T>(
|
||||
const {
|
||||
fetchAPI,
|
||||
deleteAPI,
|
||||
afterDelete,
|
||||
contentForDelete,
|
||||
API,
|
||||
polling = false,
|
||||
@@ -363,7 +365,7 @@ export default function useTableFetch<T>(
|
||||
|
||||
// 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 =======
|
||||
if (!updateManually) {
|
||||
fetchData();
|
||||
@@ -390,6 +392,7 @@ export default function useTableFetch<T>(
|
||||
successIds.push(id);
|
||||
}
|
||||
);
|
||||
afterDelete?.(successIds);
|
||||
rowSelection.removeSelectedKeys(successIds);
|
||||
fetchData();
|
||||
return res;
|
||||
|
||||
@@ -14,7 +14,6 @@ type EventsType = 'CREATE' | 'UPDATE' | 'DELETE' | 'INSERT';
|
||||
export function useUpdateChunkedList(options: {
|
||||
events?: EventsType[];
|
||||
dataList?: any[];
|
||||
triggerAt?: React.MutableRefObject<number>;
|
||||
limit?: number;
|
||||
onCreate?: (args: any) => void;
|
||||
onUpdate?: (args: any) => void;
|
||||
@@ -24,9 +23,9 @@ export function useUpdateChunkedList(options: {
|
||||
filterFun?: (args: any) => boolean;
|
||||
mapFun?: (args: any) => any;
|
||||
computedID?: (d: object) => string;
|
||||
isNewItem?: (item: any) => boolean;
|
||||
}) {
|
||||
const { events = ['CREATE', 'DELETE', 'UPDATE', 'INSERT'], triggerAt } =
|
||||
options;
|
||||
const { events = ['CREATE', 'DELETE', 'UPDATE', 'INSERT'] } = options;
|
||||
const deletedIdsRef = useRef<Set<number | string>>(new Set());
|
||||
const cacheDataListRef = useRef<any[]>(options.dataList || []);
|
||||
const timerRef = useRef<any>(null);
|
||||
@@ -71,17 +70,14 @@ export function useUpdateChunkedList(options: {
|
||||
(sItem: any) => sItem.id === item.id
|
||||
);
|
||||
const updateItem = { ...item };
|
||||
if (updateIndex === -1 && !triggerAt?.current) {
|
||||
if (updateIndex === -1) {
|
||||
acc.push(updateItem);
|
||||
} else if (!triggerAt?.current) {
|
||||
} else {
|
||||
cacheDataListRef.current[updateIndex] = updateItem;
|
||||
}
|
||||
// only push items created after the watch started
|
||||
|
||||
// TODO: only push items created after triggerAt
|
||||
if (
|
||||
triggerAt?.current &&
|
||||
Date.parse(item.created_at) >= triggerAt.current
|
||||
) {
|
||||
if (options.isNewItem?.(item)) {
|
||||
latestCreateList.push(updateItem);
|
||||
}
|
||||
|
||||
@@ -93,6 +89,10 @@ export function useUpdateChunkedList(options: {
|
||||
...cacheDataListRef.current
|
||||
].slice(0, limit);
|
||||
|
||||
options.setDataList?.([...cacheDataListRef.current], {
|
||||
createdIds: newDataList.map((item) => item.id)
|
||||
});
|
||||
|
||||
options.onCreate?.(latestCreateList);
|
||||
}
|
||||
|
||||
@@ -103,10 +103,8 @@ export function useUpdateChunkedList(options: {
|
||||
cacheDataListRef.current = cacheDataListRef.current?.filter(
|
||||
(item: any) => {
|
||||
// collect deleted items
|
||||
if (triggerAt?.current) {
|
||||
if (ids?.includes(item.id)) {
|
||||
deletedList.push(item);
|
||||
}
|
||||
if (ids?.includes(item.id) && !options.isNewItem?.(item)) {
|
||||
deletedList.push(item);
|
||||
}
|
||||
return !ids?.includes(item.id);
|
||||
}
|
||||
@@ -133,10 +131,8 @@ export function useUpdateChunkedList(options: {
|
||||
updateItem,
|
||||
...cacheDataListRef.current.slice(0, limit - 1)
|
||||
];
|
||||
if (options.onUpdate && triggerAt?.current) {
|
||||
if (Date.parse(item.created_at) >= triggerAt.current) {
|
||||
options.onUpdate?.([updateItem]);
|
||||
}
|
||||
if (options.onUpdate && options.isNewItem?.(item)) {
|
||||
options.onUpdate?.([updateItem]);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -63,8 +63,7 @@ export default function useWatchList<T = Record<string, any>>(API: string) {
|
||||
listRequestTokenRef.current?.cancel?.();
|
||||
listRequestTokenRef.current = createAxiosToken();
|
||||
const params = {
|
||||
page: 1,
|
||||
perPage: 100
|
||||
page: -1
|
||||
};
|
||||
const res: any = await queryAllDataList(params, {
|
||||
token: listRequestTokenRef.current.token
|
||||
@@ -86,6 +85,7 @@ export default function useWatchList<T = Record<string, any>>(API: string) {
|
||||
|
||||
return {
|
||||
watchDataList,
|
||||
setWatchDataList,
|
||||
deleteItemFromCache: handleDeleteItemFromCache
|
||||
};
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import { useAtom } from 'jotai';
|
||||
import { useMemo } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { DEFAULT_ENTER_PAGE } from '../config/settings';
|
||||
import GithubStar from './github-star';
|
||||
|
||||
const NewLabel = styled.span`
|
||||
position: relative;
|
||||
@@ -210,14 +211,14 @@ export const ExtraContent = (props: { isDarkTheme?: boolean }) => {
|
||||
key: 'settings',
|
||||
label: (
|
||||
<span className="flex flex-center">
|
||||
<IconFont type="icon-settings-02" />
|
||||
<IconFont type="icon-preferences" />
|
||||
<span className="m-l-8" style={{ marginLeft: 8 }}>
|
||||
{intl?.formatMessage?.({ id: 'common.button.settings' })}
|
||||
{intl?.formatMessage?.({ id: 'common.preferences' })}
|
||||
</span>
|
||||
</span>
|
||||
),
|
||||
onClick: () => {
|
||||
history.push('/profile');
|
||||
history.push('/preferences');
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -260,6 +261,7 @@ export const ExtraContent = (props: { isDarkTheme?: boolean }) => {
|
||||
<Wrapper>
|
||||
{contextHolder}
|
||||
<PluginExtraField name="OrgSwitcher" isDarkTheme={isDarkTheme} />
|
||||
{process.env.ENABLE_ENTERPRISE !== 'true' && <GithubStar />}
|
||||
<div
|
||||
style={{
|
||||
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;
|
||||
+68
-38
@@ -1,6 +1,7 @@
|
||||
import { routeCacheAtom, setRouteCache } from '@/atoms/route-cache';
|
||||
import { userAtom } from '@/atoms/user';
|
||||
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 { DEFAULT_ENTER_PAGE, GPUSTACK_API_BASE_URL } from '@/config/settings';
|
||||
@@ -11,6 +12,7 @@ import useUserSettings from '@/hooks/use-user-settings';
|
||||
import useUserSettingsStorage from '@/hooks/use-user-settings-storage';
|
||||
import useAddResource from '@/pages/dashboard/hooks/use-add-resource';
|
||||
import { logout } from '@/pages/login/apis';
|
||||
import { didInitialStateProbe, probeAccessFlags } from '@/utils/access-probes';
|
||||
import {
|
||||
readColumnSettings,
|
||||
readState,
|
||||
@@ -20,11 +22,7 @@ import {
|
||||
import { useAccessMarkedRoutes } from '@@/plugin-access';
|
||||
import { useModel } from '@@/plugin-model';
|
||||
import { ProLayout } from '@ant-design/pro-components';
|
||||
import {
|
||||
CoreUIProvider,
|
||||
IconFont,
|
||||
useOverlayScroller
|
||||
} from '@gpustack/core-ui';
|
||||
import { CoreUIProvider, IconFont } from '@gpustack/core-ui';
|
||||
import {
|
||||
Access,
|
||||
Outlet,
|
||||
@@ -44,7 +42,7 @@ import {
|
||||
import { Button, ConfigProvider, Modal, theme } from 'antd';
|
||||
import { useAtom } from 'jotai';
|
||||
import 'overlayscrollbars/overlayscrollbars.css';
|
||||
import { useMemo, useRef } from 'react';
|
||||
import { useEffect, useMemo, useRef } from 'react';
|
||||
import { PageContainerInner } from '../pages/_components/page-box';
|
||||
import Exception from './Exception';
|
||||
import './Layout.css';
|
||||
@@ -71,9 +69,9 @@ const NO_CONTAINER_PAGES = [
|
||||
|
||||
const CHECK_RESOURCE_PATH = [
|
||||
'/resources/workers',
|
||||
'/cluster-management/clusters/list',
|
||||
'/cluster-management/credentials',
|
||||
'/cluster-management/clusters/create'
|
||||
'/resources/clusters/list',
|
||||
'/resources/credentials',
|
||||
'/resources/clusters/create'
|
||||
];
|
||||
|
||||
type NewRoute = IRoute & {
|
||||
@@ -134,9 +132,6 @@ const mapRoutes = (routes: IRoute[], role: string) => {
|
||||
};
|
||||
|
||||
export default (props: any) => {
|
||||
const { initialize: initialize } = useOverlayScroller({
|
||||
defer: false
|
||||
});
|
||||
const [, contextHolder] = Modal.useModal();
|
||||
const { themeData, setUserSettings, userSettings } = useUserSettings();
|
||||
const [userInfo] = useAtom(userAtom);
|
||||
@@ -161,6 +156,50 @@ export default (props: any) => {
|
||||
|
||||
const { initialState, loading, setInitialState } = initialInfo;
|
||||
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 = {
|
||||
title: '',
|
||||
@@ -240,30 +279,7 @@ export default (props: any) => {
|
||||
}, [userSettings.collapsed]);
|
||||
|
||||
const renderMenuHeader = (logo: React.ReactNode, title: React.ReactNode) => {
|
||||
return (
|
||||
<>
|
||||
{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>
|
||||
</>
|
||||
);
|
||||
return <>{logo}</>;
|
||||
};
|
||||
|
||||
const menuContentRender = (menuProps: any, defaultDom: React.ReactNode) => {
|
||||
@@ -356,7 +372,7 @@ export default (props: any) => {
|
||||
config={{
|
||||
apiBaseUrl: GPUSTACK_API_BASE_URL,
|
||||
theme: userSettings.theme,
|
||||
iconUrl: '//at.alicdn.com/t/c/font_4613488_jmdepcs90im.js',
|
||||
iconUrl: '',
|
||||
isDarkTheme: userSettings.isDarkTheme,
|
||||
defaultColorPrimary: COLOR_PRIMARY
|
||||
}}
|
||||
@@ -407,9 +423,23 @@ export default (props: any) => {
|
||||
openKeys={false}
|
||||
disableMobile={true}
|
||||
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}
|
||||
onMenuHeaderClick={onMenuHeaderClick}
|
||||
menuHeaderRender={renderMenuHeader}
|
||||
collapsed={userSettings.collapsed}
|
||||
onPageChange={onPageChange}
|
||||
formatMessage={formatMessage}
|
||||
|
||||
@@ -256,7 +256,7 @@ export const getRightRenderContent = (opts: {
|
||||
</span>
|
||||
),
|
||||
onClick: () => {
|
||||
history.push('/profile');
|
||||
history.push('/preferences');
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
+187
-158
@@ -1,9 +1,11 @@
|
||||
import { collapsedMenuGroupsAtom } from '@/atoms/settings';
|
||||
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 { Tooltip } from 'antd';
|
||||
import { createStyles } from 'antd-style';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { createStyles, type FullToken } from 'antd-style';
|
||||
import { useAtom } from 'jotai';
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
interface MenuItem {
|
||||
icon?: string;
|
||||
@@ -20,124 +22,143 @@ interface SiderMenuProps {
|
||||
initialState: Global.InitialStateType;
|
||||
}
|
||||
|
||||
const useStyles = createStyles(({ css, token }) => {
|
||||
console.log('useStyles', token);
|
||||
const useStyles = createStyles(
|
||||
({ css, token }: { css: any; token: FullToken }) => {
|
||||
console.log('useStyles', token);
|
||||
|
||||
// @ts-ignore
|
||||
const { Menu } = token;
|
||||
// @ts-ignore
|
||||
const { Menu } = token;
|
||||
|
||||
return {
|
||||
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;
|
||||
return {
|
||||
siderMenu: css`
|
||||
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;
|
||||
`
|
||||
};
|
||||
});
|
||||
&.sider-menu-collapsed {
|
||||
.menu-item {
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
.os-scrollbar-vertical .os-scrollbar-handle {
|
||||
min-width: 4px;
|
||||
max-width: 4px;
|
||||
}
|
||||
`,
|
||||
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);
|
||||
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 { menuData, collapsed } = props;
|
||||
const { styles, cx } = useStyles();
|
||||
const location = useLocation();
|
||||
const [collapseKeys, setCollapseKeys] = useState<Set<string>>(new Set());
|
||||
console.log('SiderMenu', location.pathname);
|
||||
const [storedCollapsedGroups, setCollapsedGroups] = useAtom(
|
||||
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(() => {
|
||||
if (collapsed) {
|
||||
@@ -156,14 +177,11 @@ const SiderMenu: React.FC<SiderMenuProps> = (props) => {
|
||||
const handleToggleGroup = (e: any, menuGroup: any) => {
|
||||
e.stopPropagation();
|
||||
|
||||
console.log('handleToggleGroup', menuGroup.key);
|
||||
|
||||
if (collapseKeys.has(menuGroup.key)) {
|
||||
collapseKeys.delete(menuGroup.key);
|
||||
} else {
|
||||
collapseKeys.add(menuGroup.key);
|
||||
}
|
||||
setCollapseKeys(new Set(collapseKeys));
|
||||
setCollapsedGroups(
|
||||
collapsedGroups.includes(menuGroup.key)
|
||||
? collapsedGroups.filter((key) => key !== menuGroup.key)
|
||||
: [...collapsedGroups, menuGroup.key]
|
||||
);
|
||||
};
|
||||
|
||||
const menuItemRender = (menuItem: MenuItem, key: string) => {
|
||||
@@ -219,44 +237,55 @@ const SiderMenu: React.FC<SiderMenuProps> = (props) => {
|
||||
'sider-menu-collapsed': collapsed
|
||||
})}
|
||||
>
|
||||
{menuData.map((item: MenuItem, index: number) => (
|
||||
<div key={item.key}>
|
||||
{item.children && item.children.length > 0 ? (
|
||||
<>
|
||||
<div
|
||||
className={cx(styles.groupTitle, {
|
||||
'menu-item-group-title-collapsed': collapsed
|
||||
})}
|
||||
onClick={(e) => handleToggleGroup(e, item)}
|
||||
>
|
||||
{!collapsed ? (
|
||||
<span className="group-title-text">
|
||||
<span>{item.name}</span>
|
||||
<CaretDownOutlined
|
||||
rotate={collapseKeys.has(item.key) ? -90 : 0}
|
||||
></CaretDownOutlined>
|
||||
</span>
|
||||
) : (
|
||||
<span className={styles.line}></span>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className={cx(styles.menuItemGroup, {
|
||||
'menu-item-group-collapsed': collapsed,
|
||||
'menu-item-group-hidden':
|
||||
!collapsed && collapseKeys.has(item.key)
|
||||
})}
|
||||
>
|
||||
{item.children?.map((child: MenuItem) =>
|
||||
menuItemRender(child, child.key)
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
menuItemRender(item, item.key)
|
||||
)}
|
||||
<OverlayScroller
|
||||
styles={{
|
||||
wrapper: {
|
||||
paddingInline: 0,
|
||||
maxHeight: '100%'
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div style={{ paddingRight: 8 }}>
|
||||
{menuData.map((item: MenuItem, index: number) => (
|
||||
<div key={item.key}>
|
||||
{item.children && item.children.length > 0 ? (
|
||||
<>
|
||||
<div
|
||||
className={cx(styles.groupTitle, {
|
||||
'menu-item-group-title-collapsed': collapsed
|
||||
})}
|
||||
onClick={(e) => handleToggleGroup(e, item)}
|
||||
>
|
||||
{!collapsed ? (
|
||||
<span className="group-title-text">
|
||||
<span>{item.name}</span>
|
||||
<CaretDownOutlined
|
||||
rotate={collapseKeys.has(item.key) ? -90 : 0}
|
||||
></CaretDownOutlined>
|
||||
</span>
|
||||
) : (
|
||||
<span className={styles.line}></span>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className={cx(styles.menuItemGroup, {
|
||||
'menu-item-group-collapsed': collapsed,
|
||||
'menu-item-group-hidden':
|
||||
!collapsed && collapseKeys.has(item.key)
|
||||
})}
|
||||
>
|
||||
{item.children?.map((child: MenuItem) =>
|
||||
menuItemRender(child, child.key)
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
menuItemRender(item, item.key)
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</OverlayScroller>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -25,7 +25,7 @@ export default {
|
||||
'ai.provider.ollama': 'Ollama',
|
||||
'ai.provider.openai': 'OpenAI',
|
||||
'ai.provider.openrouter': 'OpenRouter',
|
||||
'ai.provider.qwen': 'Qwen',
|
||||
'ai.provider.qwen': 'Alibaba Cloud Model Studio',
|
||||
'ai.provider.spark': 'Spark',
|
||||
'ai.provider.stepfun': 'StepFun',
|
||||
'ai.provider.together-ai': 'TogetherAI',
|
||||
|
||||
@@ -23,5 +23,7 @@ export default {
|
||||
'apikeys.accessScope.inference': 'Inference APIs',
|
||||
'apikeys.access.permissions': 'Access Permissions',
|
||||
'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,11 +23,12 @@ export default {
|
||||
'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.defaultBackendParameters': 'Default Backend Parameters',
|
||||
'backend.form.parameterFormat': 'Parameter Format',
|
||||
'backend.form.parameterFormat.default': 'Backend Default',
|
||||
'backend.form.parameterFormat.space': 'Space (--key value)',
|
||||
'backend.form.parameterFormat.equal': 'Equal (--key=value)',
|
||||
'backend.form.commonParameters': 'Common Parameters',
|
||||
'backend.form.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',
|
||||
|
||||
@@ -37,10 +37,10 @@ export default {
|
||||
'Stress test for long-context handling. Evaluates KV cache behavior, memory usage, and backend stability.',
|
||||
'benchmark.form.profile.heavy.tips':
|
||||
'Decode-heavy generation benchmark. Measures sustained decoding speed and output token throughput.',
|
||||
'benchmark.table.filter.bygpu': 'Filter by GPU',
|
||||
'benchmark.table.filter.bymodel': 'Filter by Model',
|
||||
'benchmark.table.filter.bygpu': 'Search by GPU',
|
||||
'benchmark.table.filter.bymodel': 'Search by model',
|
||||
'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.columnSettings': 'Column Settings',
|
||||
'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,7 +39,7 @@ export default {
|
||||
'clusters.workerpool.batchSize.desc':
|
||||
'Number of workers created simultaneously in the Worker pool',
|
||||
'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':
|
||||
'On the Worker that needs to be added, run the following command to join it to the cluster.',
|
||||
'clusters.create.addCommand.k8s.tips':
|
||||
@@ -70,10 +70,13 @@ export default {
|
||||
'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.multiTag': 'Multi-select',
|
||||
'clusters.addworker.selectGPU.singleOnly':
|
||||
'The selected vendor is not in this cluster’s GPU vendor overrides — single-select only. Pick a vendor configured in the overrides to enable multi-select.',
|
||||
'clusters.addworker.selectGPU.subtitle':
|
||||
'You can select multiple GPU Vendors or none for CPU-only clusters',
|
||||
'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.dtkVersion': 'DTK Version',
|
||||
'clusters.addworker.runCommand': 'Run Command',
|
||||
'clusters.addworker.specifyWorkerIP': 'Worker IP',
|
||||
'clusters.addworker.detectWorkerIP': 'Auto-detect Worker IP',
|
||||
@@ -112,6 +115,8 @@ export default {
|
||||
'{count} new workers have been added to the cluster.',
|
||||
'clusters.create.serverUrl': 'GPUStack Server URL',
|
||||
'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.tips':
|
||||
'Specify a name for the worker container.',
|
||||
@@ -139,7 +144,7 @@ export default {
|
||||
'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.',
|
||||
'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.name': 'Volume Name',
|
||||
'clusters.volume.mountPath': 'Container Path',
|
||||
@@ -164,6 +169,10 @@ export default {
|
||||
'clusters.volume.configMap.name': 'ConfigMap Name',
|
||||
'clusters.volume.configMap.optional': 'Optional',
|
||||
'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',
|
||||
@@ -172,16 +181,20 @@ export default {
|
||||
'clusters.nodeSelector.title': 'Node Selector',
|
||||
'clusters.nodeSelector.tip':
|
||||
'Pod nodeSelector applied to every worker DaemonSet — only nodes whose labels match are eligible to run the worker.',
|
||||
'clusters.gpuVendorOverrides.title': 'GPU Vendor Overrides',
|
||||
'clusters.gpuVendorOverrides.validate.emptySelector':
|
||||
'The override for {vendor} needs at least one nodeSelector entry — an empty override would leave the multi-vendor manifest unable to target nodes for this runtime.',
|
||||
'clusters.gpuVendorOverrides.validate.duplicate':
|
||||
'Overrides for {v1} and {v2} use the same nodeSelector. Each vendor must scope to a distinct node set, otherwise their worker DaemonSets would compete for the same nodes.',
|
||||
'clusters.gpuVendorOverrides.validate.keyConflict':
|
||||
'Override for {vendor} reuses key(s) {keys} from the base Node Selector — the CPU worker would simultaneously require and forbid the key and never schedule.',
|
||||
'clusters.gpuVendorOverrides.tip':
|
||||
'Required when running multiple GPU vendors in one cluster. Each entry pins that vendor’s worker DaemonSet to nodes matching its nodeSelector, and the CPU worker is steered away from those nodes via a DoesNotExist node-affinity.',
|
||||
'clusters.gpuVendorOverrides.add': 'Add Override',
|
||||
'clusters.gpuVendorOverrides.vendor': 'GPU Vendor',
|
||||
'clusters.gpuVendorOverrides.nodeSelector': 'Node Selector'
|
||||
'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,6 +46,7 @@ export default {
|
||||
'common.button.enabled': 'Enabled',
|
||||
'common.button.disabled': 'Disabled',
|
||||
'common.button.upgrade': 'Upgrade',
|
||||
'common.enterprise.feature': 'Available in GPUStack Enterprise',
|
||||
'common.input.holder': 'Please enter',
|
||||
'common.validate.value': '{name} value is required',
|
||||
'common.button.edit': 'Edit',
|
||||
@@ -55,6 +56,7 @@ export default {
|
||||
'common.button.viewevent': 'View Events',
|
||||
'common.button.recreate': 'Recreate',
|
||||
'common.table.operation': 'Operations',
|
||||
'common.table.creator': 'Creator',
|
||||
'common.table.createTime': 'Created',
|
||||
'common.table.updateTime': 'Updated',
|
||||
'common.table.description': 'Description',
|
||||
@@ -65,6 +67,7 @@ export default {
|
||||
'common.search.name.placeholder': 'filter by name',
|
||||
'common.search.id.placeholder': 'filter by ID',
|
||||
'common.filter.byId': 'filter by ID',
|
||||
'common.filter.byCreator': 'Filter by creator',
|
||||
'common.table.type': 'Type',
|
||||
'common.table.default': 'Default Value',
|
||||
'common.copy.success': 'Copied success!',
|
||||
@@ -166,6 +169,7 @@ export default {
|
||||
'common.time.hour': 'hour',
|
||||
'common.time.minute': 'minutes',
|
||||
'common.issue.report': 'Report an issue',
|
||||
'common.github.star.tooltip': 'Star us on GitHub',
|
||||
'common.social.discord': 'Join Our Discord',
|
||||
'common.table.mark': 'Comment',
|
||||
'common.table.rollback.mark': 'Rollback Comment',
|
||||
@@ -194,6 +198,8 @@ export default {
|
||||
'common.table.user': 'User',
|
||||
'common.settings.instructions': 'Instructions',
|
||||
'common.settings.language': 'Language',
|
||||
'common.settings.language.tips':
|
||||
'Set the display language for the interface.',
|
||||
'common.delete.confirm':
|
||||
'Are you sure you want to delete the selected {type}?',
|
||||
'common.delete.single.confirm':
|
||||
@@ -247,6 +253,11 @@ export default {
|
||||
'common.appearance.tips': 'Default follows system preference.',
|
||||
'common.button.forgotpassword': 'Forgot password?',
|
||||
'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.refresh.tips':
|
||||
'The page may need to be updated. Try refreshing it!',
|
||||
@@ -257,6 +268,10 @@ export default {
|
||||
'common.login.auth': 'Authenticating...',
|
||||
'common.login.auth.failed': 'Authentication failed',
|
||||
'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.sso.noConfig':
|
||||
'Single sign-on is not enabled on this system. Please contact your administrator.',
|
||||
@@ -285,5 +300,7 @@ export default {
|
||||
'common.image.limit.height': 'Image height must be {height}.',
|
||||
'common.remaining': 'Remaining {count}',
|
||||
'common.max': 'Max {count}',
|
||||
'common.validate.group': 'Please complete the {group} configuration'
|
||||
'common.max.count': '{label} Count',
|
||||
'common.validate.group': 'Please complete the {group} configuration',
|
||||
'common.preferences': 'Preferences'
|
||||
};
|
||||
|
||||
@@ -13,7 +13,11 @@ export default {
|
||||
'gpuservice.template.command.placeholder':
|
||||
'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.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.tips':
|
||||
'The size of the container system disk.',
|
||||
'gpuservice.template.memory': 'Memory (GB)',
|
||||
'gpuservice.instance.containerDisk.remaining':
|
||||
'Container Disk (Max {count} GB)',
|
||||
@@ -77,6 +81,8 @@ export default {
|
||||
'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.instance.ssh.enable': 'Enable SSH Access',
|
||||
@@ -97,21 +103,32 @@ export default {
|
||||
'gpuservice.instance.templates': 'Instance Templates',
|
||||
'gpuservice.instance.section.storage': 'Storage',
|
||||
'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.required': 'Please enter the GPU count',
|
||||
'gpuservice.instance.gpuCount.max':
|
||||
'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.sliced': 'Sliced',
|
||||
'gpuservice.instance.memory': 'Memory',
|
||||
'gpuservice.instance.memory': 'VRAM',
|
||||
'gpuservice.instance.ram': 'RAM',
|
||||
'gpuservice.instance.os': 'OS',
|
||||
'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':
|
||||
'Search by template name, image or mount path',
|
||||
@@ -142,14 +159,13 @@ export default {
|
||||
'gpuservice.storage.capacity': 'Capacity',
|
||||
'gpuservice.storage.accessMode': 'Access Mode',
|
||||
'gpuservice.storage.persistent': 'Persistent',
|
||||
'gpuservice.storage.temporary': 'Temporary',
|
||||
'gpuservice.storage.persistentVolume': 'Persistent Volume',
|
||||
'gpuservice.storage.temporary': 'Ephemeral',
|
||||
'gpuservice.storage.persistentVolume': 'Persistent',
|
||||
'gpuservice.storage.temporary.tips':
|
||||
'Data is cleared when the instance stops.',
|
||||
'gpuservice.storage.persistentVolume.tips':
|
||||
'Data persists across restarts and is deleted only when the instance is terminated. Cannot be shared with other instances.',
|
||||
'gpuservice.storage.persistentVolume.required':
|
||||
'Please select a persistent volume',
|
||||
'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',
|
||||
@@ -159,5 +175,10 @@ export default {
|
||||
'gpuservice.storage.tempCapacity.required':
|
||||
'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."
|
||||
"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.video': 'Video',
|
||||
'menu.compare': 'Compare',
|
||||
'menu.models': 'Models',
|
||||
'menu.models': 'Model Service',
|
||||
'menu.models.modelList': 'Deploy & Manage',
|
||||
'menu.models.modelCatalog': 'Catalog',
|
||||
'menu.models.catalog': 'Model Catalog',
|
||||
@@ -27,20 +27,24 @@ export default {
|
||||
'menu.users': 'Users',
|
||||
'menu.resources.workers': 'Workers',
|
||||
'menu.resources.gpus': 'GPUs',
|
||||
'menu.resources.modelfiles': 'Model Files',
|
||||
'menu.models.modelfiles': 'Model Files',
|
||||
'menu.accessControl': 'Access Control',
|
||||
'menu.accessControl.apikeys': 'API Keys',
|
||||
'menu.accessControl.users': 'Users',
|
||||
'menu.profile': 'Profile',
|
||||
'menu.accessControl.organizations': 'Organizations',
|
||||
'menu.profile': 'Preferences',
|
||||
'menu.login': 'Login',
|
||||
'menu.usage': 'Usage',
|
||||
'menu.usage.usage': 'Usage',
|
||||
'menu.billingAndUsage': 'Usage & Billing',
|
||||
'menu.billingAndUsage.usage': 'Usage',
|
||||
'menu.billingAndUsage.billing': 'Billing',
|
||||
'menu.404': '404',
|
||||
'menu.clusterManagement': 'Cluster Management',
|
||||
'menu.clusterManagement.clusters': 'Clusters',
|
||||
'menu.clusterManagement.credentials': 'Cloud Credentials',
|
||||
'menu.clusterManagement.clusterDetail': 'Cluster Detail',
|
||||
'menu.clusterManagement.clusterCreate': 'Create Cluster',
|
||||
'menu.resources.backendsList': 'Inference Backends',
|
||||
'menu.resources.clusters': 'Clusters',
|
||||
'menu.resources.credentials': 'Cloud Credentials',
|
||||
'menu.resources.clusterDetail': 'Cluster Detail',
|
||||
'menu.resources.clusterCreate': 'Create Cluster',
|
||||
'menu.models.backendsList': 'Inference Backends',
|
||||
'menu.gpuService': 'GPU Service',
|
||||
'menu.gpuService.instances': 'GPU Instances',
|
||||
'menu.gpuService.templates': 'Instance Templates',
|
||||
|
||||
@@ -291,7 +291,7 @@ export default {
|
||||
'models.instance.startHistory': 'Run History',
|
||||
'models.instance.startHistory.tips':
|
||||
'Shows logs from the run before the last error-triggered restart.',
|
||||
'models.form.lora.label': 'LoRA Adapter',
|
||||
'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',
|
||||
|
||||
@@ -36,9 +36,12 @@ export default {
|
||||
'noresult.catalog.nofound': 'No matching models found.',
|
||||
'noresult.resources.cluster':
|
||||
'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':
|
||||
'No workers available. Add a worker to get started.',
|
||||
'noresult.resources.gotocluster': 'Create Your First Cluster',
|
||||
'noresult.resources.addk8scluster': 'Add a Kubernetes Cluster',
|
||||
'noresult.resources.gotoworker': 'Add Worker',
|
||||
'noresult.benchmark.title': 'No Benchmarks',
|
||||
'noresult.benchmark.subTitle': 'No benchmarks have been added yet.',
|
||||
|
||||
@@ -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'
|
||||
};
|
||||
@@ -17,9 +17,15 @@ export default {
|
||||
'usage.table.user.apiKeysUsed': 'API Keys Used',
|
||||
'usage.table.lastActive': 'Last Active',
|
||||
'usage.filter.granularity': 'Granularity',
|
||||
'usage.filter.granularity.hour': 'Hour',
|
||||
'usage.filter.granularity.day': 'Day',
|
||||
'usage.filter.granularity.week': 'Week',
|
||||
'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.apikeys': 'API Keys',
|
||||
'usage.tabs.users': 'User',
|
||||
@@ -32,5 +38,70 @@ export default {
|
||||
'usage.chart.cached': 'Cached',
|
||||
'usage.chart.uncached': '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.description':
|
||||
"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.empty': 'Please confirm the new password.',
|
||||
'users.password.confirm.error': 'The two passwords entered do not match.',
|
||||
|
||||
@@ -4,7 +4,7 @@ export default {
|
||||
'vendor.hygon': 'Hygon',
|
||||
'vendor.moorthreads': 'Moore Threads',
|
||||
'vendor.iluvatar': 'Iluvatar',
|
||||
'vendor.metax': 'Metax',
|
||||
'vendor.metax': 'MetaX',
|
||||
'vendor.cambricon': 'Cambricon',
|
||||
'vendor.thead': 'T-Head PPU'
|
||||
};
|
||||
|
||||
@@ -25,7 +25,7 @@ export default {
|
||||
'ai.provider.ollama': 'Ollama',
|
||||
'ai.provider.openai': 'OpenAI',
|
||||
'ai.provider.openrouter': 'OpenRouter',
|
||||
'ai.provider.qwen': 'Qwen',
|
||||
'ai.provider.qwen': 'Alibaba Cloud Model Studio',
|
||||
'ai.provider.spark': 'Spark',
|
||||
'ai.provider.stepfun': 'StepFun',
|
||||
'ai.provider.together-ai': 'TogetherAI',
|
||||
|
||||
@@ -23,7 +23,9 @@ export default {
|
||||
'apikeys.accessScope.inference': 'Inference APIs',
|
||||
'apikeys.access.permissions': 'Access Permissions',
|
||||
'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) ==========
|
||||
|
||||
@@ -23,11 +23,12 @@ export default {
|
||||
'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.defaultBackendParameters': 'Default Backend Parameters',
|
||||
'backend.form.parameterFormat': 'パラメータ形式',
|
||||
'backend.form.parameterFormat.default': 'バックエンドのデフォルト',
|
||||
'backend.form.parameterFormat.space': 'スペース区切り (--key value)',
|
||||
'backend.form.parameterFormat.equal': 'イコール連結 (--key=value)',
|
||||
'backend.form.commonParameters': 'よく使うパラメータ',
|
||||
'backend.form.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',
|
||||
|
||||
@@ -37,8 +37,8 @@ export default {
|
||||
'Stress test for long-context handling. Evaluates KV cache behavior, memory usage, and backend stability.',
|
||||
'benchmark.form.profile.heavy.tips':
|
||||
'Decode-heavy generation benchmark. Measures sustained decoding speed and output token throughput.',
|
||||
'benchmark.table.filter.bygpu': 'Filter by GPU',
|
||||
'benchmark.table.filter.bymodel': 'Filter by Model',
|
||||
'benchmark.table.filter.bygpu': 'GPU 検索',
|
||||
'benchmark.table.filter.bymodel': 'モデル検索',
|
||||
'benchmark.table.filter.bydataset': 'Filter by Dataset',
|
||||
'benchmark.table.filter.byProfile': 'Filter by Profile',
|
||||
'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,7 +39,7 @@ export default {
|
||||
'clusters.workerpool.batchSize.desc':
|
||||
'Number of workers created simultaneously in the Worker pool',
|
||||
'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':
|
||||
'On the Worker that needs to be added, run the following command to join it to the cluster.',
|
||||
'clusters.create.addCommand.k8s.tips':
|
||||
@@ -70,10 +70,13 @@ export default {
|
||||
'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.multiTag': 'Multi-select',
|
||||
'clusters.addworker.selectGPU.singleOnly':
|
||||
'The selected vendor is not in this cluster’s GPU vendor overrides — single-select only. Pick a vendor configured in the overrides to enable multi-select.',
|
||||
'clusters.addworker.selectGPU.subtitle':
|
||||
'複数の GPU ベンダーを選択するか、CPU クラスター専用の場合は選択不要です',
|
||||
'clusters.addworker.checkEnv': 'Check Environment',
|
||||
'clusters.addworker.checkEnv.cpuOnlyTips':
|
||||
'以下のコマンドを使用して、Kubernetes クラスターに少なくとも 1 つのレディーノードがあることを確認してください。CPU クラスターを登録しています。',
|
||||
'clusters.addworker.specifyArgs': 'Specify Arguments',
|
||||
'clusters.addworker.dtkVersion': 'DTK バージョン',
|
||||
'clusters.addworker.runCommand': 'Run Command',
|
||||
'clusters.addworker.specifyWorkerIP': 'Worker IP',
|
||||
'clusters.addworker.detectWorkerIP': 'Worker IP を自動検出',
|
||||
@@ -112,6 +115,8 @@ export default {
|
||||
'{count} new workers have been added to the cluster.',
|
||||
'clusters.create.serverUrl': 'GPUStack Server URL',
|
||||
'clusters.create.workerConfig': 'Worker Configuration',
|
||||
'clusters.edit.k8sOptions.changed.tip':
|
||||
'Kubernetes オプションを変更しました。変更を有効にするには、対象クラスターで登録コマンドを再実行してください。',
|
||||
'clusters.addworker.containerName': 'Worker Container Name',
|
||||
'clusters.addworker.containerName.tips':
|
||||
'Specify a name for the worker container.',
|
||||
@@ -139,7 +144,7 @@ export default {
|
||||
'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.',
|
||||
'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.name': 'Volume Name',
|
||||
'clusters.volume.mountPath': 'Container Path',
|
||||
@@ -164,6 +169,10 @@ export default {
|
||||
'clusters.volume.configMap.name': 'ConfigMap Name',
|
||||
'clusters.volume.configMap.optional': 'Optional',
|
||||
'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',
|
||||
@@ -172,18 +181,22 @@ export default {
|
||||
'clusters.nodeSelector.title': 'Node Selector',
|
||||
'clusters.nodeSelector.tip':
|
||||
'Pod nodeSelector applied to every worker DaemonSet — only nodes whose labels match are eligible to run the worker.',
|
||||
'clusters.gpuVendorOverrides.title': 'GPU Vendor Overrides',
|
||||
'clusters.gpuVendorOverrides.validate.emptySelector':
|
||||
'The override for {vendor} needs at least one nodeSelector entry — an empty override would leave the multi-vendor manifest unable to target nodes for this runtime.',
|
||||
'clusters.gpuVendorOverrides.validate.duplicate':
|
||||
'Overrides for {v1} and {v2} use the same nodeSelector. Each vendor must scope to a distinct node set, otherwise their worker DaemonSets would compete for the same nodes.',
|
||||
'clusters.gpuVendorOverrides.validate.keyConflict':
|
||||
'Override for {vendor} reuses key(s) {keys} from the base Node Selector — the CPU worker would simultaneously require and forbid the key and never schedule.',
|
||||
'clusters.gpuVendorOverrides.tip':
|
||||
'Required when running multiple GPU vendors in one cluster. Each entry pins that vendor’s worker DaemonSet to nodes matching its nodeSelector, and the CPU worker is steered away from those nodes via a DoesNotExist node-affinity.',
|
||||
'clusters.gpuVendorOverrides.add': 'Add Override',
|
||||
'clusters.gpuVendorOverrides.vendor': 'GPU Vendor',
|
||||
'clusters.gpuVendorOverrides.nodeSelector': 'Node Selector'
|
||||
'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) ==========
|
||||
|
||||
@@ -46,6 +46,7 @@ export default {
|
||||
'common.button.enabled': '有効',
|
||||
'common.button.disabled': '無効',
|
||||
'common.button.upgrade': 'アップグレード',
|
||||
'common.enterprise.feature': 'Available in GPUStack Enterprise',
|
||||
'common.input.holder': '入力してください',
|
||||
'common.validate.value': '{name} の値は必須です',
|
||||
'common.button.edit': '編集',
|
||||
@@ -55,6 +56,7 @@ export default {
|
||||
'common.button.viewevent': 'イベントを表示',
|
||||
'common.button.recreate': '再作成',
|
||||
'common.table.operation': '操作',
|
||||
'common.table.creator': '作成者',
|
||||
'common.table.createTime': '作成日時',
|
||||
'common.table.updateTime': '更新日時',
|
||||
'common.table.description': '説明',
|
||||
@@ -65,6 +67,7 @@ export default {
|
||||
'common.search.name.placeholder': '名前でフィルタ',
|
||||
'common.search.id.placeholder': 'IDでフィルタ',
|
||||
'common.filter.byId': 'IDでフィルタ',
|
||||
'common.filter.byCreator': '作成者でフィルタ',
|
||||
'common.table.type': 'タイプ',
|
||||
'common.table.default': 'デフォルト値',
|
||||
'common.copy.success': 'コピー成功!',
|
||||
@@ -167,6 +170,7 @@ export default {
|
||||
'common.time.hour': '時間',
|
||||
'common.time.minute': '分',
|
||||
'common.issue.report': '問題を報告',
|
||||
'common.github.star.tooltip': 'GitHub でスターをつける',
|
||||
'common.social.discord': 'Discordに参加',
|
||||
'common.table.mark': 'コメント',
|
||||
'common.table.rollback.mark': 'ロールバックコメント',
|
||||
@@ -195,6 +199,7 @@ export default {
|
||||
'common.table.user': 'ユーザー',
|
||||
'common.settings.instructions': '手順',
|
||||
'common.settings.language': '言語',
|
||||
'common.settings.language.tips': 'インターフェースの表示言語を設定します。',
|
||||
'common.delete.confirm': '選択した {type} を削除してもよろしいですか?',
|
||||
'common.delete.single.confirm':
|
||||
'<span style="font-size: 13px;font-weight: 700">{name}</span> を削除してもよろしいですか?',
|
||||
@@ -247,6 +252,11 @@ export default {
|
||||
'common.appearance.tips': 'Default follows system preference.',
|
||||
'common.button.forgotpassword': 'Forgot password?',
|
||||
'common.appearance.theme': 'Theme',
|
||||
'common.appearance.description':
|
||||
'デバイス上でのインターフェースの表示をカスタマイズします。',
|
||||
'common.security': 'セキュリティ',
|
||||
'common.security.description':
|
||||
'アカウントへのログインに使用するパスワードを管理します。',
|
||||
'common.page.wentwrong': 'Something went wrong.',
|
||||
'common.page.refresh.tips':
|
||||
'The page may need to be updated. Try refreshing it!',
|
||||
@@ -257,6 +267,10 @@ export default {
|
||||
'common.login.auth': 'Authenticating...',
|
||||
'common.login.auth.failed': 'Authentication failed',
|
||||
'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.sso.noConfig':
|
||||
'Single sign-on is not enabled on this system. Please contact your administrator.',
|
||||
@@ -285,7 +299,9 @@ export default {
|
||||
'common.image.limit.height': 'Image height must be {height}.',
|
||||
'common.remaining': '残り {count}',
|
||||
'common.max': '最大 {count}',
|
||||
'common.validate.group': 'Please complete the {group} configuration'
|
||||
'common.max.count': '{label} 数',
|
||||
'common.validate.group': 'Please complete the {group} configuration',
|
||||
'common.preferences': 'Preferences'
|
||||
};
|
||||
|
||||
// ========== To-Do: Translate Keys (Remove After Translation) ==========
|
||||
|
||||
@@ -13,7 +13,11 @@ export default {
|
||||
'gpuservice.template.command.placeholder':
|
||||
'引数はスペースで区切り、スペースを含む引数は引用符で囲んでください。例:/bin/bash -c "echo hello world"',
|
||||
'gpuservice.template.mountPath': 'マウントパス',
|
||||
'gpuservice.template.mountPath.tips':
|
||||
'このテンプレートからインスタンスを作成する際に、ストレージボリュームがデフォルトでマウントされるパスです。インスタンスの実行中に保持する必要があるデータの永続化に使用できます。',
|
||||
'gpuservice.template.containerDisk': 'コンテナディスク (GB)',
|
||||
'gpuservice.template.containerDisk.tips':
|
||||
'コンテナシステムディスクのサイズです。',
|
||||
'gpuservice.template.memory': 'メモリ (GB)',
|
||||
'gpuservice.instance.containerDisk.remaining':
|
||||
'コンテナディスク (最大 {count} GB)',
|
||||
@@ -76,6 +80,8 @@ export default {
|
||||
'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.instance.ssh.enable': 'SSH アクセスを有効化',
|
||||
@@ -96,20 +102,32 @@ export default {
|
||||
'gpuservice.instance.templates': 'インスタンステンプレート',
|
||||
'gpuservice.instance.section.storage': 'ストレージボリューム',
|
||||
'gpuservice.instance.type.required': 'インスタンスタイプを選択してください',
|
||||
'gpuservice.instance.type.noAvailable':
|
||||
'利用可能なインスタンスタイプがありません',
|
||||
'gpuservice.instance.gpuCount': 'GPU 数',
|
||||
'gpuservice.instance.gpuCount.required': 'GPU 数を入力してください',
|
||||
'gpuservice.instance.gpuCount.max':
|
||||
'最大 {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.sliced': '分割',
|
||||
'gpuservice.instance.memory': 'Memory',
|
||||
'gpuservice.instance.memory': 'VRAM',
|
||||
'gpuservice.instance.ram': 'RAM',
|
||||
'gpuservice.instance.os': 'OS',
|
||||
'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':
|
||||
'テンプレート名、イメージまたはマウントパスで検索',
|
||||
@@ -141,9 +159,9 @@ export default {
|
||||
'gpuservice.storage.accessMode': 'アクセスモード',
|
||||
'gpuservice.storage.persistent': '永続',
|
||||
'gpuservice.storage.temporary': '一時',
|
||||
'gpuservice.storage.persistentVolume': '永続ボリューム',
|
||||
'gpuservice.storage.persistentVolume': '永続',
|
||||
'gpuservice.storage.persistentVolume.required':
|
||||
'永続ボリュームを選択してください',
|
||||
'ストレージを選択してください',
|
||||
'gpuservice.storage.persistentVolume.capacity': '容量 (GB)',
|
||||
'gpuservice.storage.persistentVolume.capacity.required':
|
||||
'容量を入力してください',
|
||||
@@ -157,5 +175,10 @@ export default {
|
||||
'gpuservice.storage.temporary.tips':
|
||||
'Data is cleared when the instance stops.',
|
||||
'gpuservice.storage.persistentVolume.tips':
|
||||
'Data persists across restarts and is deleted only when the instance is terminated. Cannot be shared with other instances.'
|
||||
'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': 'グローバルテンプレート'
|
||||
};
|
||||
|
||||
+15
-11
@@ -8,7 +8,7 @@ export default {
|
||||
'menu.playground.text2images': '画像生成',
|
||||
'menu.playground.video': '動画',
|
||||
'menu.compare': '比較',
|
||||
'menu.models': 'モデル',
|
||||
'menu.models': 'モデルサービス',
|
||||
'menu.models.modelList': 'デプロイと管理',
|
||||
'menu.models.modelCatalog': 'カタログ',
|
||||
'menu.models.catalog': 'モデルカタログ',
|
||||
@@ -23,24 +23,28 @@ export default {
|
||||
'menu.resources': 'リソース',
|
||||
'menu.apikeys': 'APIキー',
|
||||
'menu.users': 'ユーザー',
|
||||
'menu.profile': 'プロフィール',
|
||||
'menu.profile': 'Preferences',
|
||||
'menu.login': 'ログイン',
|
||||
'menu.usage': '使用状況',
|
||||
'menu.usage.usage': '使用状況',
|
||||
'menu.billingAndUsage': '使用状況と請求',
|
||||
'menu.billingAndUsage.usage': '使用状況',
|
||||
'menu.billingAndUsage.billing': '請求',
|
||||
'menu.404': '404',
|
||||
'menu.settings': 'Settings',
|
||||
'menu.resources.workers': 'Workers',
|
||||
'menu.resources.gpus': 'GPUs',
|
||||
'menu.resources.modelfiles': 'Model Files',
|
||||
'menu.models.modelfiles': 'Model Files',
|
||||
'menu.accessControl': 'Access Control',
|
||||
'menu.accessControl.apikeys': 'API Keys',
|
||||
'menu.accessControl.users': 'Users',
|
||||
'menu.clusterManagement': 'Cluster Management',
|
||||
'menu.clusterManagement.clusters': 'Clusters',
|
||||
'menu.clusterManagement.credentials': 'Cloud Credentials',
|
||||
'menu.accessControl.organizations': 'Organizations',
|
||||
'menu.resources.clusters': 'Clusters',
|
||||
'menu.resources.credentials': 'Cloud Credentials',
|
||||
'menu.models.userModels': 'My Models',
|
||||
'menu.clusterManagement.clusterDetail': 'Cluster Detail',
|
||||
'menu.clusterManagement.clusterCreate': 'Create Cluster',
|
||||
'menu.resources.backendsList': 'Inference Backends',
|
||||
'menu.resources.clusterDetail': 'Cluster Detail',
|
||||
'menu.resources.clusterCreate': 'Create Cluster',
|
||||
'menu.models.backendsList': 'Inference Backends',
|
||||
'menu.gpuService': 'GPU Service',
|
||||
'menu.gpuService.instances': 'GPU Instances',
|
||||
'menu.gpuService.templates': 'Instance Templates',
|
||||
@@ -53,7 +57,7 @@ export default {
|
||||
// 1. 'menu.models.deployment': 'Deployment',
|
||||
// 2. 'menu.resources.workers': 'Workers',
|
||||
// 3. 'menu.resources.gpus': 'GPUs',
|
||||
// 4. 'menu.resources.modelfiles': 'Model Files',
|
||||
// 4. 'menu.models.modelfiles': 'Model Files',
|
||||
// 5. 'menu.accessControl': 'Access Control',
|
||||
// 6. 'menu.accessControl.apikeys': 'API Keys',
|
||||
// 7. 'menu.accessControl.users': 'Users',
|
||||
@@ -63,7 +67,7 @@ export default {
|
||||
// 11. 'menu.models.userModels': 'My Models'
|
||||
// 12. 'menu.clusterManagement.clusterDetail': 'Cluster Detail',
|
||||
// 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.provider': 'Provider',
|
||||
// 15. 'menu.models.providers': 'Provider',
|
||||
|
||||
@@ -291,7 +291,7 @@ export default {
|
||||
'models.instance.startHistory': 'Run History',
|
||||
'models.instance.startHistory.tips':
|
||||
'Shows logs from the run before the last error-triggered restart.',
|
||||
'models.form.lora.label': 'LoRA Adapter',
|
||||
'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',
|
||||
|
||||
@@ -36,9 +36,12 @@ export default {
|
||||
'noresult.catalog.nofound': 'No matching models found.',
|
||||
'noresult.resources.cluster':
|
||||
'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':
|
||||
'No workers available. Add a worker to get started.',
|
||||
'noresult.resources.gotocluster': 'Create Your First Cluster',
|
||||
'noresult.resources.addk8scluster': 'Add a Kubernetes Cluster',
|
||||
'noresult.resources.gotoworker': 'Add Worker',
|
||||
'noresult.benchmark.title': 'No Benchmarks',
|
||||
'noresult.benchmark.subTitle': 'No benchmarks have been added yet.',
|
||||
|
||||
@@ -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'
|
||||
};
|
||||
@@ -17,9 +17,15 @@ export default {
|
||||
'usage.table.user.apiKeysUsed': 'API Keys Used',
|
||||
'usage.table.lastActive': 'Last Active',
|
||||
'usage.filter.granularity': 'Granularity',
|
||||
'usage.filter.granularity.hour': '時間',
|
||||
'usage.filter.granularity.day': 'Day',
|
||||
'usage.filter.granularity.week': 'Week',
|
||||
'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.apikeys': 'API Keys',
|
||||
'usage.tabs.users': 'User',
|
||||
@@ -32,5 +38,70 @@ export default {
|
||||
'usage.chart.cached': 'Cached',
|
||||
'usage.chart.uncached': '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.description':
|
||||
'アカウントのセキュリティのため、初期パスワードを変更してください。',
|
||||
'users.password.modify.tips':
|
||||
'パスワードを定期的に更新すると、アカウントの安全を保てます。',
|
||||
'users.password.confirm': '新しいパスワードを確認',
|
||||
'users.password.confirm.empty': '新しいパスワードを確認してください。',
|
||||
'users.password.confirm.error': '入力された2つのパスワードが一致しません。',
|
||||
|
||||
@@ -4,7 +4,7 @@ export default {
|
||||
'vendor.hygon': 'Hygon',
|
||||
'vendor.moorthreads': 'Moore Threads',
|
||||
'vendor.iluvatar': 'Iluvatar',
|
||||
'vendor.metax': 'Metax',
|
||||
'vendor.metax': 'MetaX',
|
||||
'vendor.cambricon': 'Cambricon',
|
||||
'vendor.thead': 'T-Head PPU'
|
||||
};
|
||||
|
||||
@@ -25,7 +25,7 @@ export default {
|
||||
'ai.provider.ollama': 'Ollama',
|
||||
'ai.provider.openai': 'OpenAI',
|
||||
'ai.provider.openrouter': 'OpenRouter',
|
||||
'ai.provider.qwen': 'Qwen',
|
||||
'ai.provider.qwen': 'Alibaba Cloud Model Studio',
|
||||
'ai.provider.spark': 'Spark',
|
||||
'ai.provider.stepfun': 'StepFun',
|
||||
'ai.provider.together-ai': 'TogetherAI',
|
||||
|
||||
@@ -23,7 +23,9 @@ export default {
|
||||
'apikeys.accessScope.inference': 'Inference APIs',
|
||||
'apikeys.access.permissions': 'Access Permissions',
|
||||
'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) ==========
|
||||
|
||||
@@ -23,11 +23,12 @@ export default {
|
||||
'backend.form.defaultExecuteCommand': 'Команда выполнения по умолчанию',
|
||||
'backend.form.defaultExecuteCommand.tips': `'{{'model_path'}}', '{{'port'}}', '{{'worker_ip'}}' и '{{'model_name'}}' заполняются реальными значениями во время запуска`,
|
||||
'backend.form.defaultBackendParameters': 'Параметры бэкенда по умолчанию',
|
||||
'backend.form.parameterFormat': 'Формат параметров',
|
||||
'backend.form.parameterFormat.default': 'По умолчанию бэкенда',
|
||||
'backend.form.parameterFormat.space': 'Пробел (--key value)',
|
||||
'backend.form.parameterFormat.equal': 'Знак равенства (--key=value)',
|
||||
'backend.form.commonParameters': 'Часто используемые параметры',
|
||||
'backend.form.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': 'Конфигурация версий',
|
||||
|
||||
@@ -37,8 +37,8 @@ export default {
|
||||
'Stress test for long-context handling. Evaluates KV cache behavior, memory usage, and backend stability.',
|
||||
'benchmark.form.profile.heavy.tips':
|
||||
'Decode-heavy generation benchmark. Measures sustained decoding speed and output token throughput.',
|
||||
'benchmark.table.filter.bygpu': 'Filter by GPU',
|
||||
'benchmark.table.filter.bymodel': 'Filter by Model',
|
||||
'benchmark.table.filter.bygpu': 'Поиск GPU',
|
||||
'benchmark.table.filter.bymodel': 'Поиск модели',
|
||||
'benchmark.table.filter.bydataset': 'Filter by Dataset',
|
||||
'benchmark.table.filter.byProfile': 'Filter by Profile',
|
||||
'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,7 +39,7 @@ export default {
|
||||
'clusters.workerpool.batchSize.desc':
|
||||
'Количество воркеров, создаваемых одновременно в пуле воркеров',
|
||||
'clusters.create.addworker.tips':
|
||||
'Пожалуйста, убедитесь, что выполнены <a href={link} target="_blank">предварительные условия</a> для {label} перед выполнением следующей команды.',
|
||||
'Пожалуйста, убедитесь, что выполнены <a href={link} target="_blank">предварительные условия</a> перед выполнением следующей команды.',
|
||||
'clusters.create.addCommand.tips':
|
||||
'На воркере, который необходимо добавить, выполните следующую команду, чтобы присоединить его к кластеру.',
|
||||
'clusters.create.addCommand.k8s.tips':
|
||||
@@ -70,10 +70,13 @@ export default {
|
||||
'Для <span class="bold-text">не-Docker</span> кластеров, пожалуйста, регистрируйте кластеры или управляйте пулами воркеров на странице Кластеры.',
|
||||
'clusters.addworker.selectGPU': 'Выбрать производителя GPU',
|
||||
'clusters.addworker.selectGPU.multiTag': 'Multi-select',
|
||||
'clusters.addworker.selectGPU.singleOnly':
|
||||
'The selected vendor is not in this cluster’s GPU vendor overrides — single-select only. Pick a vendor configured in the overrides to enable multi-select.',
|
||||
'clusters.addworker.selectGPU.subtitle':
|
||||
'Вы можете выбрать несколько производителей GPU или не выбирать для кластера только с CPU',
|
||||
'clusters.addworker.checkEnv': 'Проверить окружение',
|
||||
'clusters.addworker.checkEnv.cpuOnlyTips':
|
||||
'Используйте следующую команду, чтобы убедиться, что в кластере Kubernetes есть хотя бы один готовый узел. Вы регистрируете кластер только с CPU.',
|
||||
'clusters.addworker.specifyArgs': 'Указать аргументы',
|
||||
'clusters.addworker.dtkVersion': 'Версия DTK',
|
||||
'clusters.addworker.runCommand': 'Выполнить команду',
|
||||
'clusters.addworker.specifyWorkerIP': 'Указать IP воркера',
|
||||
'clusters.addworker.detectWorkerIP': 'Автоматически определить IP воркера',
|
||||
@@ -112,6 +115,8 @@ export default {
|
||||
'{count} новых воркеров были добавлены в кластер.',
|
||||
'clusters.create.serverUrl': 'URL сервера GPUStack',
|
||||
'clusters.create.workerConfig': 'Конфигурация воркера',
|
||||
'clusters.edit.k8sOptions.changed.tip':
|
||||
'Вы изменили параметры Kubernetes. Чтобы изменения вступили в силу, повторно выполните команду регистрации в целевом кластере.',
|
||||
'clusters.addworker.containerName': 'Имя контейнера воркера',
|
||||
'clusters.addworker.containerName.tips':
|
||||
'Укажите имя для контейнера воркера.',
|
||||
@@ -140,7 +145,7 @@ export default {
|
||||
'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.',
|
||||
'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.name': 'Volume Name',
|
||||
'clusters.volume.mountPath': 'Container Path',
|
||||
@@ -165,6 +170,10 @@ export default {
|
||||
'clusters.volume.configMap.name': 'ConfigMap Name',
|
||||
'clusters.volume.configMap.optional': 'Optional',
|
||||
'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',
|
||||
@@ -173,18 +182,22 @@ export default {
|
||||
'clusters.nodeSelector.title': 'Node Selector',
|
||||
'clusters.nodeSelector.tip':
|
||||
'Pod nodeSelector applied to every worker DaemonSet — only nodes whose labels match are eligible to run the worker.',
|
||||
'clusters.gpuVendorOverrides.title': 'GPU Vendor Overrides',
|
||||
'clusters.gpuVendorOverrides.validate.emptySelector':
|
||||
'The override for {vendor} needs at least one nodeSelector entry — an empty override would leave the multi-vendor manifest unable to target nodes for this runtime.',
|
||||
'clusters.gpuVendorOverrides.validate.duplicate':
|
||||
'Overrides for {v1} and {v2} use the same nodeSelector. Each vendor must scope to a distinct node set, otherwise their worker DaemonSets would compete for the same nodes.',
|
||||
'clusters.gpuVendorOverrides.validate.keyConflict':
|
||||
'Override for {vendor} reuses key(s) {keys} from the base Node Selector — the CPU worker would simultaneously require and forbid the key and never schedule.',
|
||||
'clusters.gpuVendorOverrides.tip':
|
||||
'Required when running multiple GPU vendors in one cluster. Each entry pins that vendor’s worker DaemonSet to nodes matching its nodeSelector, and the CPU worker is steered away from those nodes via a DoesNotExist node-affinity.',
|
||||
'clusters.gpuVendorOverrides.add': 'Add Override',
|
||||
'clusters.gpuVendorOverrides.vendor': 'GPU Vendor',
|
||||
'clusters.gpuVendorOverrides.nodeSelector': 'Node Selector'
|
||||
'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) ==========
|
||||
|
||||
@@ -46,6 +46,7 @@ export default {
|
||||
'common.button.enabled': 'Активно',
|
||||
'common.button.disabled': 'Отключено',
|
||||
'common.button.upgrade': 'Обновить',
|
||||
'common.enterprise.feature': 'Available in GPUStack Enterprise',
|
||||
'common.input.holder': 'Введите значение',
|
||||
'common.validate.value': 'Поле {name} обязательно',
|
||||
'common.button.edit': 'Редактировать',
|
||||
@@ -55,6 +56,7 @@ export default {
|
||||
'common.button.viewevent': 'Просмотр событий',
|
||||
'common.button.recreate': 'Пересоздать',
|
||||
'common.table.operation': 'Действия',
|
||||
'common.table.creator': 'Создатель',
|
||||
'common.table.createTime': 'Создано',
|
||||
'common.table.updateTime': 'Обновлено',
|
||||
'common.table.description': 'Описание',
|
||||
@@ -65,6 +67,7 @@ export default {
|
||||
'common.search.name.placeholder': 'Фильтр по названию',
|
||||
'common.search.id.placeholder': 'Фильтр по ID',
|
||||
'common.filter.byId': 'Фильтр по ID',
|
||||
'common.filter.byCreator': 'Фильтр по создателю',
|
||||
'common.table.type': 'Тип',
|
||||
'common.table.default': 'Значение по умолчанию',
|
||||
'common.copy.success': 'Скопировано!',
|
||||
@@ -164,6 +167,7 @@ export default {
|
||||
'common.time.hour': 'Час',
|
||||
'common.time.minute': 'Минута',
|
||||
'common.issue.report': 'Сообщить о проблеме',
|
||||
'common.github.star.tooltip': 'Поставьте нам звезду на GitHub',
|
||||
'common.social.discord': 'Присоединиться к Discord',
|
||||
'common.table.mark': 'Комментарий',
|
||||
'common.table.rollback.mark': 'Комментарий к откату',
|
||||
@@ -193,6 +197,7 @@ export default {
|
||||
'common.table.user': 'Пользователь',
|
||||
'common.settings.instructions': 'Инструкции',
|
||||
'common.settings.language': 'Язык',
|
||||
'common.settings.language.tips': 'Задайте язык отображения интерфейса.',
|
||||
'common.delete.confirm': 'Вы уверены, что хотите удалить выбранный {type}?',
|
||||
'common.delete.single.confirm':
|
||||
'Вы уверены, что хотите удалить <span style="font-size: 13px;font-weight: 700">{name}</span>?',
|
||||
@@ -246,6 +251,11 @@ export default {
|
||||
'common.appearance.tips': 'По умолчанию соответствует системным настройкам.',
|
||||
'common.button.forgotpassword': 'Забыли пароль?',
|
||||
'common.appearance.theme': 'Тема',
|
||||
'common.appearance.description':
|
||||
'Настройте внешний вид интерфейса на вашем устройстве.',
|
||||
'common.security': 'Безопасность',
|
||||
'common.security.description':
|
||||
'Управляйте паролем для входа в учётную запись.',
|
||||
'common.page.wentwrong': 'Что-то пошло не так.',
|
||||
'common.page.refresh.tips':
|
||||
'Страница может нуждаться в обновлении. Попробуйте обновить её!',
|
||||
@@ -256,11 +266,15 @@ export default {
|
||||
'common.login.auth': 'Аутентификация...',
|
||||
'common.login.auth.failed': 'Ошибка аутентификации',
|
||||
'common.login.password': 'Войти с паролем',
|
||||
'common.login.username.holder': 'Введите имя пользователя',
|
||||
'common.login.password.holder': 'Введите пароль',
|
||||
'common.login.newpassword.holder': 'Введите новый пароль',
|
||||
'common.login.confirm.holder': 'Введите пароль ещё раз',
|
||||
'common.external.login': 'Войти через {type}',
|
||||
'common.sso.noConfig':
|
||||
'Единый вход не настроен в этой системе. Пожалуйста, обратитесь к администратору.',
|
||||
'common.button.edit.item': 'Редактировать {name}',
|
||||
'common.button.copy.item': 'Duplicate {name}',
|
||||
'common.button.copy.item': 'Дублировать {name}',
|
||||
'common.button.terminal': 'Терминал',
|
||||
'common.button.addItem': 'Добавить элемент',
|
||||
'common.help.default': 'По умолчанию: {content}',
|
||||
@@ -284,7 +298,9 @@ export default {
|
||||
'common.image.limit.height': 'Image height must be {height}.',
|
||||
'common.remaining': 'Остаток {count}',
|
||||
'common.max': 'Макс. {count}',
|
||||
'common.validate.group': 'Please complete the {group} configuration'
|
||||
'common.max.count': 'Количество {label}',
|
||||
'common.validate.group': 'Please complete the {group} configuration',
|
||||
'common.preferences': 'Preferences'
|
||||
};
|
||||
|
||||
// ========== To-Do: Translate Keys (Remove After Translation) ==========
|
||||
|
||||
@@ -14,7 +14,11 @@ export default {
|
||||
'gpuservice.template.command.placeholder':
|
||||
'Разделяйте аргументы пробелами; аргументы с пробелами заключайте в кавычки, например: /bin/bash -c "echo hello world"',
|
||||
'gpuservice.template.mountPath': 'Путь монтирования',
|
||||
'gpuservice.template.mountPath.tips':
|
||||
'Путь, по которому том хранилища монтируется по умолчанию при создании экземпляра из этого шаблона. Может использоваться для сохранения данных, которые нужно сохранить во время работы экземпляра.',
|
||||
'gpuservice.template.containerDisk': 'Диск контейнера (GB)',
|
||||
'gpuservice.template.containerDisk.tips':
|
||||
'Размер системного диска контейнера.',
|
||||
'gpuservice.template.memory': 'Память (GB)',
|
||||
'gpuservice.instance.containerDisk.remaining':
|
||||
'Диск контейнера (Макс. {count} GB)',
|
||||
@@ -80,6 +84,8 @@ export default {
|
||||
'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.instance.ssh.enable': 'Включить SSH-доступ',
|
||||
@@ -100,18 +106,27 @@ export default {
|
||||
'gpuservice.instance.templates': 'Шаблоны экземпляров',
|
||||
'gpuservice.instance.section.storage': 'Том хранилища',
|
||||
'gpuservice.instance.type.required': 'Выберите тип экземпляра',
|
||||
'gpuservice.instance.type.noAvailable': 'Нет доступных типов экземпляров',
|
||||
'gpuservice.instance.gpuCount': 'Количество GPU',
|
||||
'gpuservice.instance.gpuCount.required': 'Введите количество GPU',
|
||||
'gpuservice.instance.gpuCount.max': 'Выберите максимум {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.sliced': 'Разделено',
|
||||
'gpuservice.instance.memory': 'Память',
|
||||
'gpuservice.instance.memory': 'VRAM',
|
||||
'gpuservice.instance.ram': 'RAM',
|
||||
'gpuservice.instance.os': 'ОС',
|
||||
'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':
|
||||
'Поиск по имени шаблона, образу или пути монтирования',
|
||||
@@ -143,8 +158,8 @@ export default {
|
||||
'gpuservice.storage.accessMode': 'Режим доступа',
|
||||
'gpuservice.storage.persistent': 'Постоянное',
|
||||
'gpuservice.storage.temporary': 'Временное',
|
||||
'gpuservice.storage.persistentVolume': 'Постоянный том',
|
||||
'gpuservice.storage.persistentVolume.required': 'Выберите постоянный том',
|
||||
'gpuservice.storage.persistentVolume': 'Постоянное',
|
||||
'gpuservice.storage.persistentVolume.required': 'Выберите хранилище',
|
||||
'gpuservice.storage.persistentVolume.capacity': 'Ёмкость (ГБ)',
|
||||
'gpuservice.storage.persistentVolume.capacity.required': 'Введите ёмкость',
|
||||
'gpuservice.storage.persistentVolume.releaseWithInstance':
|
||||
@@ -157,5 +172,10 @@ export default {
|
||||
'gpuservice.storage.temporary.tips':
|
||||
'Data is cleared when the instance stops.',
|
||||
'gpuservice.storage.persistentVolume.tips':
|
||||
'Data persists across restarts and is deleted only when the instance is terminated. Cannot be shared with other instances.'
|
||||
'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.video': 'Видео',
|
||||
'menu.compare': 'Сравнение',
|
||||
'menu.models': 'Модели',
|
||||
'menu.models': 'Сервисы моделей',
|
||||
'menu.models.modelList': 'Развертывание и управление',
|
||||
'menu.models.modelCatalog': 'Каталог',
|
||||
'menu.models.catalog': 'Каталог моделей',
|
||||
@@ -23,23 +23,27 @@ export default {
|
||||
'menu.resources': 'Ресурсы',
|
||||
'menu.apikeys': 'API-ключи',
|
||||
'menu.users': 'Пользователи',
|
||||
'menu.profile': 'Профиль',
|
||||
'menu.profile': 'Preferences',
|
||||
'menu.login': 'Авторизация',
|
||||
'menu.usage': 'Использование',
|
||||
'menu.usage.usage': 'Использование',
|
||||
'menu.billingAndUsage': 'Использование и биллинг',
|
||||
'menu.billingAndUsage.usage': 'Использование',
|
||||
'menu.billingAndUsage.billing': 'Биллинг',
|
||||
'menu.404': 'Ошибка 404',
|
||||
'menu.resources.workers': 'Воркеры',
|
||||
'menu.resources.gpus': 'GPUs',
|
||||
'menu.resources.modelfiles': 'Файлы модлей',
|
||||
'menu.models.modelfiles': 'Файлы модлей',
|
||||
'menu.accessControl': 'Управление доступом',
|
||||
'menu.accessControl.apikeys': 'API Ключи',
|
||||
'menu.accessControl.users': 'Пользователи',
|
||||
'menu.clusterManagement': 'Управление кластерами',
|
||||
'menu.clusterManagement.clusters': 'Кластеры',
|
||||
'menu.clusterManagement.credentials': 'Облачные аккаунты',
|
||||
'menu.accessControl.organizations': 'Организации',
|
||||
'menu.resources.clusters': 'Кластеры',
|
||||
'menu.resources.credentials': 'Облачные аккаунты',
|
||||
'menu.models.userModels': 'Мои модели',
|
||||
'menu.clusterManagement.clusterDetail': 'Детали кластера',
|
||||
'menu.clusterManagement.clusterCreate': 'Создать кластер',
|
||||
'menu.resources.backendsList': 'Бэкенды запуска',
|
||||
'menu.resources.clusterDetail': 'Детали кластера',
|
||||
'menu.resources.clusterCreate': 'Создать кластер',
|
||||
'menu.models.backendsList': 'Бэкенды запуска',
|
||||
'menu.settings': 'Settings',
|
||||
'menu.gpuService': 'GPU Service',
|
||||
'menu.gpuService.instances': 'GPU Instances',
|
||||
|
||||
@@ -295,7 +295,7 @@ export default {
|
||||
'models.instance.startHistory': 'Run History',
|
||||
'models.instance.startHistory.tips':
|
||||
'Shows logs from the run before the last error-triggered restart.',
|
||||
'models.form.lora.label': 'LoRA Adapter',
|
||||
'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',
|
||||
|
||||
@@ -37,9 +37,12 @@ export default {
|
||||
'noresult.catalog.nofound': 'Подходящие модели не найдены.',
|
||||
'noresult.resources.cluster':
|
||||
'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':
|
||||
'No workers available. Add a worker to get started.',
|
||||
'noresult.resources.gotocluster': 'Create Your First Cluster',
|
||||
'noresult.resources.addk8scluster': 'Add a Kubernetes Cluster',
|
||||
'noresult.resources.gotoworker': 'Add Worker',
|
||||
'noresult.benchmark.title': 'No Benchmarks',
|
||||
'noresult.benchmark.subTitle': 'No benchmarks have been added yet.',
|
||||
@@ -67,8 +70,7 @@ export default {
|
||||
'noresult.gpuservice.storage.subTitle': 'Хранилища ещё не добавлены.',
|
||||
'noresult.gpuservice.storage.nofound': 'Подходящие хранилища не найдены.',
|
||||
'noresult.gpuservice.storageType.title': 'Нет типов хранилищ',
|
||||
'noresult.gpuservice.storageType.subTitle':
|
||||
'Типы хранилищ ещё не добавлены.',
|
||||
'noresult.gpuservice.storageType.subTitle': 'Типы хранилищ ещё не добавлены.',
|
||||
'noresult.gpuservice.storageType.nofound':
|
||||
'Подходящие типы хранилищ не найдены.',
|
||||
'noresult.gpuservice.sshkey.title': 'Нет открытых ключей 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'
|
||||
};
|
||||
@@ -17,9 +17,15 @@ export default {
|
||||
'usage.table.user.apiKeysUsed': 'API Keys Used',
|
||||
'usage.table.lastActive': 'Last Active',
|
||||
'usage.filter.granularity': 'Granularity',
|
||||
'usage.filter.granularity.hour': 'Час',
|
||||
'usage.filter.granularity.day': 'Day',
|
||||
'usage.filter.granularity.week': 'Week',
|
||||
'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.apikeys': 'API Keys',
|
||||
'usage.tabs.users': 'User',
|
||||
@@ -32,5 +38,70 @@ export default {
|
||||
'usage.chart.cached': 'Cached',
|
||||
'usage.chart.uncached': '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': 'ГБ-дни',
|
||||
'usage.metric.gbHours': 'ГБ-часы',
|
||||
'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. Один инстанс, работающий 2 часа = 2 часа инстанса.',
|
||||
'usage.metric.gbDays.tip':
|
||||
'Ёмкость хранилища, проинтегрированная по времени, в ГБ × дни: 10 ГБ в течение 5 дней = 50 ГБ-дней. (= ГБ-часы ÷ 24)',
|
||||
'usage.metric.gbHours.tip':
|
||||
'Ёмкость хранилища, проинтегрированная по времени, в ГБ × часы: 10 ГБ в течение 5 часов = 50 ГБ-часов.',
|
||||
|
||||
// --- 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': 'ГБ-дни по времени',
|
||||
|
||||
// --- 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.description':
|
||||
'В целях безопасности измените первоначальный пароль.',
|
||||
'users.password.modify.tips':
|
||||
'Регулярное обновление пароля помогает защитить вашу учётную запись.',
|
||||
'users.password.confirm': 'Подтвердите новый пароль',
|
||||
'users.password.confirm.empty': 'Подтвердите новый пароль',
|
||||
'users.password.confirm.error': 'Пароли не совпадают',
|
||||
|
||||
@@ -4,7 +4,7 @@ export default {
|
||||
'vendor.hygon': 'Hygon',
|
||||
'vendor.moorthreads': 'Moore Threads',
|
||||
'vendor.iluvatar': 'Iluvatar',
|
||||
'vendor.metax': 'Metax',
|
||||
'vendor.metax': 'MetaX',
|
||||
'vendor.cambricon': 'Cambricon',
|
||||
'vendor.thead': 'T-Head PPU'
|
||||
};
|
||||
|
||||
@@ -25,7 +25,7 @@ export default {
|
||||
'ai.provider.ollama': 'Ollama',
|
||||
'ai.provider.openai': 'OpenAI',
|
||||
'ai.provider.openrouter': 'OpenRouter',
|
||||
'ai.provider.qwen': 'Qwen',
|
||||
'ai.provider.qwen': 'Alibaba Cloud Model Studio',
|
||||
'ai.provider.spark': 'Spark',
|
||||
'ai.provider.stepfun': 'StepFun',
|
||||
'ai.provider.together-ai': 'TogetherAI',
|
||||
|
||||
@@ -23,5 +23,7 @@ export default {
|
||||
'apikeys.accessScope.inference': 'Inference APIs',
|
||||
'apikeys.access.permissions': 'Access Permissions',
|
||||
'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,11 +23,12 @@ export default {
|
||||
'backend.form.defaultExecuteCommand': 'Varsayılan Çalıştırma Komutu',
|
||||
'backend.form.defaultExecuteCommand.tips': `'{{'model_path'}}', '{{'port'}}', '{{'worker_ip'}}' ve '{{'model_name'}}' dağıtım sırasında gerçek değerlerle değiştirilecek yer tutuculardır.`,
|
||||
'backend.form.defaultBackendParameters': 'Varsayılan Altyapı Parametreleri',
|
||||
'backend.form.parameterFormat': 'Parametre Biçimi',
|
||||
'backend.form.parameterFormat.default': 'Varsayılan',
|
||||
'backend.form.parameterFormat.space': 'Boşluk (--key value)',
|
||||
'backend.form.parameterFormat.equal': 'Eşittir (--key=value)',
|
||||
'backend.form.commonParameters': 'Yaygın Parametreler',
|
||||
'backend.form.flagFormat': 'Bayrak Biçimi',
|
||||
'backend.form.flagFormat.tips':
|
||||
'Bir seçenek ile değerinin birleştirilme biçimi. Boş bırakılırsa her parametre girildiği biçimde korunur, biçim birleştirilmez.',
|
||||
'backend.form.flagFormat.space': 'Boşlukla Ayır (--key value)',
|
||||
'backend.form.flagFormat.equal': 'Eşittir İşareti (--key=value)',
|
||||
'backend.form.commonParameters': 'Ortak Altyapı Parametreleri',
|
||||
'backend.form.commonParameters.tips':
|
||||
'Dağıtım sırasında altyapı parametreleri girişinde öneri olarak gösterilir.',
|
||||
'backend.form.versionConfig': 'Sürüm Yapılandırması',
|
||||
|
||||
@@ -37,8 +37,8 @@ export default {
|
||||
'Uzun bağlam işleme için stres testi. KV önbellek davranışını, bellek kullanımını ve altyapı kararlılığını değerlendirir.',
|
||||
'benchmark.form.profile.heavy.tips':
|
||||
'Çözümleme ağırlıklı üretim kıyaslaması. Sürekli çözümleme hızını ve çıkış token verimini ölçer.',
|
||||
'benchmark.table.filter.bygpu': "GPU'ya göre filtrele",
|
||||
'benchmark.table.filter.bymodel': 'Modele göre filtrele',
|
||||
'benchmark.table.filter.bygpu': 'GPU ara',
|
||||
'benchmark.table.filter.bymodel': 'Model ara',
|
||||
'benchmark.table.filter.bydataset': 'Veri Kümesine göre filtrele',
|
||||
'benchmark.table.filter.byProfile': 'Profile göre filtrele',
|
||||
'benchmark.table.avg': 'Ort.',
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
export default {
|
||||
'billing.upsell.title': 'Billing is an Enterprise feature',
|
||||
'billing.upsell.subtitle':
|
||||
'Track spend, generate invoices, and enforce budgets across teams. Upgrade to GPUStack Enterprise to manage billing.',
|
||||
'billing.upsell.featuresTitle': 'What you get in Enterprise',
|
||||
'billing.upsell.feature.usage':
|
||||
'See cost breakdowns by organization, user, and model',
|
||||
'billing.upsell.feature.invoices':
|
||||
'Generate invoices and export billing reports',
|
||||
'billing.upsell.feature.budgets':
|
||||
'Set budgets and spending limits with alerts',
|
||||
'billing.upsell.feature.chargeback':
|
||||
'Attribute and charge back usage to teams and projects',
|
||||
'billing.upsell.cta': 'Learn about Enterprise'
|
||||
};
|
||||
@@ -39,7 +39,7 @@ export default {
|
||||
'clusters.workerpool.batchSize.desc':
|
||||
'İşçi havuzunda eşzamanlı olarak oluşturulan işçi düğüm sayısı',
|
||||
'clusters.create.addworker.tips':
|
||||
'Aşağıdaki komutu çalıştırmadan önce lütfen {label} için <a href={link} target="_blank">ön koşulların</a> karşılandığından emin olun.',
|
||||
'Aşağıdaki komutu çalıştırmadan önce lütfen <a href={link} target="_blank">ön koşulların</a> karşılandığından emin olun.',
|
||||
'clusters.create.addCommand.tips':
|
||||
'Eklenmesi gereken İşçi Düğümde, kümeye katılması için aşağıdaki komutu çalıştırın.',
|
||||
'clusters.create.addCommand.k8s.tips':
|
||||
@@ -70,10 +70,13 @@ export default {
|
||||
'<span class="bold-text">Docker dışı</span> kümeler için lütfen Kümeler sayfasından küme kaydı oluşturun veya işçi havuzlarını yönetin.',
|
||||
'clusters.addworker.selectGPU': 'GPU Üreticisi Seç',
|
||||
'clusters.addworker.selectGPU.multiTag': 'Multi-select',
|
||||
'clusters.addworker.selectGPU.singleOnly':
|
||||
'The selected vendor is not in this cluster’s GPU vendor overrides — single-select only. Pick a vendor configured in the overrides to enable multi-select.',
|
||||
'clusters.addworker.selectGPU.subtitle':
|
||||
'Birden fazla GPU Üreticisi seçebilir veya yalnızca CPU kümeleri için hiçbirini seçmeyebilirsiniz',
|
||||
'clusters.addworker.checkEnv': 'Ortamı Kontrol Et',
|
||||
'clusters.addworker.checkEnv.cpuOnlyTips':
|
||||
'Kubernetes kümesinde en az bir hazır düğüm olduğunu doğrulamak için aşağıdaki komutu kullanın. Yalnızca CPU kümelerini kaydediyorsunuz.',
|
||||
'clusters.addworker.specifyArgs': 'Argümanları Belirle',
|
||||
'clusters.addworker.dtkVersion': 'DTK Sürümü',
|
||||
'clusters.addworker.runCommand': 'Komutu Çalıştır',
|
||||
'clusters.addworker.specifyWorkerIP': "İşçi Düğüm IP'si",
|
||||
'clusters.addworker.detectWorkerIP': "İşçi Düğüm IP'sini Otomatik Algıla",
|
||||
@@ -112,6 +115,8 @@ export default {
|
||||
'{count} yeni işçi düğüm kümeye eklendi.',
|
||||
'clusters.create.serverUrl': "GPUStack Sunucu URL'si",
|
||||
'clusters.create.workerConfig': 'İşçi Düğüm Yapılandırması',
|
||||
'clusters.edit.k8sOptions.changed.tip':
|
||||
'Kubernetes seçeneklerini değiştirdiniz. Değişikliklerin etkili olması için kayıt komutunu hedef kümede yeniden çalıştırın.',
|
||||
'clusters.addworker.containerName': 'İşçi Düğüm Konteyner Adı',
|
||||
'clusters.addworker.containerName.tips':
|
||||
'İşçi düğüm konteyneri için bir ad belirtin.',
|
||||
@@ -140,7 +145,7 @@ export default {
|
||||
'clusters.addworker.theadNotes-02':
|
||||
'T-Head PPU, cihaz enjeksiyonu için Container Device Interface (CDI) kullanır ve CDI oluşturma için <span class="bold-text">/var/run/cdi</span> dizininin kullanılabilir olmasını gerektirir.',
|
||||
'clusters.addworker.nvidiaNotes':
|
||||
'GPUStack v2.1\'deki yerleşik çıkarım altyapıları <span class="bold-text">CUDA 12.6+</span> gerektirir. Lütfen NVIDIA sürücü sürümünüzün <span class="bold-text">560</span> veya daha yeni olduğundan emin olun.',
|
||||
'GPUStack\'teki yerleşik çıkarım altyapıları <span class="bold-text">CUDA 12.8+</span> gerektirir. Lütfen NVIDIA sürücü sürümünüzün <span class="bold-text">570</span> veya daha yeni olduğundan emin olun.',
|
||||
'clusters.volume.title': 'Volume Mounts',
|
||||
'clusters.volume.name': 'Volume Name',
|
||||
'clusters.volume.mountPath': 'Container Path',
|
||||
@@ -165,6 +170,10 @@ export default {
|
||||
'clusters.volume.configMap.name': 'ConfigMap Name',
|
||||
'clusters.volume.configMap.optional': 'Optional',
|
||||
'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',
|
||||
@@ -173,16 +182,20 @@ export default {
|
||||
'clusters.nodeSelector.title': 'Node Selector',
|
||||
'clusters.nodeSelector.tip':
|
||||
'Pod nodeSelector applied to every worker DaemonSet — only nodes whose labels match are eligible to run the worker.',
|
||||
'clusters.gpuVendorOverrides.title': 'GPU Vendor Overrides',
|
||||
'clusters.gpuVendorOverrides.validate.emptySelector':
|
||||
'The override for {vendor} needs at least one nodeSelector entry — an empty override would leave the multi-vendor manifest unable to target nodes for this runtime.',
|
||||
'clusters.gpuVendorOverrides.validate.duplicate':
|
||||
'Overrides for {v1} and {v2} use the same nodeSelector. Each vendor must scope to a distinct node set, otherwise their worker DaemonSets would compete for the same nodes.',
|
||||
'clusters.gpuVendorOverrides.validate.keyConflict':
|
||||
'Override for {vendor} reuses key(s) {keys} from the base Node Selector — the CPU worker would simultaneously require and forbid the key and never schedule.',
|
||||
'clusters.gpuVendorOverrides.tip':
|
||||
'Required when running multiple GPU vendors in one cluster. Each entry pins that vendor’s worker DaemonSet to nodes matching its nodeSelector, and the CPU worker is steered away from those nodes via a DoesNotExist node-affinity.',
|
||||
'clusters.gpuVendorOverrides.add': 'Add Override',
|
||||
'clusters.gpuVendorOverrides.vendor': 'GPU Vendor',
|
||||
'clusters.gpuVendorOverrides.nodeSelector': 'Node Selector'
|
||||
'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,6 +46,7 @@ export default {
|
||||
'common.button.enabled': 'Etkin',
|
||||
'common.button.disabled': 'Devre dışı',
|
||||
'common.button.upgrade': 'Yükselt',
|
||||
'common.enterprise.feature': 'Available in GPUStack Enterprise',
|
||||
'common.input.holder': 'Lütfen girin',
|
||||
'common.validate.value': '{name} değeri gereklidir',
|
||||
'common.button.edit': 'Düzenle',
|
||||
@@ -55,6 +56,7 @@ export default {
|
||||
'common.button.viewevent': 'Olayları Görüntüle',
|
||||
'common.button.recreate': 'Yeniden Oluştur',
|
||||
'common.table.operation': 'İşlemler',
|
||||
'common.table.creator': 'Oluşturan',
|
||||
'common.table.createTime': 'Oluşturulma',
|
||||
'common.table.updateTime': 'Güncellenme',
|
||||
'common.table.description': 'Açıklama',
|
||||
@@ -65,6 +67,7 @@ export default {
|
||||
'common.search.name.placeholder': 'ada göre filtrele',
|
||||
'common.search.id.placeholder': 'kimliğe göre filtrele',
|
||||
'common.filter.byId': 'kimliğe göre filtrele',
|
||||
'common.filter.byCreator': 'Oluşturana göre filtrele',
|
||||
'common.table.type': 'Tür',
|
||||
'common.table.default': 'Varsayılan Değer',
|
||||
'common.copy.success': 'Kopyalama başarılı!',
|
||||
@@ -164,6 +167,7 @@ export default {
|
||||
'common.time.hour': 'saat',
|
||||
'common.time.minute': 'dakika',
|
||||
'common.issue.report': 'Sorun bildir',
|
||||
'common.github.star.tooltip': "GitHub'da bize yıldız verin",
|
||||
'common.social.discord': "Discord'umuza Katılın",
|
||||
'common.table.mark': 'Yorum',
|
||||
'common.table.rollback.mark': 'Geri Alma Yorumu',
|
||||
@@ -195,6 +199,7 @@ export default {
|
||||
'common.table.user': 'Kullanıcı',
|
||||
'common.settings.instructions': 'Talimatlar',
|
||||
'common.settings.language': 'Dil',
|
||||
'common.settings.language.tips': 'Arayüzün görüntüleme dilini ayarlayın.',
|
||||
'common.delete.confirm':
|
||||
'Seçili {type} öğesini silmek istediğinizden emin misiniz?',
|
||||
'common.delete.single.confirm':
|
||||
@@ -250,6 +255,11 @@ export default {
|
||||
'common.appearance.tips': 'Varsayılan olarak sistem tercihini takip eder.',
|
||||
'common.button.forgotpassword': 'Şifrenizi mi unuttunuz?',
|
||||
'common.appearance.theme': 'Tema',
|
||||
'common.appearance.description':
|
||||
'Arayüzün cihazınızdaki görünümünü özelleştirin.',
|
||||
'common.security': 'Güvenlik',
|
||||
'common.security.description':
|
||||
'Hesabınıza giriş yapmak için kullanılan parolayı yönetin.',
|
||||
'common.page.wentwrong': 'Bir şeyler ters gitti.',
|
||||
'common.page.refresh.tips':
|
||||
'Sayfanın güncellenmesi gerekebilir. Yenilemeyi deneyin!',
|
||||
@@ -260,6 +270,10 @@ export default {
|
||||
'common.login.auth': 'Kimlik doğrulanıyor...',
|
||||
'common.login.auth.failed': 'Kimlik doğrulama başarısız',
|
||||
'common.login.password': 'Şifre ile giriş yap',
|
||||
'common.login.username.holder': 'Lütfen kullanıcı adını girin',
|
||||
'common.login.password.holder': 'Lütfen şifreyi girin',
|
||||
'common.login.newpassword.holder': 'Lütfen yeni şifreyi girin',
|
||||
'common.login.confirm.holder': 'Lütfen şifreyi tekrar girin',
|
||||
'common.external.login': '{type} ile giriş yap',
|
||||
'common.sso.noConfig':
|
||||
'Bu sistemde çoklu oturum açma etkinleştirilmemiş. Lütfen yöneticinize başvurun.',
|
||||
@@ -288,5 +302,7 @@ export default {
|
||||
'common.image.limit.height': 'Image height must be {height}.',
|
||||
'common.remaining': 'Kalan {count}',
|
||||
'common.max': 'Maks. {count}',
|
||||
'common.validate.group': 'Please complete the {group} configuration'
|
||||
'common.max.count': '{label} Sayısı',
|
||||
'common.validate.group': 'Please complete the {group} configuration',
|
||||
'common.preferences': 'Tercihler'
|
||||
};
|
||||
|
||||
@@ -13,7 +13,10 @@ export default {
|
||||
'gpuservice.template.command.placeholder':
|
||||
'Argümanları boşlukla ayırın; boşluk içeren argümanları tırnak içine alın, örn.: /bin/bash -c "echo hello world"',
|
||||
'gpuservice.template.mountPath': 'Bağlama Yolu',
|
||||
'gpuservice.template.mountPath.tips':
|
||||
'Bu şablondan bir örnek oluşturulurken depolama biriminin varsayılan olarak bağlanacağı yol. Örnek çalışırken saklanması gereken verileri kalıcı hale getirmek için kullanılabilir.',
|
||||
'gpuservice.template.containerDisk': 'Konteyner Diski (GB)',
|
||||
'gpuservice.template.containerDisk.tips': 'Konteyner sistem diskinin boyutu.',
|
||||
'gpuservice.template.memory': 'Bellek (GB)',
|
||||
'gpuservice.instance.containerDisk.remaining':
|
||||
'Konteyner Diski (Maks. {count} GB)',
|
||||
@@ -77,6 +80,8 @@ export default {
|
||||
'gpuservice.publicKey': 'SSH Açık Anahtarı',
|
||||
'gpuservice.publicKey.add': 'SSH Açık Anahtarı Ekle',
|
||||
'gpuservice.publicKey.edit': 'SSH Açık Anahtarını Düzenle',
|
||||
'gpuservice.publicKey.delete.tips':
|
||||
'Bir SSH Açık Anahtarını silmek, mevcut bağlı Örneklerin erişimini iptal etmez. Erişimi kaldırmak için ilgili Örnekleri ayrı ayrı düzenleyin.',
|
||||
'gpuservice.publicKey.filter.name': 'Ada göre ara',
|
||||
'gpuservice.publicKey.label': 'SSH Açık Anahtarı',
|
||||
'gpuservice.instance.ssh.enable': 'SSH Erişimini Etkinleştir',
|
||||
@@ -97,18 +102,27 @@ export default {
|
||||
'gpuservice.instance.templates': 'Örnek Şablonları',
|
||||
'gpuservice.instance.section.storage': 'Depolama Hacmi',
|
||||
'gpuservice.instance.type.required': 'Lütfen bir örnek türü seçin',
|
||||
'gpuservice.instance.type.noAvailable': 'Kullanılabilir örnek türü yok',
|
||||
'gpuservice.instance.gpuCount': 'GPU Sayısı',
|
||||
'gpuservice.instance.gpuCount.required': 'Lütfen GPU sayısını girin',
|
||||
'gpuservice.instance.gpuCount.max': 'En fazla {count} GPU kartı seçin',
|
||||
'gpuservice.instance.gpuCount.min': 'En az {count} GPU kartı seçin',
|
||||
'gpuservice.instance.cpuCount.max': 'En fazla {count} CPU çekirdeği seçin',
|
||||
'gpuservice.instance.cpuCount.min': 'En az {count} CPU çekirdeği seçin',
|
||||
'gpuservice.instance.gpuCount.noAvailable':
|
||||
'Kullanılabilir GPU kaynağı yok, lütfen başka bir örnek türü seçin.',
|
||||
'gpuservice.instance.gpuCount.zero': 'Yalnızca CPU, ortam hazırlığı için.',
|
||||
'gpuservice.instance.stock': 'Stok',
|
||||
'gpuservice.instance.sliced': 'Bölünmüş',
|
||||
'gpuservice.instance.memory': 'Bellek',
|
||||
'gpuservice.instance.memory': 'VRAM',
|
||||
'gpuservice.instance.ram': 'RAM',
|
||||
'gpuservice.instance.os': 'OS',
|
||||
'gpuservice.instance.arch': 'Mimari',
|
||||
'gpuservice.instance.disk': 'Disk',
|
||||
'gpuservice.table.count': 'Sayı',
|
||||
'gpuservice.instance.disk.system': 'Sistem Diski',
|
||||
'gpuservice.instance.disk.ephemeral': 'Geçici Depolama',
|
||||
'gpuservice.instance.disk.persistent': 'Kalıcı Depolama',
|
||||
'gpuservice.instance.search.type.placeholder': 'Ada göre ara',
|
||||
'gpuservice.instance.search.template.placeholder':
|
||||
'Şablon adına, imaja veya bağlama yoluna göre ara',
|
||||
@@ -141,9 +155,8 @@ export default {
|
||||
'gpuservice.storage.accessMode': 'Erişim Modu',
|
||||
'gpuservice.storage.persistent': 'Kalıcı',
|
||||
'gpuservice.storage.temporary': 'Geçici',
|
||||
'gpuservice.storage.persistentVolume': 'Kalıcı Hacim',
|
||||
'gpuservice.storage.persistentVolume.required':
|
||||
'Lütfen bir kalıcı hacim seçin',
|
||||
'gpuservice.storage.persistentVolume': 'Kalıcı',
|
||||
'gpuservice.storage.persistentVolume.required': 'Lütfen bir depolama seçin',
|
||||
'gpuservice.storage.persistentVolume.capacity': 'Kapasite (GB)',
|
||||
'gpuservice.storage.persistentVolume.capacity.required':
|
||||
'Lütfen kapasiteyi girin',
|
||||
@@ -157,5 +170,10 @@ export default {
|
||||
'gpuservice.storage.temporary.tips':
|
||||
'Data is cleared when the instance stops.',
|
||||
'gpuservice.storage.persistentVolume.tips':
|
||||
'Data persists across restarts and is deleted only when the instance is terminated. Cannot be shared with other instances.'
|
||||
'Data persists across instance restarts. Persistent volumes remain intact after instance termination and can be shared by multiple instances.',
|
||||
'gpuservice.form.storage.select': 'Depolamayı Seç',
|
||||
'gpuservice.creator': 'Oluşturan',
|
||||
'gpuservice.owner.global': 'Genel',
|
||||
'gpuservice.template.group.yours': 'Şablonlarınız',
|
||||
'gpuservice.template.group.global': 'Genel Şablonlar'
|
||||
};
|
||||
|
||||
@@ -8,7 +8,7 @@ export default {
|
||||
'menu.playground.text2images': 'Görsel',
|
||||
'menu.playground.video': 'Video',
|
||||
'menu.compare': 'Karşılaştır',
|
||||
'menu.models': 'Modeller',
|
||||
'menu.models': 'Model Hizmetleri',
|
||||
'menu.models.modelList': 'Dağıt ve Yönet',
|
||||
'menu.models.modelCatalog': 'Katalog',
|
||||
'menu.models.catalog': 'Model Kataloğu',
|
||||
@@ -25,20 +25,24 @@ export default {
|
||||
'menu.users': 'Kullanıcılar',
|
||||
'menu.resources.workers': 'İşçi Düğümler',
|
||||
'menu.resources.gpus': "GPU'lar",
|
||||
'menu.resources.modelfiles': 'Model Dosyaları',
|
||||
'menu.models.modelfiles': 'Model Dosyaları',
|
||||
'menu.accessControl': 'Erişim Kontrolü',
|
||||
'menu.accessControl.apikeys': 'API Anahtarları',
|
||||
'menu.accessControl.users': 'Kullanıcılar',
|
||||
'menu.profile': 'Profil',
|
||||
'menu.accessControl.organizations': 'Organizasyonlar',
|
||||
'menu.profile': 'Preferences',
|
||||
'menu.login': 'Giriş',
|
||||
'menu.usage': 'Kullanım',
|
||||
'menu.usage.usage': 'Kullanım',
|
||||
'menu.billingAndUsage': 'Kullanım ve Faturalandırma',
|
||||
'menu.billingAndUsage.usage': 'Kullanım',
|
||||
'menu.billingAndUsage.billing': 'Faturalandırma',
|
||||
'menu.404': '404',
|
||||
'menu.clusterManagement': 'Küme Yönetimi',
|
||||
'menu.clusterManagement.clusters': 'Kümeler',
|
||||
'menu.clusterManagement.credentials': 'Bulut Kimlik Bilgileri',
|
||||
'menu.clusterManagement.clusterDetail': 'Küme Detayı',
|
||||
'menu.clusterManagement.clusterCreate': 'Küme Oluştur',
|
||||
'menu.resources.backendsList': 'Çıkarım Altyapıları',
|
||||
'menu.resources.clusters': 'Kümeler',
|
||||
'menu.resources.credentials': 'Bulut Kimlik Bilgileri',
|
||||
'menu.resources.clusterDetail': 'Küme Detayı',
|
||||
'menu.resources.clusterCreate': 'Küme Oluştur',
|
||||
'menu.models.backendsList': 'Çıkarım Altyapıları',
|
||||
'menu.models.instances': 'Instances',
|
||||
'menu.settings': 'Settings',
|
||||
'menu.gpuService': 'GPU Service',
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user