Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a19fea0c6c | ||
|
|
a516e6ce72 | ||
|
|
2e375c6603 | ||
|
|
7d94c77c15 | ||
|
|
457d2f2f72 | ||
|
|
5e7d83e5dd | ||
|
|
b1cffe047c | ||
|
|
6509f2a4ff | ||
|
|
f719c11606 | ||
|
|
b8d7873b77 | ||
|
|
a9248a98a5 | ||
|
|
8f85e9a082 | ||
|
|
1a6d1654b2 | ||
|
|
abe705c034 | ||
|
|
8b34f68824 | ||
|
|
e37828a2cd | ||
|
|
4709661f93 | ||
|
|
1d7543f19c | ||
|
|
1cf146ca32 | ||
|
|
b05c510776 | ||
|
|
e79d5f8962 | ||
|
|
5503fdc21e | ||
|
|
36a1038d12 | ||
|
|
ee42a9d0ed | ||
|
|
28976dea4b | ||
|
|
22e5cc3aa5 | ||
|
|
3052c0b23e | ||
|
|
1c9cabf7e7 | ||
|
|
03842d1741 | ||
|
|
84304eadb7 | ||
|
|
ab3739219e | ||
|
|
7cf4932f3a | ||
|
|
032f1eefef | ||
|
|
e35be94f7c | ||
|
|
e76e2ad85a | ||
|
|
cb19cd2476 | ||
|
|
c532732ce9 | ||
|
|
2bc10dbb2e | ||
|
|
b634a03ef9 | ||
|
|
ac64bb98ad | ||
|
|
aa49539210 | ||
|
|
1f5678f781 | ||
|
|
7adc089e94 | ||
|
|
6e1e9c07ee | ||
|
|
1d55382f3b | ||
|
|
e68bb0bc4e | ||
|
|
c8e14dade5 | ||
|
|
e4c1a3f5c2 | ||
|
|
cdd6ef9cb3 | ||
|
|
737d43fd16 | ||
|
|
9f5163531c | ||
|
|
df5cf0f4e5 | ||
|
|
e160b64697 | ||
|
|
7d36c6ac4a | ||
|
|
0820edf55f | ||
|
|
12e324893f | ||
|
|
4017c4dd33 | ||
|
|
17b65d80e1 | ||
|
|
47cb3321ab | ||
|
|
547b9f617b | ||
|
|
9e7bd78693 | ||
|
|
c53d60b628 | ||
|
|
9566e0cdb2 | ||
|
|
ed3e779d22 | ||
|
|
3e2133753d | ||
|
|
736a853192 | ||
|
|
08ef2ffa9a | ||
|
|
6cf512cccd | ||
|
|
6f56e614f3 |
@@ -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
|
.swc
|
||||||
.DS_Store
|
.DS_Store
|
||||||
.idea
|
.idea
|
||||||
.claude
|
.claude/settings.local.json
|
||||||
/dist.zip
|
/dist.zip
|
||||||
.cache
|
.cache
|
||||||
@@ -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.
|
|
||||||
@@ -346,6 +346,7 @@ const baseRoutes = [
|
|||||||
icon: 'icon-billing-outlined',
|
icon: 'icon-billing-outlined',
|
||||||
selectedIcon: 'icon-billing-filled',
|
selectedIcon: 'icon-billing-filled',
|
||||||
defaultIcon: 'icon-billing-outlined',
|
defaultIcon: 'icon-billing-outlined',
|
||||||
|
hideInMenu: process.env.ENABLE_ENTERPRISE === 'true',
|
||||||
// OSS exposes the menu as a teaser for the enterprise billing
|
// OSS exposes the menu as a teaser for the enterprise billing
|
||||||
// module. The page itself just renders an upsell notice — the real
|
// module. The page itself just renders an upsell notice — the real
|
||||||
// billing UI lives in the enterprise plugin and shadows this route
|
// billing UI lives in the enterprise plugin and shadows this route
|
||||||
|
|||||||
+1
-1
@@ -17,7 +17,7 @@
|
|||||||
"@ant-design/pro-components": "3.1.0-0",
|
"@ant-design/pro-components": "3.1.0-0",
|
||||||
"@antv/g6": "^5.0.51",
|
"@antv/g6": "^5.0.51",
|
||||||
"@braintree/sanitize-url": "^7.1.1",
|
"@braintree/sanitize-url": "^7.1.1",
|
||||||
"@gpustack/core-ui": "^1.0.27",
|
"@gpustack/core-ui": "^1.0.32",
|
||||||
"@huggingface/gguf": "^0.1.7",
|
"@huggingface/gguf": "^0.1.7",
|
||||||
"@huggingface/hub": "^0.15.1",
|
"@huggingface/hub": "^0.15.1",
|
||||||
"@huggingface/tasks": "^0.11.6",
|
"@huggingface/tasks": "^0.11.6",
|
||||||
|
|||||||
Generated
+117
-117
@@ -24,8 +24,8 @@ importers:
|
|||||||
specifier: ^7.1.1
|
specifier: ^7.1.1
|
||||||
version: 7.1.2
|
version: 7.1.2
|
||||||
'@gpustack/core-ui':
|
'@gpustack/core-ui':
|
||||||
specifier: ^1.0.27
|
specifier: ^1.0.32
|
||||||
version: 1.0.27(czdvzceysqw7iv6pct2ucnb23e)
|
version: 1.0.32(czdvzceysqw7iv6pct2ucnb23e)
|
||||||
'@huggingface/gguf':
|
'@huggingface/gguf':
|
||||||
specifier: ^0.1.7
|
specifier: ^0.1.7
|
||||||
version: 0.1.18
|
version: 0.1.18
|
||||||
@@ -49,7 +49,7 @@ importers:
|
|||||||
version: 4.17.24
|
version: 4.17.24
|
||||||
'@umijs/max':
|
'@umijs/max':
|
||||||
specifier: ^4.6.15
|
specifier: ^4.6.15
|
||||||
version: 4.6.51(@babel/core@7.29.0)(@types/node@25.6.2)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(lightningcss@1.22.1)(prettier@3.8.3)(rc-field-form@2.7.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.29.0)(react@18.3.1))(terser@5.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':
|
'@xterm/addon-fit':
|
||||||
specifier: ^0.10.0
|
specifier: ^0.10.0
|
||||||
version: 0.10.0(@xterm/xterm@5.5.0)
|
version: 0.10.0(@xterm/xterm@5.5.0)
|
||||||
@@ -109,7 +109,7 @@ importers:
|
|||||||
version: 3.3.0
|
version: 3.3.0
|
||||||
jotai:
|
jotai:
|
||||||
specifier: ^2.8.4
|
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:
|
js-yaml:
|
||||||
specifier: ^4.1.0
|
specifier: ^4.1.0
|
||||||
version: 4.1.1
|
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)
|
version: 6.4.1(css-to-react-native@3.2.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||||
umi-presets-pro:
|
umi-presets-pro:
|
||||||
specifier: ^2.0.3
|
specifier: ^2.0.3
|
||||||
version: 2.0.3(@babel/core@7.29.0)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(antd@6.3.7(date-fns@2.30.0)(moment@2.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(chokidar@3.6.0)(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(encoding@0.1.13)(rc-field-form@2.7.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3)(umi@4.6.51(@babel/core@7.29.0)(@types/node@25.6.2)(@types/react@18.3.28)(eslint@9.39.4(jiti@2.7.0))(lightningcss@1.22.1)(prettier@3.8.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.29.0)(react@18.3.1))(stylelint@14.8.2)(terser@5.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:
|
wavesurfer.js:
|
||||||
specifier: ^7.8.8
|
specifier: ^7.8.8
|
||||||
version: 7.12.6
|
version: 7.12.6
|
||||||
@@ -233,10 +233,10 @@ importers:
|
|||||||
version: 1.0.1
|
version: 1.0.1
|
||||||
'@umijs/plugins':
|
'@umijs/plugins':
|
||||||
specifier: ^4.4.11
|
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:
|
babel-plugin-named-asset-import:
|
||||||
specifier: ^0.3.8
|
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:
|
case-sensitive-paths-webpack-plugin:
|
||||||
specifier: ^2.4.0
|
specifier: ^2.4.0
|
||||||
version: 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}
|
resolution: {integrity: sha512-KWk80UPIzPmUg+P0rKh6TqspRw0G6eux1PuJr+zz47ftMaZ9QDwbGzHZbtzWkl5hgayM/qrKRutllRC7D/vVXQ==, tarball: https://registry.npmjs.org/@formatjs/intl-utils/-/intl-utils-2.3.0.tgz}
|
||||||
deprecated: the package is rather renamed to @formatjs/ecma-abstract with some changes in functionality (primarily selectUnit is removed and we don't plan to make any further changes to this package
|
deprecated: the package is rather renamed to @formatjs/ecma-abstract with some changes in functionality (primarily selectUnit is removed and we don't plan to make any further changes to this package
|
||||||
|
|
||||||
'@gpustack/core-ui@1.0.27':
|
'@gpustack/core-ui@1.0.32':
|
||||||
resolution: {integrity: sha512-m3ue0EHFKULla0mpnpxZwn9LVaGKS+HnuzQYSBECQa4vaP8MEEQsR7oFBtG8bhWUWYA9VzP6k7fjozDkP6TymA==, tarball: https://registry.npmjs.org/@gpustack/core-ui/-/core-ui-1.0.27.tgz}
|
resolution: {integrity: sha512-kGTazoqbK2KyZgOP6gmQaRxTiQVfF2IKLGDXjJq6w6BbmJgALXFJA2v2ROjAbjEVyfTdBzyYeXfPo/JgISpMNw==, tarball: https://registry.npmjs.org/@gpustack/core-ui/-/core-ui-1.0.32.tgz}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@ant-design/icons': '>=6.0.0'
|
'@ant-design/icons': ^6.1.0
|
||||||
'@ant-design/pro-components': 3.1.0-0
|
'@ant-design/pro-components': 3.1.0-0
|
||||||
'@monaco-editor/react': ^4.6.0
|
'@monaco-editor/react': ^4.6.0
|
||||||
ahooks: '>=3.0.0'
|
ahooks: ^3.8.5
|
||||||
antd: '>=6.0.0'
|
antd: ^6.3.3
|
||||||
antd-style: '>=3.0.0'
|
antd-style: ^3.6.2
|
||||||
axios: '>=1.8.0'
|
axios: ^1.8.2
|
||||||
echarts: '>=5.0.0'
|
echarts: ^5.5.1
|
||||||
file-saver: ^2.0.5
|
file-saver: ^2.0.5
|
||||||
monaco-editor: ^0.30.1
|
monaco-editor: ^0.30.1
|
||||||
monaco-yaml: ^4.0.0
|
monaco-yaml: ^4.0.0
|
||||||
overlayscrollbars-react: ^0.5.6
|
overlayscrollbars-react: ^0.5.6
|
||||||
react: '>=18.0.0'
|
react: ^18.2.0
|
||||||
react-dom: '>=18.0.0'
|
react-dom: ^18.2.0
|
||||||
styled-components: '>=6.0.0'
|
styled-components: ^6.1.15
|
||||||
|
|
||||||
'@hono/node-server@1.19.14':
|
'@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}
|
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
|
'@radix-ui/popper': 0.0.10
|
||||||
react: 18.3.1
|
react: 18.3.1
|
||||||
|
|
||||||
'@alita/plugins@3.5.5(@babel/core@7.29.0)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(antd@6.3.7(date-fns@2.30.0)(moment@2.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(rc-field-form@2.7.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
|
'@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:
|
dependencies:
|
||||||
'@alita/babel-transform-jsx-class': 0.0.2
|
'@alita/babel-transform-jsx-class': 0.0.2
|
||||||
'@alita/inspx': 0.0.2(react@18.3.1)
|
'@alita/inspx': 0.0.2(react@18.3.1)
|
||||||
'@alita/request': 3.1.2
|
'@alita/request': 3.1.2
|
||||||
'@alita/types': 3.1.2
|
'@alita/types': 3.1.2
|
||||||
'@umijs/bundler-utils': 4.4.11
|
'@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
|
'@umijs/utils': 4.4.11
|
||||||
ahooks: 3.9.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
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)
|
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:
|
dependencies:
|
||||||
'@babel/types': 7.29.0
|
'@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:
|
dependencies:
|
||||||
'@babel/core': 7.29.0
|
'@babel/core': 7.23.6
|
||||||
'@babel/helper-plugin-utils': 7.28.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:
|
dependencies:
|
||||||
'@babel/core': 7.29.0
|
'@babel/core': 7.23.6
|
||||||
'@babel/helper-plugin-utils': 7.28.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:
|
dependencies:
|
||||||
'@babel/core': 7.29.0
|
'@babel/core': 7.23.6
|
||||||
'@babel/helper-plugin-utils': 7.28.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:
|
dependencies:
|
||||||
'@babel/core': 7.29.0
|
'@babel/core': 7.23.6
|
||||||
'@babel/helper-plugin-utils': 7.28.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:
|
dependencies:
|
||||||
'@babel/core': 7.29.0
|
'@babel/core': 7.23.6
|
||||||
'@babel/helper-plugin-utils': 7.28.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:
|
dependencies:
|
||||||
'@babel/core': 7.29.0
|
'@babel/core': 7.23.6
|
||||||
'@babel/helper-plugin-utils': 7.28.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:
|
dependencies:
|
||||||
'@babel/core': 7.29.0
|
'@babel/core': 7.23.6
|
||||||
'@babel/helper-plugin-utils': 7.28.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:
|
dependencies:
|
||||||
'@babel/core': 7.29.0
|
'@babel/core': 7.23.6
|
||||||
'@babel/helper-plugin-utils': 7.28.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:
|
dependencies:
|
||||||
'@babel/core': 7.29.0
|
'@babel/core': 7.23.6
|
||||||
'@babel/helper-plugin-utils': 7.28.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:
|
dependencies:
|
||||||
'@babel/core': 7.29.0
|
'@babel/core': 7.23.6
|
||||||
'@babel/helper-plugin-utils': 7.28.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:
|
dependencies:
|
||||||
'@babel/core': 7.29.0
|
'@babel/core': 7.23.6
|
||||||
'@babel/helper-plugin-utils': 7.28.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:
|
dependencies:
|
||||||
'@babel/core': 7.29.0
|
'@babel/core': 7.23.6
|
||||||
'@babel/helper-plugin-utils': 7.28.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:
|
dependencies:
|
||||||
'@babel/core': 7.29.0
|
'@babel/core': 7.23.6
|
||||||
'@babel/helper-plugin-utils': 7.28.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:
|
dependencies:
|
||||||
'@babel/core': 7.29.0
|
'@babel/core': 7.23.6
|
||||||
'@babel/helper-plugin-utils': 7.28.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:
|
dependencies:
|
||||||
'@babel/core': 7.29.0
|
'@babel/core': 7.23.6
|
||||||
'@babel/helper-plugin-utils': 7.28.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:
|
dependencies:
|
||||||
'@babel/core': 7.29.0
|
'@babel/core': 7.23.6
|
||||||
'@babel/helper-plugin-utils': 7.28.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:
|
dependencies:
|
||||||
'@babel/core': 7.29.0
|
'@babel/core': 7.23.6
|
||||||
'@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0)
|
'@babel/helper-module-transforms': 7.28.6(@babel/core@7.23.6)
|
||||||
'@babel/helper-plugin-utils': 7.28.6
|
'@babel/helper-plugin-utils': 7.28.6
|
||||||
'@babel/helper-simple-access': 7.27.1
|
'@babel/helper-simple-access': 7.27.1
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
@@ -10808,7 +10808,7 @@ snapshots:
|
|||||||
|
|
||||||
'@formatjs/intl-utils@2.3.0': {}
|
'@formatjs/intl-utils@2.3.0': {}
|
||||||
|
|
||||||
'@gpustack/core-ui@1.0.27(czdvzceysqw7iv6pct2ucnb23e)':
|
'@gpustack/core-ui@1.0.32(czdvzceysqw7iv6pct2ucnb23e)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@ant-design/icons': 6.2.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
'@ant-design/icons': 6.2.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||||
'@ant-design/pro-components': 3.1.0-0(antd@6.3.7(date-fns@2.30.0)(moment@2.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
'@ant-design/pro-components': 3.1.0-0(antd@6.3.7(date-fns@2.30.0)(moment@2.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||||
@@ -12281,11 +12281,11 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- 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:
|
dependencies:
|
||||||
'@umijs/bundler-utils': 4.6.51
|
'@umijs/bundler-utils': 4.6.51
|
||||||
'@umijs/bundler-webpack': 4.6.51(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14))
|
'@umijs/bundler-webpack': 4.6.51(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14))
|
||||||
'@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
|
compression: 1.8.1
|
||||||
connect-history-api-fallback: 2.0.0
|
connect-history-api-fallback: 2.0.0
|
||||||
cors: 2.8.6
|
cors: 2.8.6
|
||||||
@@ -12553,14 +12553,14 @@ snapshots:
|
|||||||
- supports-color
|
- supports-color
|
||||||
- typescript
|
- typescript
|
||||||
|
|
||||||
'@umijs/max@4.6.51(@babel/core@7.29.0)(@types/node@25.6.2)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(lightningcss@1.22.1)(prettier@3.8.3)(rc-field-form@2.7.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.29.0)(react@18.3.1))(terser@5.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:
|
dependencies:
|
||||||
'@umijs/lint': 4.6.51(eslint@8.35.0)(stylelint@14.8.2)(typescript@5.9.3)
|
'@umijs/lint': 4.6.51(eslint@8.35.0)(stylelint@14.8.2)(typescript@5.9.3)
|
||||||
'@umijs/plugins': 4.6.51(@babel/core@7.29.0)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(antd@4.24.16(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(rc-field-form@2.7.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
'@umijs/plugins': 4.6.51(@babel/core@7.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)
|
antd: 4.24.16(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||||
eslint: 8.35.0
|
eslint: 8.35.0
|
||||||
stylelint: 14.8.2
|
stylelint: 14.8.2
|
||||||
umi: 4.6.51(@babel/core@7.29.0)(@types/node@25.6.2)(@types/react@18.3.28)(eslint@8.35.0)(lightningcss@1.22.1)(prettier@3.8.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.29.0)(react@18.3.1))(stylelint@14.8.2)(terser@5.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:
|
transitivePeerDependencies:
|
||||||
- '@babel/core'
|
- '@babel/core'
|
||||||
- '@rspack/core'
|
- '@rspack/core'
|
||||||
@@ -12642,7 +12642,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
tsx: 3.12.2
|
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:
|
dependencies:
|
||||||
'@ahooksjs/use-request': 2.8.15(react@18.3.1)
|
'@ahooksjs/use-request': 2.8.15(react@18.3.1)
|
||||||
'@ant-design/antd-theme-variable': 1.0.0
|
'@ant-design/antd-theme-variable': 1.0.0
|
||||||
@@ -12657,7 +12657,7 @@ snapshots:
|
|||||||
antd-dayjs-webpack-plugin: 1.0.6(dayjs@1.11.20)
|
antd-dayjs-webpack-plugin: 1.0.6(dayjs@1.11.20)
|
||||||
axios: 0.27.2
|
axios: 0.27.2
|
||||||
babel-plugin-import: 1.13.8
|
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
|
dayjs: 1.11.20
|
||||||
dva-core: 2.0.4(redux@4.2.1)
|
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))
|
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
|
- react-native
|
||||||
- supports-color
|
- 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:
|
dependencies:
|
||||||
'@ahooksjs/use-request': 2.8.15(react@18.3.1)
|
'@ahooksjs/use-request': 2.8.15(react@18.3.1)
|
||||||
'@ant-design/antd-theme-variable': 1.0.0
|
'@ant-design/antd-theme-variable': 1.0.0
|
||||||
@@ -12702,7 +12702,7 @@ snapshots:
|
|||||||
antd-dayjs-webpack-plugin: 1.0.6(dayjs@1.11.20)
|
antd-dayjs-webpack-plugin: 1.0.6(dayjs@1.11.20)
|
||||||
axios: 0.27.2
|
axios: 0.27.2
|
||||||
babel-plugin-import: 1.13.8
|
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
|
dayjs: 1.11.20
|
||||||
dva-core: 2.0.4(redux@4.2.1)
|
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))
|
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
|
- react-native
|
||||||
- supports-color
|
- 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:
|
dependencies:
|
||||||
'@ahooksjs/use-request': 2.8.15(react@18.3.1)
|
'@ahooksjs/use-request': 2.8.15(react@18.3.1)
|
||||||
'@ant-design/antd-theme-variable': 1.0.0
|
'@ant-design/antd-theme-variable': 1.0.0
|
||||||
@@ -12747,7 +12747,7 @@ snapshots:
|
|||||||
antd-dayjs-webpack-plugin: 1.0.6(dayjs@1.11.20)
|
antd-dayjs-webpack-plugin: 1.0.6(dayjs@1.11.20)
|
||||||
axios: 0.27.2
|
axios: 0.27.2
|
||||||
babel-plugin-import: 1.13.8
|
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
|
dayjs: 1.11.20
|
||||||
dva-core: 2.0.4(redux@4.2.1)
|
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))
|
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
|
- react-native
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
'@umijs/preset-umi@4.6.51(@types/node@25.6.2)(@types/react@18.3.28)(lightningcss@1.22.1)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.29.0)(react@18.3.1))(terser@5.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:
|
dependencies:
|
||||||
'@iconify/utils': 2.1.1
|
'@iconify/utils': 2.1.1
|
||||||
'@stagewise/toolbar': 0.6.2
|
'@stagewise/toolbar': 0.6.2
|
||||||
@@ -12787,7 +12787,7 @@ snapshots:
|
|||||||
'@umijs/bundler-esbuild': 4.6.51
|
'@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-mako': 0.11.10(postcss@8.5.14)(sass@1.54.0)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14))
|
||||||
'@umijs/bundler-utils': 4.6.51
|
'@umijs/bundler-utils': 4.6.51
|
||||||
'@umijs/bundler-utoopack': 4.6.51(styled-jsx@5.1.7(@babel/core@7.29.0)(react@18.3.1))(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14))
|
'@umijs/bundler-utoopack': 4.6.51(styled-jsx@5.1.7(@babel/core@7.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-vite': 4.6.51(@types/node@25.6.2)(lightningcss@1.22.1)(postcss@8.5.14)(rollup@3.30.0)(sass@1.54.0)(terser@5.48.0)
|
||||||
'@umijs/bundler-webpack': 4.6.51(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14))
|
'@umijs/bundler-webpack': 4.6.51(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14))
|
||||||
'@umijs/core': 4.6.51
|
'@umijs/core': 4.6.51
|
||||||
@@ -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-helmet-async: 1.3.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||||
react-router-dom: 6.3.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
react-router-dom: 6.3.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||||
|
|
||||||
'@umijs/request-record@1.1.4(umi@4.6.51(@babel/core@7.29.0)(@types/node@25.6.2)(@types/react@18.3.28)(eslint@9.39.4(jiti@2.7.0))(lightningcss@1.22.1)(prettier@3.8.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.29.0)(react@18.3.1))(stylelint@14.8.2)(terser@5.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:
|
dependencies:
|
||||||
chokidar: 3.6.0
|
chokidar: 3.6.0
|
||||||
express: 4.22.1
|
express: 4.22.1
|
||||||
lodash: 4.18.1
|
lodash: 4.18.1
|
||||||
prettier: 2.8.8
|
prettier: 2.8.8
|
||||||
umi: 4.6.51(@babel/core@7.29.0)(@types/node@25.6.2)(@types/react@18.3.28)(eslint@9.39.4(jiti@2.7.0))(lightningcss@1.22.1)(prettier@3.8.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.29.0)(react@18.3.1))(stylelint@14.8.2)(terser@5.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:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
@@ -12892,13 +12892,13 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
'@umijs/test@4.6.51(@babel/core@7.29.0)':
|
'@umijs/test@4.6.51(@babel/core@7.23.6)':
|
||||||
dependencies:
|
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
|
'@jest/types': 27.5.1
|
||||||
'@umijs/bundler-utils': 4.6.51
|
'@umijs/bundler-utils': 4.6.51
|
||||||
'@umijs/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
|
esbuild: 0.21.4
|
||||||
identity-obj-proxy: 3.0.0
|
identity-obj-proxy: 3.0.0
|
||||||
isomorphic-unfetch: 4.0.2
|
isomorphic-unfetch: 4.0.2
|
||||||
@@ -13018,7 +13018,7 @@ snapshots:
|
|||||||
'@utoo/pack-win32-x64-msvc@1.4.3':
|
'@utoo/pack-win32-x64-msvc@1.4.3':
|
||||||
optional: true
|
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:
|
dependencies:
|
||||||
'@babel/code-frame': 7.22.5
|
'@babel/code-frame': 7.22.5
|
||||||
'@hono/node-server': 1.19.14(hono@4.12.18)
|
'@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))
|
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
|
semver: 7.8.0
|
||||||
send: 0.17.1
|
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
|
ws: 8.20.0
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@utoo/pack-darwin-arm64': 1.4.3
|
'@utoo/pack-darwin-arm64': 1.4.3
|
||||||
@@ -13555,13 +13555,13 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- debug
|
- debug
|
||||||
|
|
||||||
babel-jest@29.7.0(@babel/core@7.29.0):
|
babel-jest@29.7.0(@babel/core@7.23.6):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/core': 7.29.0
|
'@babel/core': 7.23.6
|
||||||
'@jest/transform': 29.7.0
|
'@jest/transform': 29.7.0
|
||||||
'@types/babel__core': 7.20.5
|
'@types/babel__core': 7.20.5
|
||||||
babel-plugin-istanbul: 6.1.1
|
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
|
chalk: 4.1.2
|
||||||
graceful-fs: 4.2.11
|
graceful-fs: 4.2.11
|
||||||
slash: 3.0.0
|
slash: 3.0.0
|
||||||
@@ -13601,9 +13601,9 @@ snapshots:
|
|||||||
cosmiconfig: 7.1.0
|
cosmiconfig: 7.1.0
|
||||||
resolve: 1.22.12
|
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:
|
dependencies:
|
||||||
'@babel/core': 7.29.0
|
'@babel/core': 7.23.6
|
||||||
|
|
||||||
babel-plugin-react-compiler@0.0.0-experimental-c23de8d-20240515:
|
babel-plugin-react-compiler@0.0.0-experimental-c23de8d-20240515:
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -13615,11 +13615,11 @@ snapshots:
|
|||||||
zod: 3.25.76
|
zod: 3.25.76
|
||||||
zod-validation-error: 2.1.0(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:
|
dependencies:
|
||||||
'@babel/helper-annotate-as-pure': 7.27.3
|
'@babel/helper-annotate-as-pure': 7.27.3
|
||||||
'@babel/helper-module-imports': 7.28.6
|
'@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
|
lodash: 4.18.1
|
||||||
picomatch: 2.3.2
|
picomatch: 2.3.2
|
||||||
styled-components: 6.1.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
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'
|
- '@babel/core'
|
||||||
- supports-color
|
- 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:
|
dependencies:
|
||||||
'@babel/core': 7.29.0
|
'@babel/core': 7.23.6
|
||||||
'@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)
|
||||||
'@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.29.0)
|
'@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.23.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)
|
||||||
'@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)
|
||||||
'@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)
|
||||||
'@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)
|
||||||
'@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)
|
||||||
'@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)
|
||||||
'@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)
|
||||||
'@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)
|
||||||
'@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)
|
||||||
'@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)
|
||||||
'@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)
|
||||||
'@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)
|
||||||
'@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)
|
||||||
|
|
||||||
babel-preset-jest@29.6.3(@babel/core@7.29.0):
|
babel-preset-jest@29.6.3(@babel/core@7.23.6):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/core': 7.29.0
|
'@babel/core': 7.23.6
|
||||||
babel-plugin-jest-hoist: 29.6.3
|
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: {}
|
babel-runtime-jsx-plus@0.1.5: {}
|
||||||
|
|
||||||
@@ -16410,9 +16410,9 @@ snapshots:
|
|||||||
|
|
||||||
jiti@2.7.0: {}
|
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:
|
optionalDependencies:
|
||||||
'@babel/core': 7.29.0
|
'@babel/core': 7.23.6
|
||||||
'@babel/template': 7.28.6
|
'@babel/template': 7.28.6
|
||||||
'@types/react': 18.3.28
|
'@types/react': 18.3.28
|
||||||
react: 18.3.1
|
react: 18.3.1
|
||||||
@@ -19805,12 +19805,12 @@ snapshots:
|
|||||||
css-to-react-native: 3.2.0
|
css-to-react-native: 3.2.0
|
||||||
react-dom: 18.3.1(react@18.3.1)
|
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:
|
dependencies:
|
||||||
client-only: 0.0.1
|
client-only: 0.0.1
|
||||||
react: 18.3.1
|
react: 18.3.1
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@babel/core': 7.29.0
|
'@babel/core': 7.23.6
|
||||||
|
|
||||||
stylelint-config-recommended@7.0.0(stylelint@14.8.2):
|
stylelint-config-recommended@7.0.0(stylelint@14.8.2):
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -20164,12 +20164,12 @@ snapshots:
|
|||||||
|
|
||||||
ua-parser-js@0.7.41: {}
|
ua-parser-js@0.7.41: {}
|
||||||
|
|
||||||
umi-presets-pro@2.0.3(@babel/core@7.29.0)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(antd@6.3.7(date-fns@2.30.0)(moment@2.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(chokidar@3.6.0)(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(encoding@0.1.13)(rc-field-form@2.7.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3)(umi@4.6.51(@babel/core@7.29.0)(@types/node@25.6.2)(@types/react@18.3.28)(eslint@9.39.4(jiti@2.7.0))(lightningcss@1.22.1)(prettier@3.8.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.29.0)(react@18.3.1))(stylelint@14.8.2)(terser@5.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:
|
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/max-plugin-openapi': 2.0.3(chokidar@3.6.0)(encoding@0.1.13)(typescript@5.9.3)
|
||||||
'@umijs/plugins': 4.6.51(@babel/core@7.29.0)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(antd@6.3.7(date-fns@2.30.0)(moment@2.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(rc-field-form@2.7.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
'@umijs/plugins': 4.6.51(@babel/core@7.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.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)))
|
||||||
swagger-ui-dist: 4.19.1
|
swagger-ui-dist: 4.19.1
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- '@babel/core'
|
- '@babel/core'
|
||||||
@@ -20193,17 +20193,17 @@ snapshots:
|
|||||||
isomorphic-fetch: 2.2.1
|
isomorphic-fetch: 2.2.1
|
||||||
qs: 6.15.1
|
qs: 6.15.1
|
||||||
|
|
||||||
umi@4.6.51(@babel/core@7.29.0)(@types/node@25.6.2)(@types/react@18.3.28)(eslint@8.35.0)(lightningcss@1.22.1)(prettier@3.8.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.29.0)(react@18.3.1))(stylelint@14.8.2)(terser@5.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:
|
dependencies:
|
||||||
'@babel/runtime': 7.23.6
|
'@babel/runtime': 7.23.6
|
||||||
'@umijs/bundler-utils': 4.6.51
|
'@umijs/bundler-utils': 4.6.51
|
||||||
'@umijs/bundler-webpack': 4.6.51(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14))
|
'@umijs/bundler-webpack': 4.6.51(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14))
|
||||||
'@umijs/core': 4.6.51
|
'@umijs/core': 4.6.51
|
||||||
'@umijs/lint': 4.6.51(eslint@8.35.0)(stylelint@14.8.2)(typescript@5.9.3)
|
'@umijs/lint': 4.6.51(eslint@8.35.0)(stylelint@14.8.2)(typescript@5.9.3)
|
||||||
'@umijs/preset-umi': 4.6.51(@types/node@25.6.2)(@types/react@18.3.28)(lightningcss@1.22.1)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.29.0)(react@18.3.1))(terser@5.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/renderer-react': 4.6.51(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||||
'@umijs/server': 4.6.51
|
'@umijs/server': 4.6.51
|
||||||
'@umijs/test': 4.6.51(@babel/core@7.29.0)
|
'@umijs/test': 4.6.51(@babel/core@7.23.6)
|
||||||
'@umijs/utils': 4.6.51
|
'@umijs/utils': 4.6.51
|
||||||
prettier-plugin-organize-imports: 3.2.4(prettier@3.8.3)(typescript@5.9.3)
|
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)
|
prettier-plugin-packagejson: 2.4.3(prettier@3.8.3)
|
||||||
@@ -20247,17 +20247,17 @@ snapshots:
|
|||||||
- webpack-hot-middleware
|
- webpack-hot-middleware
|
||||||
- webpack-plugin-serve
|
- webpack-plugin-serve
|
||||||
|
|
||||||
umi@4.6.51(@babel/core@7.29.0)(@types/node@25.6.2)(@types/react@18.3.28)(eslint@9.39.4(jiti@2.7.0))(lightningcss@1.22.1)(prettier@3.8.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.29.0)(react@18.3.1))(stylelint@14.8.2)(terser@5.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:
|
dependencies:
|
||||||
'@babel/runtime': 7.23.6
|
'@babel/runtime': 7.23.6
|
||||||
'@umijs/bundler-utils': 4.6.51
|
'@umijs/bundler-utils': 4.6.51
|
||||||
'@umijs/bundler-webpack': 4.6.51(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14))
|
'@umijs/bundler-webpack': 4.6.51(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14))
|
||||||
'@umijs/core': 4.6.51
|
'@umijs/core': 4.6.51
|
||||||
'@umijs/lint': 4.6.51(eslint@9.39.4(jiti@2.7.0))(stylelint@14.8.2)(typescript@5.9.3)
|
'@umijs/lint': 4.6.51(eslint@9.39.4(jiti@2.7.0))(stylelint@14.8.2)(typescript@5.9.3)
|
||||||
'@umijs/preset-umi': 4.6.51(@types/node@25.6.2)(@types/react@18.3.28)(lightningcss@1.22.1)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.29.0)(react@18.3.1))(terser@5.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/renderer-react': 4.6.51(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||||
'@umijs/server': 4.6.51
|
'@umijs/server': 4.6.51
|
||||||
'@umijs/test': 4.6.51(@babel/core@7.29.0)
|
'@umijs/test': 4.6.51(@babel/core@7.23.6)
|
||||||
'@umijs/utils': 4.6.51
|
'@umijs/utils': 4.6.51
|
||||||
prettier-plugin-organize-imports: 3.2.4(prettier@3.8.3)(typescript@5.9.3)
|
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)
|
prettier-plugin-packagejson: 2.4.3(prettier@3.8.3)
|
||||||
|
|||||||
+15
-83
@@ -3,9 +3,6 @@ import { GPUStackVersionAtom, UpdateCheckAtom, userAtom } from '@/atoms/user';
|
|||||||
import { setAtomStorage } from '@/atoms/utils';
|
import { setAtomStorage } from '@/atoms/utils';
|
||||||
import { DEFAULT_ENTER_PAGE, GPUSTACK_API_BASE_URL } from '@/config/settings';
|
import { DEFAULT_ENTER_PAGE, GPUSTACK_API_BASE_URL } from '@/config/settings';
|
||||||
import { COLOR_PRIMARY } from '@/config/theme/constants';
|
import { COLOR_PRIMARY } from '@/config/theme/constants';
|
||||||
import { queryClusterList } from '@/pages/cluster-management/apis';
|
|
||||||
import { ProviderValueMap } from '@/pages/cluster-management/config';
|
|
||||||
import { queryResourceEvents } from '@/pages/usage/apis/resource';
|
|
||||||
import { getGPUStackPlugin } from '@/plugins';
|
import { getGPUStackPlugin } from '@/plugins';
|
||||||
import { enterprisePluginReady } from '@/plugins/enterprise-ready';
|
import { enterprisePluginReady } from '@/plugins/enterprise-ready';
|
||||||
import { GPUStackPluginManager } from '@/plugins/manager';
|
import { GPUStackPluginManager } from '@/plugins/manager';
|
||||||
@@ -17,6 +14,10 @@ import {
|
|||||||
} from '@/services/profile/apis';
|
} from '@/services/profile/apis';
|
||||||
import { fetchSystemConfig } from '@/services/system/query-system-config';
|
import { fetchSystemConfig } from '@/services/system/query-system-config';
|
||||||
import { isOnline } from '@/utils';
|
import { isOnline } from '@/utils';
|
||||||
|
import {
|
||||||
|
markInitialStateProbed,
|
||||||
|
probeAccessFlags
|
||||||
|
} from '@/utils/access-probes';
|
||||||
import { installTenantFetch } from '@/utils/install-fetch';
|
import { installTenantFetch } from '@/utils/install-fetch';
|
||||||
import {
|
import {
|
||||||
IS_FIRST_LOGIN,
|
IS_FIRST_LOGIN,
|
||||||
@@ -41,80 +42,6 @@ const checkDefaultPage = async (userInfo: any) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Probes the caller's cluster list once so access predicates can gate
|
|
||||||
// GPU Service (Kubernetes-only). Cheap (one list request) and never
|
|
||||||
// blocks login — any failure just falls back to `undefined`, which
|
|
||||||
// the predicate treats as "unknown / don't restrict beyond role".
|
|
||||||
// The result is also mirrored into sessionStorage so access extensions
|
|
||||||
// that run without the initialState argument can read it (e.g. to
|
|
||||||
// override the admin shortcut in scopes where the menu shouldn't
|
|
||||||
// show even for admins).
|
|
||||||
const HAS_K8S_CLUSTER_KEY = 'hasKubernetesCluster';
|
|
||||||
const probeHasKubernetesCluster = async (): Promise<boolean | undefined> => {
|
|
||||||
try {
|
|
||||||
const res = await queryClusterList(
|
|
||||||
{ page: -1 },
|
|
||||||
{
|
|
||||||
skipErrorHandler: true
|
|
||||||
}
|
|
||||||
);
|
|
||||||
const value = (res?.items ?? []).some(
|
|
||||||
(c) => c?.provider === ProviderValueMap.Kubernetes
|
|
||||||
);
|
|
||||||
try {
|
|
||||||
window.sessionStorage.setItem(HAS_K8S_CLUSTER_KEY, JSON.stringify(value));
|
|
||||||
} catch {
|
|
||||||
// sessionStorage may be unavailable (Safari private mode); the
|
|
||||||
// access predicate already handles a missing value as "unknown".
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('probeHasKubernetesCluster error', error);
|
|
||||||
try {
|
|
||||||
window.sessionStorage.removeItem(HAS_K8S_CLUSTER_KEY);
|
|
||||||
} catch {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Probes whether the caller has ANY resource-usage events (GPU/CPU instance or
|
|
||||||
// storage lifecycle). Used alongside the cluster probe so a user who has run
|
|
||||||
// GPU instances still sees GPU Service / the full Usage page even if they
|
|
||||||
// currently have no Kubernetes cluster. Mirrored into sessionStorage for the
|
|
||||||
// access extensions; any failure → undefined ("unknown — don't restrict").
|
|
||||||
const HAS_RESOURCE_EVENTS_KEY = 'hasResourceEvents';
|
|
||||||
const probeHasResourceEvents = async (): Promise<boolean | undefined> => {
|
|
||||||
try {
|
|
||||||
// No date range = "ever"; scope is clamped to the caller server-side.
|
|
||||||
const res = await queryResourceEvents(
|
|
||||||
{ perPage: 1 },
|
|
||||||
{
|
|
||||||
skipErrorHandler: true
|
|
||||||
}
|
|
||||||
);
|
|
||||||
const value = (res?.pagination?.total ?? 0) > 0;
|
|
||||||
try {
|
|
||||||
window.sessionStorage.setItem(
|
|
||||||
HAS_RESOURCE_EVENTS_KEY,
|
|
||||||
JSON.stringify(value)
|
|
||||||
);
|
|
||||||
} catch {
|
|
||||||
// sessionStorage may be unavailable; predicate treats missing as unknown.
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('probeHasResourceEvents error', error);
|
|
||||||
try {
|
|
||||||
window.sessionStorage.removeItem(HAS_RESOURCE_EVENTS_KEY);
|
|
||||||
} catch {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// runtime configuration
|
// runtime configuration
|
||||||
export async function getInitialState(): Promise<{
|
export async function getInitialState(): Promise<{
|
||||||
fetchUserInfo: () => Promise<Global.UserInfo>;
|
fetchUserInfo: () => Promise<Global.UserInfo>;
|
||||||
@@ -235,19 +162,24 @@ export async function getInitialState(): Promise<{
|
|||||||
getAppVersionInfo();
|
getAppVersionInfo();
|
||||||
|
|
||||||
if (![DEFAULT_ENTER_PAGE.login].includes(location.pathname)) {
|
if (![DEFAULT_ENTER_PAGE.login].includes(location.pathname)) {
|
||||||
const [userInfo, hasKubernetesCluster, hasResourceEvents] =
|
const [userInfo, accessFlags] = await Promise.all([
|
||||||
await Promise.all([
|
|
||||||
fetchUserInfo(),
|
fetchUserInfo(),
|
||||||
probeHasKubernetesCluster(),
|
probeAccessFlags()
|
||||||
probeHasResourceEvents()
|
|
||||||
]);
|
]);
|
||||||
|
// Record that the probes ran for an authenticated user this page load
|
||||||
|
// (the refresh path) so the layout doesn't re-probe. A failed
|
||||||
|
// fetch (empty user — e.g. unauthenticated deep link that bounces to
|
||||||
|
// login) is NOT marked: the user will log in via SPA afterwards and
|
||||||
|
// the layout becomes responsible for probing.
|
||||||
|
if (userInfo?.username) {
|
||||||
|
markInitialStateProbed();
|
||||||
|
}
|
||||||
checkDefaultPage(userInfo);
|
checkDefaultPage(userInfo);
|
||||||
return {
|
return {
|
||||||
fetchUserInfo,
|
fetchUserInfo,
|
||||||
currentUser: userInfo,
|
currentUser: userInfo,
|
||||||
pluginData,
|
pluginData,
|
||||||
hasKubernetesCluster,
|
...accessFlags
|
||||||
hasResourceEvents
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return {
|
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 |
@@ -63,6 +63,7 @@ export const fromClusterCreationAtom = atom(false);
|
|||||||
export const clusterSessionAtom = atom<{
|
export const clusterSessionAtom = atom<{
|
||||||
firstAddWorker: boolean;
|
firstAddWorker: boolean;
|
||||||
firstAddCluster: boolean;
|
firstAddCluster: boolean;
|
||||||
|
presetClusterType?: 'model' | 'gpu';
|
||||||
// Provider to preselect when the create flow opens — set by the
|
// Provider to preselect when the create flow opens — set by the
|
||||||
// empty-state CTA on feature pages that need a specific provider
|
// empty-state CTA on feature pages that need a specific provider
|
||||||
// (e.g. GPU Service can only schedule on Kubernetes, so its
|
// (e.g. GPU Service can only schedule on Kubernetes, so its
|
||||||
|
|||||||
+10
-3
@@ -276,7 +276,6 @@ body {
|
|||||||
border-radius: var(--table-td-radius);
|
border-radius: var(--table-td-radius);
|
||||||
|
|
||||||
> td {
|
> td {
|
||||||
background-color: unset !important;
|
|
||||||
border-bottom: 1px solid var(--ant-color-split);
|
border-bottom: 1px solid var(--ant-color-split);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -639,10 +638,15 @@ body {
|
|||||||
inset: 53px auto auto 0 !important;
|
inset: 53px auto auto 0 !important;
|
||||||
border-radius: 0 0 var(--border-radius-base) var(--border-radius-base);
|
border-radius: 0 0 var(--border-radius-base) var(--border-radius-base);
|
||||||
border-top: none;
|
border-top: none;
|
||||||
max-height: 240px;
|
.ant-cascader-menus {
|
||||||
min-height: 100px;
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
&.gpu-selector {
|
&.gpu-selector {
|
||||||
|
.ant-cascader-menu {
|
||||||
|
max-height: 400px;
|
||||||
|
height: unset;
|
||||||
|
}
|
||||||
.ant-cascader-menu:last-child {
|
.ant-cascader-menu:last-child {
|
||||||
flex: auto;
|
flex: auto;
|
||||||
|
|
||||||
@@ -847,3 +851,6 @@ body {
|
|||||||
padding-right: 8px;
|
padding-right: 8px;
|
||||||
padding-block: 8px;
|
padding-block: 8px;
|
||||||
}
|
}
|
||||||
|
.ant-select-multiple .ant-select-content {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|||||||
+114
-36
@@ -1,54 +1,132 @@
|
|||||||
import useMemoizedFn from 'ahooks/lib/useMemoizedFn';
|
import useMemoizedFn from 'ahooks/lib/useMemoizedFn';
|
||||||
import { converter, modeHsl, modeRgb, useMode } from 'culori/fn';
|
import { clampChroma, formatHex, modeOklch, modeRgb, useMode } from 'culori/fn';
|
||||||
|
import useUserSettings from './use-user-settings';
|
||||||
|
|
||||||
const toHsl = converter('hsl');
|
// Linear-style minimal/neutral palette: a TIGHT blue -> violet band in OKLCH
|
||||||
|
// (perceptually uniform), kept moderate-chroma so fills read as clean and gentle
|
||||||
const DEFAULT_BRAND_HUE = 211; // brand color
|
// rather than candy-colored. We deliberately stay narrow and DON'T fan out to
|
||||||
const COOL_HUE_START = 180;
|
// green/magenta — in this aesthetic series are separated by LIGHTNESS, not by
|
||||||
const COOL_HUE_END = 280;
|
// spreading across the hue wheel. Every series (including the first) is generated
|
||||||
|
// from this one ramp, so the whole palette stays in a single cohesive color
|
||||||
|
// family — no special high-saturation brand color that clashes with the rest.
|
||||||
|
const COOL_HUE_START = 250; // blue
|
||||||
|
const COOL_HUE_END = 315; // violet (stops short of magenta/pink, stays neutral-cool)
|
||||||
const COOL_HUE_RANGE = COOL_HUE_END - COOL_HUE_START;
|
const COOL_HUE_RANGE = COOL_HUE_END - COOL_HUE_START;
|
||||||
const GOLDEN_RATIO_CONJUGATE = 0.618033988749895;
|
|
||||||
|
|
||||||
function colorStringToHue(input?: string): number | undefined {
|
/**
|
||||||
if (!input) return undefined;
|
* Vivid, distinct cool accents — for places that need a handful of "primary"
|
||||||
const color = toHsl(input);
|
* colors, one per card/section (e.g. the summary trend cards), NOT a stacked
|
||||||
if (!color || typeof color.h !== 'number') return undefined;
|
* multi-series palette. Every color is anchor-quality (bright + saturated) and
|
||||||
return color.h;
|
* spread evenly across the blue→violet band, so the set reads as several equally
|
||||||
}
|
* strong primaries rather than one bold + several washed-out fills.
|
||||||
|
*/
|
||||||
export default function useCoolColors() {
|
export function useCoolAccents() {
|
||||||
// const brandHue = useMemo(
|
|
||||||
// () => colorStringToHue(userSettings.colorPrimary) ?? DEFAULT_BRAND_HUE,
|
|
||||||
// [userSettings.colorPrimary]
|
|
||||||
// );
|
|
||||||
useMode(modeRgb);
|
useMode(modeRgb);
|
||||||
useMode(modeHsl);
|
useMode(modeOklch);
|
||||||
|
|
||||||
const brandHue = DEFAULT_BRAND_HUE;
|
const { isDarkTheme } = useUserSettings();
|
||||||
|
|
||||||
return useMemoizedFn((count: number): string[] => {
|
return useMemoizedFn((count: number): string[] => {
|
||||||
if (count <= 0) return [];
|
if (count <= 0) return [];
|
||||||
|
|
||||||
const colors: string[] = [];
|
const l = isDarkTheme ? 0.62 : 0.64;
|
||||||
|
const c = isDarkTheme ? 0.16 : 0.2;
|
||||||
|
|
||||||
|
const out: string[] = [];
|
||||||
for (let i = 0; i < count; i++) {
|
for (let i = 0; i < count; i++) {
|
||||||
let hue: number;
|
const t = count <= 1 ? 0 : i / (count - 1);
|
||||||
|
const hue = COOL_HUE_START + t * COOL_HUE_RANGE;
|
||||||
if (i === 0) {
|
out.push(
|
||||||
hue = brandHue - 5;
|
formatHex(clampChroma({ mode: 'oklch', l, c, h: hue }, 'oklch'))
|
||||||
} else {
|
);
|
||||||
const offset = (i * GOLDEN_RATIO_CONJUGATE) % 1;
|
|
||||||
hue = COOL_HUE_START + offset * COOL_HUE_RANGE;
|
|
||||||
|
|
||||||
if (Math.abs(hue - brandHue) < 5) {
|
|
||||||
hue = (hue + 10) % COOL_HUE_END;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
return out;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const s = i === 0 ? 100 : 75 + (i % 3) * 5;
|
export default function useCoolColors() {
|
||||||
const l = i === 0 ? 50 : 55 + (i % 2) * 5;
|
useMode(modeRgb);
|
||||||
|
useMode(modeOklch);
|
||||||
|
|
||||||
colors.push(`hsl(${Math.round(hue)}, ${s}%, ${l}%)`);
|
const { isDarkTheme } = useUserSettings();
|
||||||
|
|
||||||
|
return useMemoizedFn((count: number): string[] => {
|
||||||
|
if (count <= 0) return [];
|
||||||
|
|
||||||
|
// Moderate chroma: clean and crisp, but gentle (not neon). Too low reads as
|
||||||
|
// muddy/dirty; too high reads as harsh. Dark mode a touch lower so fills
|
||||||
|
// stay calm against the dark canvas.
|
||||||
|
const baseChroma = isDarkTheme ? 0.085 : 0.12;
|
||||||
|
|
||||||
|
// First series is the "primary" anchor: SAME blue family (the bluest end of
|
||||||
|
// the ramp, nearest the brand hue) but clearly brighter and more saturated —
|
||||||
|
// a vivid, clean brand-blue that reads as the base color. The rest of the
|
||||||
|
// palette stays low-chroma, so the anchor pops as the primary while the
|
||||||
|
// family still feels cohesive.
|
||||||
|
const colors: string[] = [
|
||||||
|
formatHex(
|
||||||
|
clampChroma(
|
||||||
|
{
|
||||||
|
mode: 'oklch',
|
||||||
|
l: isDarkTheme ? 0.62 : 0.66,
|
||||||
|
c: isDarkTheme ? 0.16 : 0.2,
|
||||||
|
h: COOL_HUE_START
|
||||||
|
},
|
||||||
|
'oklch'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
];
|
||||||
|
|
||||||
|
const rest = count - 1;
|
||||||
|
if (rest <= 0) return colors;
|
||||||
|
|
||||||
|
// Separation is driven by LIGHTNESS, not hue. The hue band is narrow, so as
|
||||||
|
// the count grows we add lightness TIERS — each tier reuses the same narrow
|
||||||
|
// hue ramp at a distinct lightness level, multiplying how many separable
|
||||||
|
// colors fit while keeping the whole palette in one cohesive family.
|
||||||
|
const tiers = rest <= 6 ? 1 : rest <= 12 ? 2 : 3;
|
||||||
|
const steps = Math.ceil(rest / tiers);
|
||||||
|
|
||||||
|
// Distinct lightness levels per tier count (index 0 → 2 tiers, 1 → 3 tiers).
|
||||||
|
// Lighter, airier levels for a fresh/crisp feel; kept in the upper-mid range
|
||||||
|
// so fills stay clean and legible.
|
||||||
|
const lightTiers = isDarkTheme
|
||||||
|
? [
|
||||||
|
[0.68, 0.5],
|
||||||
|
[0.72, 0.6, 0.48]
|
||||||
|
]
|
||||||
|
: [
|
||||||
|
[0.82, 0.64],
|
||||||
|
[0.84, 0.72, 0.6]
|
||||||
|
];
|
||||||
|
// Single-tier light/dark zig-zag for the common small-count case.
|
||||||
|
const [lightLo, lightHi] = isDarkTheme ? [0.5, 0.68] : [0.64, 0.82];
|
||||||
|
|
||||||
|
for (let i = 0; i < rest; i++) {
|
||||||
|
// Cycle the tier on every step so consecutive series always differ in
|
||||||
|
// lightness — exactly where stacked bars are hardest to tell apart.
|
||||||
|
const tier = i % tiers;
|
||||||
|
const step = Math.floor(i / tiers);
|
||||||
|
|
||||||
|
// Even hue ramp across the narrow band. Each tier is offset by a fraction
|
||||||
|
// of a step so same-step colors in different tiers don't share a hue.
|
||||||
|
const t = steps <= 1 ? 0.5 : (step + tier / tiers) / steps;
|
||||||
|
const hue = COOL_HUE_START + t * COOL_HUE_RANGE;
|
||||||
|
|
||||||
|
// 1 tier (≤6 series): light/dark zig-zag. 2-3 tiers: the tier's level.
|
||||||
|
const lightness =
|
||||||
|
tiers === 1 ? (i % 2 ? lightLo : lightHi) : lightTiers[tiers - 2][tier];
|
||||||
|
|
||||||
|
// Clamp chroma into the sRGB gamut so values don't get distorted by a raw
|
||||||
|
// channel clip when serialized to hex.
|
||||||
|
colors.push(
|
||||||
|
formatHex(
|
||||||
|
clampChroma(
|
||||||
|
{ mode: 'oklch', l: lightness, c: baseChroma, h: hue },
|
||||||
|
'oklch'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return colors;
|
return colors;
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ export function useQueryDataList<
|
|||||||
Response = Array<ListItem>
|
Response = Array<ListItem>
|
||||||
>(option: {
|
>(option: {
|
||||||
key: string;
|
key: string;
|
||||||
|
manual?: boolean;
|
||||||
responseType?: 'array' | 'object';
|
responseType?: 'array' | 'object';
|
||||||
fetchList: (
|
fetchList: (
|
||||||
params: Params,
|
params: Params,
|
||||||
@@ -38,6 +39,7 @@ export function useQueryDataList<
|
|||||||
fetchList,
|
fetchList,
|
||||||
getLabel,
|
getLabel,
|
||||||
getValue,
|
getValue,
|
||||||
|
manual = true,
|
||||||
responseType = 'array',
|
responseType = 'array',
|
||||||
errorMsg
|
errorMsg
|
||||||
} = option;
|
} = option;
|
||||||
@@ -70,7 +72,7 @@ export function useQueryDataList<
|
|||||||
return responseType === 'array' ? res.items || [] : res;
|
return responseType === 'array' ? res.items || [] : res;
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
manual: true,
|
manual: manual,
|
||||||
debounceWait: option.debounceWait || 300,
|
debounceWait: option.debounceWait || 300,
|
||||||
onSuccess: () => {},
|
onSuccess: () => {},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
@@ -105,16 +107,18 @@ export function useQueryDataList<
|
|||||||
export function useQueryData<Detail, Params = any>(option: {
|
export function useQueryData<Detail, Params = any>(option: {
|
||||||
key: string;
|
key: string;
|
||||||
delay?: number;
|
delay?: number;
|
||||||
|
manual?: boolean;
|
||||||
fetchDetail: (params: Params, options?: any) => Promise<Detail>;
|
fetchDetail: (params: Params, options?: any) => Promise<Detail>;
|
||||||
getData?: (response: Detail, params?: any) => any;
|
getData?: (response: Detail, params?: any) => any;
|
||||||
errorMsg?: string;
|
errorMsg?: string;
|
||||||
}): {
|
}): {
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
detailData: Detail;
|
detailData: Detail;
|
||||||
|
manual?: boolean;
|
||||||
cancelRequest: () => void;
|
cancelRequest: () => void;
|
||||||
fetchData: (params: Params, extra?: any) => Promise<Detail>;
|
fetchData: (params: Params, extra?: any) => Promise<Detail>;
|
||||||
} {
|
} {
|
||||||
const { key, fetchDetail, getData, errorMsg, delay } = option;
|
const { key, fetchDetail, getData, errorMsg, delay, manual = true } = option;
|
||||||
const axiosTokenRef = useRef<CancelTokenSource | null>(null);
|
const axiosTokenRef = useRef<CancelTokenSource | null>(null);
|
||||||
const [detailData, setDetailData] = useState<Detail>({} as Detail);
|
const [detailData, setDetailData] = useState<Detail>({} as Detail);
|
||||||
|
|
||||||
@@ -142,7 +146,7 @@ export function useQueryData<Detail, Params = any>(option: {
|
|||||||
return res;
|
return res;
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
manual: true,
|
manual: manual,
|
||||||
onSuccess: () => {},
|
onSuccess: () => {},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
message.error(
|
message.error(
|
||||||
|
|||||||
+46
-1
@@ -12,6 +12,7 @@ import useUserSettings from '@/hooks/use-user-settings';
|
|||||||
import useUserSettingsStorage from '@/hooks/use-user-settings-storage';
|
import useUserSettingsStorage from '@/hooks/use-user-settings-storage';
|
||||||
import useAddResource from '@/pages/dashboard/hooks/use-add-resource';
|
import useAddResource from '@/pages/dashboard/hooks/use-add-resource';
|
||||||
import { logout } from '@/pages/login/apis';
|
import { logout } from '@/pages/login/apis';
|
||||||
|
import { didInitialStateProbe, probeAccessFlags } from '@/utils/access-probes';
|
||||||
import {
|
import {
|
||||||
readColumnSettings,
|
readColumnSettings,
|
||||||
readState,
|
readState,
|
||||||
@@ -41,7 +42,7 @@ import {
|
|||||||
import { Button, ConfigProvider, Modal, theme } from 'antd';
|
import { Button, ConfigProvider, Modal, theme } from 'antd';
|
||||||
import { useAtom } from 'jotai';
|
import { useAtom } from 'jotai';
|
||||||
import 'overlayscrollbars/overlayscrollbars.css';
|
import 'overlayscrollbars/overlayscrollbars.css';
|
||||||
import { useMemo, useRef } from 'react';
|
import { useEffect, useMemo, useRef } from 'react';
|
||||||
import { PageContainerInner } from '../pages/_components/page-box';
|
import { PageContainerInner } from '../pages/_components/page-box';
|
||||||
import Exception from './Exception';
|
import Exception from './Exception';
|
||||||
import './Layout.css';
|
import './Layout.css';
|
||||||
@@ -155,6 +156,50 @@ export default (props: any) => {
|
|||||||
|
|
||||||
const { initialState, loading, setInitialState } = initialInfo;
|
const { initialState, loading, setInitialState } = initialInfo;
|
||||||
const access = useAccess();
|
const access = useAccess();
|
||||||
|
const probedForUserRef = useRef<string | null>(null);
|
||||||
|
|
||||||
|
// Backfill the access probes (cluster / resource-events) once we're in
|
||||||
|
// the authenticated shell. `getInitialState` runs only once at app
|
||||||
|
// boot and can't probe on the login page (no session => 401), so after
|
||||||
|
// a first login the flags arrive here as `undefined` — which the
|
||||||
|
// access predicate treats as "don't restrict", flashing GPU Service /
|
||||||
|
// the full Usage page on until a manual refresh. This layout mounts
|
||||||
|
// only post-auth (login is `layout:false`) and on every entry, so it's
|
||||||
|
// the reliable place to resolve them.
|
||||||
|
//
|
||||||
|
// The effect is keyed on the user IDENTITY, not on the flag values —
|
||||||
|
// gating on the flags is what made this fragile (on a refresh
|
||||||
|
// `getInitialState` commits `currentUser` and the flags in the same
|
||||||
|
// update, so a flag-gated effect sees them already-known and never
|
||||||
|
// fires). Whether to actually hit the network is decided by the
|
||||||
|
// module-scoped `didInitialStateProbe()` marker, which is true only
|
||||||
|
// when `getInitialState` already probed for an authenticated user this
|
||||||
|
// page load (the refresh path) — so we skip the duplicate request there
|
||||||
|
// but still probe on the SPA-login path. Keying on identity also
|
||||||
|
// re-probes correctly if the signed-in user changes.
|
||||||
|
const currentUser = initialState?.currentUser;
|
||||||
|
const username = currentUser?.username;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!username || !setInitialState) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (probedForUserRef.current === username) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
probedForUserRef.current = username;
|
||||||
|
// The refresh path already resolved the flags inside `getInitialState`
|
||||||
|
// for this user — don't issue a duplicate probe.
|
||||||
|
if (didInitialStateProbe()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
probeAccessFlags().then((accessFlags) => {
|
||||||
|
setInitialState((prev: any) => ({
|
||||||
|
...prev,
|
||||||
|
...accessFlags
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
}, [username, setInitialState]);
|
||||||
|
|
||||||
const userConfig = {
|
const userConfig = {
|
||||||
title: '',
|
title: '',
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export default {
|
|||||||
'ai.provider.ollama': 'Ollama',
|
'ai.provider.ollama': 'Ollama',
|
||||||
'ai.provider.openai': 'OpenAI',
|
'ai.provider.openai': 'OpenAI',
|
||||||
'ai.provider.openrouter': 'OpenRouter',
|
'ai.provider.openrouter': 'OpenRouter',
|
||||||
'ai.provider.qwen': 'Qwen',
|
'ai.provider.qwen': 'Alibaba Cloud Model Studio',
|
||||||
'ai.provider.spark': 'Spark',
|
'ai.provider.spark': 'Spark',
|
||||||
'ai.provider.stepfun': 'StepFun',
|
'ai.provider.stepfun': 'StepFun',
|
||||||
'ai.provider.together-ai': 'TogetherAI',
|
'ai.provider.together-ai': 'TogetherAI',
|
||||||
|
|||||||
@@ -144,7 +144,7 @@ export default {
|
|||||||
'clusters.addworker.theadNotes-02':
|
'clusters.addworker.theadNotes-02':
|
||||||
'T-Head PPU uses the Container Device Interface (CDI) for device injection and requires the <span class="bold-text">/var/run/cdi</span> directory to be available for CDI generation.',
|
'T-Head PPU uses the Container Device Interface (CDI) for device injection and requires the <span class="bold-text">/var/run/cdi</span> directory to be available for CDI generation.',
|
||||||
'clusters.addworker.nvidiaNotes':
|
'clusters.addworker.nvidiaNotes':
|
||||||
'The built-in inference backends in GPUStack v2.1 require <span class="bold-text">CUDA 12.6+</span>. Please ensure your NVIDIA driver version is <span class="bold-text">560</span> or newer.',
|
'The built-in inference backends in GPUStack require <span class="bold-text">CUDA 12.8+</span>. Please ensure your NVIDIA driver version is <span class="bold-text">570</span> or newer.',
|
||||||
'clusters.volume.title': 'Volume Mounts',
|
'clusters.volume.title': 'Volume Mounts',
|
||||||
'clusters.volume.name': 'Volume Name',
|
'clusters.volume.name': 'Volume Name',
|
||||||
'clusters.volume.mountPath': 'Container Path',
|
'clusters.volume.mountPath': 'Container Path',
|
||||||
|
|||||||
@@ -198,6 +198,8 @@ export default {
|
|||||||
'common.table.user': 'User',
|
'common.table.user': 'User',
|
||||||
'common.settings.instructions': 'Instructions',
|
'common.settings.instructions': 'Instructions',
|
||||||
'common.settings.language': 'Language',
|
'common.settings.language': 'Language',
|
||||||
|
'common.settings.language.tips':
|
||||||
|
'Set the display language for the interface.',
|
||||||
'common.delete.confirm':
|
'common.delete.confirm':
|
||||||
'Are you sure you want to delete the selected {type}?',
|
'Are you sure you want to delete the selected {type}?',
|
||||||
'common.delete.single.confirm':
|
'common.delete.single.confirm':
|
||||||
@@ -251,6 +253,11 @@ export default {
|
|||||||
'common.appearance.tips': 'Default follows system preference.',
|
'common.appearance.tips': 'Default follows system preference.',
|
||||||
'common.button.forgotpassword': 'Forgot password?',
|
'common.button.forgotpassword': 'Forgot password?',
|
||||||
'common.appearance.theme': 'Theme',
|
'common.appearance.theme': 'Theme',
|
||||||
|
'common.appearance.description':
|
||||||
|
'Customize how the interface looks on your device.',
|
||||||
|
'common.security': 'Security',
|
||||||
|
'common.security.description':
|
||||||
|
'Manage the password used to sign in to your account.',
|
||||||
'common.page.wentwrong': 'Something went wrong.',
|
'common.page.wentwrong': 'Something went wrong.',
|
||||||
'common.page.refresh.tips':
|
'common.page.refresh.tips':
|
||||||
'The page may need to be updated. Try refreshing it!',
|
'The page may need to be updated. Try refreshing it!',
|
||||||
|
|||||||
@@ -81,6 +81,8 @@ export default {
|
|||||||
'gpuservice.publicKey': 'SSH Public Key',
|
'gpuservice.publicKey': 'SSH Public Key',
|
||||||
'gpuservice.publicKey.add': 'Add SSH Public Key',
|
'gpuservice.publicKey.add': 'Add SSH Public Key',
|
||||||
'gpuservice.publicKey.edit': 'Edit 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.filter.name': 'Search by name',
|
||||||
'gpuservice.publicKey.label': 'SSH Public Key',
|
'gpuservice.publicKey.label': 'SSH Public Key',
|
||||||
'gpuservice.instance.ssh.enable': 'Enable SSH Access',
|
'gpuservice.instance.ssh.enable': 'Enable SSH Access',
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ export default {
|
|||||||
'users.password.modify.title': 'Modify Password',
|
'users.password.modify.title': 'Modify Password',
|
||||||
'users.password.modify.description':
|
'users.password.modify.description':
|
||||||
"For your account's security, please change your initial password.",
|
"For your account's security, please change your initial password.",
|
||||||
|
'users.password.modify.tips':
|
||||||
|
'Regularly updating your password helps keep your account secure.',
|
||||||
'users.password.confirm': 'Confirm New Password',
|
'users.password.confirm': 'Confirm New Password',
|
||||||
'users.password.confirm.empty': 'Please confirm the new password.',
|
'users.password.confirm.empty': 'Please confirm the new password.',
|
||||||
'users.password.confirm.error': 'The two passwords entered do not match.',
|
'users.password.confirm.error': 'The two passwords entered do not match.',
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export default {
|
|||||||
'ai.provider.ollama': 'Ollama',
|
'ai.provider.ollama': 'Ollama',
|
||||||
'ai.provider.openai': 'OpenAI',
|
'ai.provider.openai': 'OpenAI',
|
||||||
'ai.provider.openrouter': 'OpenRouter',
|
'ai.provider.openrouter': 'OpenRouter',
|
||||||
'ai.provider.qwen': 'Qwen',
|
'ai.provider.qwen': 'Alibaba Cloud Model Studio',
|
||||||
'ai.provider.spark': 'Spark',
|
'ai.provider.spark': 'Spark',
|
||||||
'ai.provider.stepfun': 'StepFun',
|
'ai.provider.stepfun': 'StepFun',
|
||||||
'ai.provider.together-ai': 'TogetherAI',
|
'ai.provider.together-ai': 'TogetherAI',
|
||||||
|
|||||||
@@ -144,7 +144,7 @@ export default {
|
|||||||
'clusters.addworker.theadNotes-02':
|
'clusters.addworker.theadNotes-02':
|
||||||
'T-Head PPU uses the Container Device Interface (CDI) for device injection and requires the <span class="bold-text">/var/run/cdi</span> directory to be available for CDI generation.',
|
'T-Head PPU uses the Container Device Interface (CDI) for device injection and requires the <span class="bold-text">/var/run/cdi</span> directory to be available for CDI generation.',
|
||||||
'clusters.addworker.nvidiaNotes':
|
'clusters.addworker.nvidiaNotes':
|
||||||
'The built-in inference backends in GPUStack v2.1 require <span class="bold-text">CUDA 12.6+</span>. Please ensure your NVIDIA driver version is <span class="bold-text">560</span> or newer.',
|
'The built-in inference backends in GPUStack require <span class="bold-text">CUDA 12.8+</span>. Please ensure your NVIDIA driver version is <span class="bold-text">570</span> or newer.',
|
||||||
'clusters.volume.title': 'Volume Mounts',
|
'clusters.volume.title': 'Volume Mounts',
|
||||||
'clusters.volume.name': 'Volume Name',
|
'clusters.volume.name': 'Volume Name',
|
||||||
'clusters.volume.mountPath': 'Container Path',
|
'clusters.volume.mountPath': 'Container Path',
|
||||||
|
|||||||
@@ -199,6 +199,7 @@ export default {
|
|||||||
'common.table.user': 'ユーザー',
|
'common.table.user': 'ユーザー',
|
||||||
'common.settings.instructions': '手順',
|
'common.settings.instructions': '手順',
|
||||||
'common.settings.language': '言語',
|
'common.settings.language': '言語',
|
||||||
|
'common.settings.language.tips': 'インターフェースの表示言語を設定します。',
|
||||||
'common.delete.confirm': '選択した {type} を削除してもよろしいですか?',
|
'common.delete.confirm': '選択した {type} を削除してもよろしいですか?',
|
||||||
'common.delete.single.confirm':
|
'common.delete.single.confirm':
|
||||||
'<span style="font-size: 13px;font-weight: 700">{name}</span> を削除してもよろしいですか?',
|
'<span style="font-size: 13px;font-weight: 700">{name}</span> を削除してもよろしいですか?',
|
||||||
@@ -251,6 +252,11 @@ export default {
|
|||||||
'common.appearance.tips': 'Default follows system preference.',
|
'common.appearance.tips': 'Default follows system preference.',
|
||||||
'common.button.forgotpassword': 'Forgot password?',
|
'common.button.forgotpassword': 'Forgot password?',
|
||||||
'common.appearance.theme': 'Theme',
|
'common.appearance.theme': 'Theme',
|
||||||
|
'common.appearance.description':
|
||||||
|
'デバイス上でのインターフェースの表示をカスタマイズします。',
|
||||||
|
'common.security': 'セキュリティ',
|
||||||
|
'common.security.description':
|
||||||
|
'アカウントへのログインに使用するパスワードを管理します。',
|
||||||
'common.page.wentwrong': 'Something went wrong.',
|
'common.page.wentwrong': 'Something went wrong.',
|
||||||
'common.page.refresh.tips':
|
'common.page.refresh.tips':
|
||||||
'The page may need to be updated. Try refreshing it!',
|
'The page may need to be updated. Try refreshing it!',
|
||||||
|
|||||||
@@ -80,6 +80,8 @@ export default {
|
|||||||
'gpuservice.publicKey': 'SSH 公開鍵',
|
'gpuservice.publicKey': 'SSH 公開鍵',
|
||||||
'gpuservice.publicKey.add': 'SSH 公開鍵を追加',
|
'gpuservice.publicKey.add': 'SSH 公開鍵を追加',
|
||||||
'gpuservice.publicKey.edit': 'SSH 公開鍵を編集',
|
'gpuservice.publicKey.edit': 'SSH 公開鍵を編集',
|
||||||
|
'gpuservice.publicKey.delete.tips':
|
||||||
|
'SSH 公開鍵を削除しても、既にアタッチされているインスタンスのアクセス権は取り消されません。アクセス権を削除するには、対象のインスタンスを個別に編集してください。',
|
||||||
'gpuservice.publicKey.filter.name': '名前で検索',
|
'gpuservice.publicKey.filter.name': '名前で検索',
|
||||||
'gpuservice.publicKey.label': 'SSH 公開鍵',
|
'gpuservice.publicKey.label': 'SSH 公開鍵',
|
||||||
'gpuservice.instance.ssh.enable': 'SSH アクセスを有効化',
|
'gpuservice.instance.ssh.enable': 'SSH アクセスを有効化',
|
||||||
|
|||||||
@@ -29,6 +29,8 @@ export default {
|
|||||||
'users.password.modify.title': 'パスワードを変更',
|
'users.password.modify.title': 'パスワードを変更',
|
||||||
'users.password.modify.description':
|
'users.password.modify.description':
|
||||||
'アカウントのセキュリティのため、初期パスワードを変更してください。',
|
'アカウントのセキュリティのため、初期パスワードを変更してください。',
|
||||||
|
'users.password.modify.tips':
|
||||||
|
'パスワードを定期的に更新すると、アカウントの安全を保てます。',
|
||||||
'users.password.confirm': '新しいパスワードを確認',
|
'users.password.confirm': '新しいパスワードを確認',
|
||||||
'users.password.confirm.empty': '新しいパスワードを確認してください。',
|
'users.password.confirm.empty': '新しいパスワードを確認してください。',
|
||||||
'users.password.confirm.error': '入力された2つのパスワードが一致しません。',
|
'users.password.confirm.error': '入力された2つのパスワードが一致しません。',
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export default {
|
|||||||
'ai.provider.ollama': 'Ollama',
|
'ai.provider.ollama': 'Ollama',
|
||||||
'ai.provider.openai': 'OpenAI',
|
'ai.provider.openai': 'OpenAI',
|
||||||
'ai.provider.openrouter': 'OpenRouter',
|
'ai.provider.openrouter': 'OpenRouter',
|
||||||
'ai.provider.qwen': 'Qwen',
|
'ai.provider.qwen': 'Alibaba Cloud Model Studio',
|
||||||
'ai.provider.spark': 'Spark',
|
'ai.provider.spark': 'Spark',
|
||||||
'ai.provider.stepfun': 'StepFun',
|
'ai.provider.stepfun': 'StepFun',
|
||||||
'ai.provider.together-ai': 'TogetherAI',
|
'ai.provider.together-ai': 'TogetherAI',
|
||||||
|
|||||||
@@ -145,7 +145,7 @@ export default {
|
|||||||
'clusters.addworker.theadNotes-02':
|
'clusters.addworker.theadNotes-02':
|
||||||
'T-Head PPU uses the Container Device Interface (CDI) for device injection and requires the <span class="bold-text">/var/run/cdi</span> directory to be available for CDI generation.',
|
'T-Head PPU uses the Container Device Interface (CDI) for device injection and requires the <span class="bold-text">/var/run/cdi</span> directory to be available for CDI generation.',
|
||||||
'clusters.addworker.nvidiaNotes':
|
'clusters.addworker.nvidiaNotes':
|
||||||
'The built-in inference backends in GPUStack v2.1 require <span class="bold-text">CUDA 12.6+</span>. Please ensure your NVIDIA driver version is <span class="bold-text">560</span> or newer.',
|
'The built-in inference backends in GPUStack require <span class="bold-text">CUDA 12.8+</span>. Please ensure your NVIDIA driver version is <span class="bold-text">570</span> or newer.',
|
||||||
'clusters.volume.title': 'Volume Mounts',
|
'clusters.volume.title': 'Volume Mounts',
|
||||||
'clusters.volume.name': 'Volume Name',
|
'clusters.volume.name': 'Volume Name',
|
||||||
'clusters.volume.mountPath': 'Container Path',
|
'clusters.volume.mountPath': 'Container Path',
|
||||||
|
|||||||
@@ -197,6 +197,7 @@ export default {
|
|||||||
'common.table.user': 'Пользователь',
|
'common.table.user': 'Пользователь',
|
||||||
'common.settings.instructions': 'Инструкции',
|
'common.settings.instructions': 'Инструкции',
|
||||||
'common.settings.language': 'Язык',
|
'common.settings.language': 'Язык',
|
||||||
|
'common.settings.language.tips': 'Задайте язык отображения интерфейса.',
|
||||||
'common.delete.confirm': 'Вы уверены, что хотите удалить выбранный {type}?',
|
'common.delete.confirm': 'Вы уверены, что хотите удалить выбранный {type}?',
|
||||||
'common.delete.single.confirm':
|
'common.delete.single.confirm':
|
||||||
'Вы уверены, что хотите удалить <span style="font-size: 13px;font-weight: 700">{name}</span>?',
|
'Вы уверены, что хотите удалить <span style="font-size: 13px;font-weight: 700">{name}</span>?',
|
||||||
@@ -250,6 +251,11 @@ export default {
|
|||||||
'common.appearance.tips': 'По умолчанию соответствует системным настройкам.',
|
'common.appearance.tips': 'По умолчанию соответствует системным настройкам.',
|
||||||
'common.button.forgotpassword': 'Забыли пароль?',
|
'common.button.forgotpassword': 'Забыли пароль?',
|
||||||
'common.appearance.theme': 'Тема',
|
'common.appearance.theme': 'Тема',
|
||||||
|
'common.appearance.description':
|
||||||
|
'Настройте внешний вид интерфейса на вашем устройстве.',
|
||||||
|
'common.security': 'Безопасность',
|
||||||
|
'common.security.description':
|
||||||
|
'Управляйте паролем для входа в учётную запись.',
|
||||||
'common.page.wentwrong': 'Что-то пошло не так.',
|
'common.page.wentwrong': 'Что-то пошло не так.',
|
||||||
'common.page.refresh.tips':
|
'common.page.refresh.tips':
|
||||||
'Страница может нуждаться в обновлении. Попробуйте обновить её!',
|
'Страница может нуждаться в обновлении. Попробуйте обновить её!',
|
||||||
|
|||||||
@@ -84,6 +84,8 @@ export default {
|
|||||||
'gpuservice.publicKey': 'Открытый ключ SSH',
|
'gpuservice.publicKey': 'Открытый ключ SSH',
|
||||||
'gpuservice.publicKey.add': 'Добавить открытый ключ SSH',
|
'gpuservice.publicKey.add': 'Добавить открытый ключ SSH',
|
||||||
'gpuservice.publicKey.edit': 'Изменить открытый ключ SSH',
|
'gpuservice.publicKey.edit': 'Изменить открытый ключ SSH',
|
||||||
|
'gpuservice.publicKey.delete.tips':
|
||||||
|
'Удаление открытого ключа SSH не отзывает доступ для уже подключённых экземпляров. Чтобы удалить доступ, отредактируйте эти экземпляры отдельно.',
|
||||||
'gpuservice.publicKey.filter.name': 'Поиск по имени',
|
'gpuservice.publicKey.filter.name': 'Поиск по имени',
|
||||||
'gpuservice.publicKey.label': 'Открытый ключ SSH',
|
'gpuservice.publicKey.label': 'Открытый ключ SSH',
|
||||||
'gpuservice.instance.ssh.enable': 'Включить SSH-доступ',
|
'gpuservice.instance.ssh.enable': 'Включить SSH-доступ',
|
||||||
|
|||||||
@@ -29,6 +29,8 @@ export default {
|
|||||||
'users.password.modify.title': 'Смена пароля',
|
'users.password.modify.title': 'Смена пароля',
|
||||||
'users.password.modify.description':
|
'users.password.modify.description':
|
||||||
'В целях безопасности измените первоначальный пароль.',
|
'В целях безопасности измените первоначальный пароль.',
|
||||||
|
'users.password.modify.tips':
|
||||||
|
'Регулярное обновление пароля помогает защитить вашу учётную запись.',
|
||||||
'users.password.confirm': 'Подтвердите новый пароль',
|
'users.password.confirm': 'Подтвердите новый пароль',
|
||||||
'users.password.confirm.empty': 'Подтвердите новый пароль',
|
'users.password.confirm.empty': 'Подтвердите новый пароль',
|
||||||
'users.password.confirm.error': 'Пароли не совпадают',
|
'users.password.confirm.error': 'Пароли не совпадают',
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export default {
|
|||||||
'ai.provider.ollama': 'Ollama',
|
'ai.provider.ollama': 'Ollama',
|
||||||
'ai.provider.openai': 'OpenAI',
|
'ai.provider.openai': 'OpenAI',
|
||||||
'ai.provider.openrouter': 'OpenRouter',
|
'ai.provider.openrouter': 'OpenRouter',
|
||||||
'ai.provider.qwen': 'Qwen',
|
'ai.provider.qwen': 'Alibaba Cloud Model Studio',
|
||||||
'ai.provider.spark': 'Spark',
|
'ai.provider.spark': 'Spark',
|
||||||
'ai.provider.stepfun': 'StepFun',
|
'ai.provider.stepfun': 'StepFun',
|
||||||
'ai.provider.together-ai': 'TogetherAI',
|
'ai.provider.together-ai': 'TogetherAI',
|
||||||
|
|||||||
@@ -145,7 +145,7 @@ export default {
|
|||||||
'clusters.addworker.theadNotes-02':
|
'clusters.addworker.theadNotes-02':
|
||||||
'T-Head PPU, cihaz enjeksiyonu için Container Device Interface (CDI) kullanır ve CDI oluşturma için <span class="bold-text">/var/run/cdi</span> dizininin kullanılabilir olmasını gerektirir.',
|
'T-Head PPU, cihaz enjeksiyonu için Container Device Interface (CDI) kullanır ve CDI oluşturma için <span class="bold-text">/var/run/cdi</span> dizininin kullanılabilir olmasını gerektirir.',
|
||||||
'clusters.addworker.nvidiaNotes':
|
'clusters.addworker.nvidiaNotes':
|
||||||
'GPUStack v2.1\'deki yerleşik çıkarım altyapıları <span class="bold-text">CUDA 12.6+</span> gerektirir. Lütfen NVIDIA sürücü sürümünüzün <span class="bold-text">560</span> veya daha yeni olduğundan emin olun.',
|
'GPUStack\'teki yerleşik çıkarım altyapıları <span class="bold-text">CUDA 12.8+</span> gerektirir. Lütfen NVIDIA sürücü sürümünüzün <span class="bold-text">570</span> veya daha yeni olduğundan emin olun.',
|
||||||
'clusters.volume.title': 'Volume Mounts',
|
'clusters.volume.title': 'Volume Mounts',
|
||||||
'clusters.volume.name': 'Volume Name',
|
'clusters.volume.name': 'Volume Name',
|
||||||
'clusters.volume.mountPath': 'Container Path',
|
'clusters.volume.mountPath': 'Container Path',
|
||||||
|
|||||||
@@ -199,6 +199,7 @@ export default {
|
|||||||
'common.table.user': 'Kullanıcı',
|
'common.table.user': 'Kullanıcı',
|
||||||
'common.settings.instructions': 'Talimatlar',
|
'common.settings.instructions': 'Talimatlar',
|
||||||
'common.settings.language': 'Dil',
|
'common.settings.language': 'Dil',
|
||||||
|
'common.settings.language.tips': 'Arayüzün görüntüleme dilini ayarlayın.',
|
||||||
'common.delete.confirm':
|
'common.delete.confirm':
|
||||||
'Seçili {type} öğesini silmek istediğinizden emin misiniz?',
|
'Seçili {type} öğesini silmek istediğinizden emin misiniz?',
|
||||||
'common.delete.single.confirm':
|
'common.delete.single.confirm':
|
||||||
@@ -254,6 +255,11 @@ export default {
|
|||||||
'common.appearance.tips': 'Varsayılan olarak sistem tercihini takip eder.',
|
'common.appearance.tips': 'Varsayılan olarak sistem tercihini takip eder.',
|
||||||
'common.button.forgotpassword': 'Şifrenizi mi unuttunuz?',
|
'common.button.forgotpassword': 'Şifrenizi mi unuttunuz?',
|
||||||
'common.appearance.theme': 'Tema',
|
'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.wentwrong': 'Bir şeyler ters gitti.',
|
||||||
'common.page.refresh.tips':
|
'common.page.refresh.tips':
|
||||||
'Sayfanın güncellenmesi gerekebilir. Yenilemeyi deneyin!',
|
'Sayfanın güncellenmesi gerekebilir. Yenilemeyi deneyin!',
|
||||||
|
|||||||
@@ -80,6 +80,8 @@ export default {
|
|||||||
'gpuservice.publicKey': 'SSH Açık Anahtarı',
|
'gpuservice.publicKey': 'SSH Açık Anahtarı',
|
||||||
'gpuservice.publicKey.add': 'SSH Açık Anahtarı Ekle',
|
'gpuservice.publicKey.add': 'SSH Açık Anahtarı Ekle',
|
||||||
'gpuservice.publicKey.edit': 'SSH Açık Anahtarını Düzenle',
|
'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.filter.name': 'Ada göre ara',
|
||||||
'gpuservice.publicKey.label': 'SSH Açık Anahtarı',
|
'gpuservice.publicKey.label': 'SSH Açık Anahtarı',
|
||||||
'gpuservice.instance.ssh.enable': 'SSH Erişimini Etkinleştir',
|
'gpuservice.instance.ssh.enable': 'SSH Erişimini Etkinleştir',
|
||||||
|
|||||||
@@ -29,6 +29,8 @@ export default {
|
|||||||
'users.password.modify.title': 'Şifreyi Değiştir',
|
'users.password.modify.title': 'Şifreyi Değiştir',
|
||||||
'users.password.modify.description':
|
'users.password.modify.description':
|
||||||
'Hesabınızın güvenliği için lütfen başlangıç şifrenizi değiştirin.',
|
'Hesabınızın güvenliği için lütfen başlangıç şifrenizi değiştirin.',
|
||||||
|
'users.password.modify.tips':
|
||||||
|
'Parolanızı düzenli olarak güncellemek hesabınızın güvenliğini korumaya yardımcı olur.',
|
||||||
'users.password.confirm': 'Yeni Şifreyi Onayla',
|
'users.password.confirm': 'Yeni Şifreyi Onayla',
|
||||||
'users.password.confirm.empty': 'Lütfen yeni şifreyi tekrar girin.',
|
'users.password.confirm.empty': 'Lütfen yeni şifreyi tekrar girin.',
|
||||||
'users.password.confirm.error': 'Girilen iki şifre eşleşmiyor.',
|
'users.password.confirm.error': 'Girilen iki şifre eşleşmiyor.',
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export default {
|
|||||||
'ai.provider.ollama': 'Ollama',
|
'ai.provider.ollama': 'Ollama',
|
||||||
'ai.provider.openai': 'OpenAI',
|
'ai.provider.openai': 'OpenAI',
|
||||||
'ai.provider.openrouter': 'OpenRouter',
|
'ai.provider.openrouter': 'OpenRouter',
|
||||||
'ai.provider.qwen': '通义千问',
|
'ai.provider.qwen': '阿里云百炼',
|
||||||
'ai.provider.spark': '讯飞星火',
|
'ai.provider.spark': '讯飞星火',
|
||||||
'ai.provider.stepfun': '阶跃星辰',
|
'ai.provider.stepfun': '阶跃星辰',
|
||||||
'ai.provider.together-ai': 'Together AI',
|
'ai.provider.together-ai': 'Together AI',
|
||||||
|
|||||||
@@ -138,7 +138,7 @@ export default {
|
|||||||
'clusters.addworker.theadNotes-02':
|
'clusters.addworker.theadNotes-02':
|
||||||
'平头哥(T-Head)PPU 使用容器设备接口(CDI)进行设备注入,因此需要确保 <span class="bold-text">/var/run/cdi</span> 目录可用以生成 CDI。',
|
'平头哥(T-Head)PPU 使用容器设备接口(CDI)进行设备注入,因此需要确保 <span class="bold-text">/var/run/cdi</span> 目录可用以生成 CDI。',
|
||||||
'clusters.addworker.nvidiaNotes':
|
'clusters.addworker.nvidiaNotes':
|
||||||
'GPUStack v2.1 内置推理后端依赖 <span class="bold-text">CUDA 12.6</span> 及以上版本,请确保 NVIDIA 驱动版本为 <span class="bold-text">560</span> 或以上。',
|
'GPUStack 内置推理后端依赖 <span class="bold-text">CUDA 12.8</span> 及以上版本,请确保 NVIDIA 驱动版本为 <span class="bold-text">570</span> 或以上。',
|
||||||
'clusters.volume.title': '卷挂载',
|
'clusters.volume.title': '卷挂载',
|
||||||
'clusters.volume.name': '卷名称',
|
'clusters.volume.name': '卷名称',
|
||||||
'clusters.volume.mountPath': '容器内路径',
|
'clusters.volume.mountPath': '容器内路径',
|
||||||
|
|||||||
@@ -190,6 +190,7 @@ export default {
|
|||||||
'common.table.user': '用户',
|
'common.table.user': '用户',
|
||||||
'common.settings.instructions': '操作指引',
|
'common.settings.instructions': '操作指引',
|
||||||
'common.settings.language': '语言',
|
'common.settings.language': '语言',
|
||||||
|
'common.settings.language.tips': '设置界面的显示语言。',
|
||||||
'common.delete.confirm': '确定删除选中的{type}吗?',
|
'common.delete.confirm': '确定删除选中的{type}吗?',
|
||||||
'common.delete.single.confirm':
|
'common.delete.single.confirm':
|
||||||
'确定删除以下内容?\n <span style="font-size: 13px;font-weight: 700">{name}</span>',
|
'确定删除以下内容?\n <span style="font-size: 13px;font-weight: 700">{name}</span>',
|
||||||
@@ -246,6 +247,9 @@ export default {
|
|||||||
'common.appearance.tips': '默认跟随系统设置',
|
'common.appearance.tips': '默认跟随系统设置',
|
||||||
'common.button.forgotpassword': '忘记密码?',
|
'common.button.forgotpassword': '忘记密码?',
|
||||||
'common.appearance.theme': '主题',
|
'common.appearance.theme': '主题',
|
||||||
|
'common.appearance.description': '自定义界面在您设备上的视觉表现。',
|
||||||
|
'common.security': '安全设置',
|
||||||
|
'common.security.description': '管理用于登录账户的密码。',
|
||||||
'common.page.wentwrong': '哎呀,出了点问题',
|
'common.page.wentwrong': '哎呀,出了点问题',
|
||||||
'common.page.refresh.tips': '页面似乎需要更新,刷新一下试试吧!',
|
'common.page.refresh.tips': '页面似乎需要更新,刷新一下试试吧!',
|
||||||
'common.tips.escape.disable': '请点击「取消」按钮或右上角 X 关闭窗口',
|
'common.tips.escape.disable': '请点击「取消」按钮或右上角 X 关闭窗口',
|
||||||
|
|||||||
@@ -75,6 +75,8 @@ export default {
|
|||||||
'gpuservice.publicKey': 'SSH 公钥',
|
'gpuservice.publicKey': 'SSH 公钥',
|
||||||
'gpuservice.publicKey.add': '添加 SSH 公钥',
|
'gpuservice.publicKey.add': '添加 SSH 公钥',
|
||||||
'gpuservice.publicKey.edit': '编辑 SSH 公钥',
|
'gpuservice.publicKey.edit': '编辑 SSH 公钥',
|
||||||
|
'gpuservice.publicKey.delete.tips':
|
||||||
|
'删除 SSH Public Key 不会撤销已挂载 GPU 实例的访问权限。如需移除访问权限,请分别编辑对应的 GPU 实例。',
|
||||||
'gpuservice.publicKey.filter.name': '按名称搜索',
|
'gpuservice.publicKey.filter.name': '按名称搜索',
|
||||||
'gpuservice.publicKey.label': 'SSH 公钥',
|
'gpuservice.publicKey.label': 'SSH 公钥',
|
||||||
'gpuservice.instance.ssh.enable': '启用 SSH 访问',
|
'gpuservice.instance.ssh.enable': '启用 SSH 访问',
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ export default {
|
|||||||
'users.password.length': '长度在6至64个字符之间',
|
'users.password.length': '长度在6至64个字符之间',
|
||||||
'users.password.modify.title': '修改密码',
|
'users.password.modify.title': '修改密码',
|
||||||
'users.password.modify.description': '为了确保您的账户安全,请修改初始密码',
|
'users.password.modify.description': '为了确保您的账户安全,请修改初始密码',
|
||||||
|
'users.password.modify.tips': '定期更新密码有助于保护账户安全。',
|
||||||
'users.password.confirm': '确认新密码',
|
'users.password.confirm': '确认新密码',
|
||||||
'users.password.confirm.empty': '请确认新密码',
|
'users.password.confirm.empty': '请确认新密码',
|
||||||
'users.password.confirm.error': '两次输入的密码不一致',
|
'users.password.confirm.error': '两次输入的密码不一致',
|
||||||
|
|||||||
@@ -1,9 +1,15 @@
|
|||||||
import useCoolColors from '@/hooks/use-cool-colors';
|
import useCoolColors from '@/hooks/use-cool-colors';
|
||||||
import { Chart } from '@gpustack/core-ui';
|
import { Chart } from '@gpustack/core-ui';
|
||||||
import { formatLargeNumber } from '@gpustack/core-ui/utils';
|
import { formatLargeNumber } from '@gpustack/core-ui/utils';
|
||||||
import { Empty, theme } from 'antd';
|
import { Empty, Spin, theme } from 'antd';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import React, { useEffect, useMemo, useRef } from 'react';
|
import React, {
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useRef,
|
||||||
|
useState
|
||||||
|
} from 'react';
|
||||||
|
|
||||||
export interface BarSeriesItem {
|
export interface BarSeriesItem {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -17,6 +23,7 @@ export interface BarChartProps {
|
|||||||
xAxisData: string[];
|
xAxisData: string[];
|
||||||
height: number | string;
|
height: number | string;
|
||||||
width?: number | string;
|
width?: number | string;
|
||||||
|
loading?: boolean;
|
||||||
legendData?: { name: string; icon?: string }[];
|
legendData?: { name: string; icon?: string }[];
|
||||||
labelFormatter?: (val: any, index?: number) => string;
|
labelFormatter?: (val: any, index?: number) => string;
|
||||||
tooltipValueFormatter?: (val: any) => string;
|
tooltipValueFormatter?: (val: any) => string;
|
||||||
@@ -38,6 +45,7 @@ const BarChart: React.FC<BarChartProps> = (props) => {
|
|||||||
height,
|
height,
|
||||||
width,
|
width,
|
||||||
legendData,
|
legendData,
|
||||||
|
loading,
|
||||||
labelFormatter,
|
labelFormatter,
|
||||||
tooltipValueFormatter,
|
tooltipValueFormatter,
|
||||||
title,
|
title,
|
||||||
@@ -48,6 +56,27 @@ const BarChart: React.FC<BarChartProps> = (props) => {
|
|||||||
const chartRef = useRef<{ chart: any } | null>(null);
|
const chartRef = useRef<{ chart: any } | null>(null);
|
||||||
const generateCoolColors = useCoolColors();
|
const generateCoolColors = useCoolColors();
|
||||||
|
|
||||||
|
// ECharts reads the DOM width at init time; with a "100%" width it can pick
|
||||||
|
// up a stale/tiny value while the flex child's layout is still resolving,
|
||||||
|
// rendering every bar squeezed at the left edge until its internal (throttled)
|
||||||
|
// ResizeObserver corrects it ~100ms later — a visible blue-sliver flash. We
|
||||||
|
// measure the container ourselves via a callback ref (which fires during
|
||||||
|
// commit, before paint) and feed ECharts an explicit pixel width, so the very
|
||||||
|
// first render is already correct. No gating, so the chart mounts with no
|
||||||
|
// extra delay; the ResizeObserver keeps the width in sync afterwards.
|
||||||
|
const resizeObserverRef = useRef<ResizeObserver | null>(null);
|
||||||
|
const [measuredWidth, setMeasuredWidth] = useState(0);
|
||||||
|
const measureRef = useCallback((node: HTMLDivElement | null) => {
|
||||||
|
resizeObserverRef.current?.disconnect();
|
||||||
|
if (!node) return;
|
||||||
|
setMeasuredWidth(node.clientWidth);
|
||||||
|
resizeObserverRef.current = new ResizeObserver(() => {
|
||||||
|
setMeasuredWidth(node.clientWidth);
|
||||||
|
});
|
||||||
|
resizeObserverRef.current.observe(node);
|
||||||
|
}, []);
|
||||||
|
useEffect(() => () => resizeObserverRef.current?.disconnect(), []);
|
||||||
|
|
||||||
const dynamicColors = useMemo(
|
const dynamicColors = useMemo(
|
||||||
() => generateCoolColors(seriesData.length),
|
() => generateCoolColors(seriesData.length),
|
||||||
[seriesData.length, generateCoolColors]
|
[seriesData.length, generateCoolColors]
|
||||||
@@ -277,18 +306,43 @@ const BarChart: React.FC<BarChartProps> = (props) => {
|
|||||||
justifyContent: 'center'
|
justifyContent: 'center'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
{loading ? (
|
||||||
|
<Spin size="middle" />
|
||||||
|
) : (
|
||||||
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />
|
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<div
|
||||||
|
ref={measureRef}
|
||||||
|
style={{ width: width || '100%', height, position: 'relative' }}
|
||||||
|
>
|
||||||
<Chart
|
<Chart
|
||||||
ref={chartRef as any}
|
ref={chartRef as any}
|
||||||
options={options as any}
|
options={options as any}
|
||||||
height={height}
|
height={height}
|
||||||
width={width || '100%'}
|
width={measuredWidth || width || '100%'}
|
||||||
/>
|
/>
|
||||||
|
{loading && (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
inset: 20,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
background: 'var(--ant-color-bg-container)',
|
||||||
|
opacity: 0.6,
|
||||||
|
pointerEvents: 'none'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Spin size="middle" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,68 +0,0 @@
|
|||||||
import { IconFont } from '@gpustack/core-ui';
|
|
||||||
import { Collapse, CollapseProps } from 'antd';
|
|
||||||
import React from 'react';
|
|
||||||
import styled from 'styled-components';
|
|
||||||
|
|
||||||
const CollapseInner = styled(Collapse)`
|
|
||||||
.ant-collapse-header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 10px !important;
|
|
||||||
padding-inline: 5px !important;
|
|
||||||
padding-block: 8px !important;
|
|
||||||
border-radius: var(--border-radius-base) !important;
|
|
||||||
font-size: 14px !important;
|
|
||||||
font-weight: 600 !important;
|
|
||||||
&:hover {
|
|
||||||
background-color: var(--ant-color-fill-tertiary) !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.ant-collapse-body {
|
|
||||||
padding-inline: 0 !important;
|
|
||||||
padding-block: 0 !important;
|
|
||||||
}
|
|
||||||
.ant-collapse-header-text {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
height: 24px;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const CollapsePanel: React.FC<{
|
|
||||||
items: CollapseProps['items'];
|
|
||||||
activeKey: string | string[];
|
|
||||||
accordion?: boolean;
|
|
||||||
defaultActiveKey?: string | string[];
|
|
||||||
onChange?: (key: string | string[]) => void;
|
|
||||||
styles?: Record<string, React.CSSProperties>;
|
|
||||||
}> = ({ items, activeKey, accordion, defaultActiveKey, onChange, styles }) => {
|
|
||||||
return (
|
|
||||||
<CollapseInner
|
|
||||||
expandIconPlacement="start"
|
|
||||||
bordered={false}
|
|
||||||
ghost
|
|
||||||
accordion={accordion}
|
|
||||||
activeKey={activeKey}
|
|
||||||
defaultActiveKey={defaultActiveKey}
|
|
||||||
onChange={onChange}
|
|
||||||
destroyOnHidden={false}
|
|
||||||
styles={{
|
|
||||||
...styles,
|
|
||||||
header: {
|
|
||||||
backgroundColor: 'var(--ant-collapse-header-bg)'
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
expandIcon={({ isActive }) => (
|
|
||||||
<IconFont
|
|
||||||
type="icon-down"
|
|
||||||
rotate={isActive ? 0 : -90}
|
|
||||||
style={{ fontSize: '14px' }}
|
|
||||||
></IconFont>
|
|
||||||
)}
|
|
||||||
items={items}
|
|
||||||
></CollapseInner>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default CollapsePanel;
|
|
||||||
@@ -1,250 +0,0 @@
|
|||||||
import {
|
|
||||||
readColumnSettings,
|
|
||||||
writeColumnSettings
|
|
||||||
} from '@/utils/localstore/index';
|
|
||||||
import { SettingOutlined } from '@ant-design/icons';
|
|
||||||
import { OverlayScroller } from '@gpustack/core-ui';
|
|
||||||
import { useIntl } from '@umijs/max';
|
|
||||||
import { Button, Checkbox, Col, Popover, Row, Tooltip } from 'antd';
|
|
||||||
import React, { useEffect } from 'react';
|
|
||||||
import styled from 'styled-components';
|
|
||||||
|
|
||||||
const Container = styled.div`
|
|
||||||
padding: 8px 12px;
|
|
||||||
padding-right: 4px;
|
|
||||||
.title {
|
|
||||||
font-weight: 500;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
}
|
|
||||||
.btn-wrapper {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
padding-top: 12px;
|
|
||||||
}
|
|
||||||
.buttons {
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
justify-content: flex-end;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const Title = styled.div`
|
|
||||||
font-weight: 500;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
margin-top: 4px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const LabelWrapper = styled.span`
|
|
||||||
color: var(--ant-color-text-secondary);
|
|
||||||
> span {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 4px;
|
|
||||||
color: var(--ant-color-text-secondary);
|
|
||||||
.sub-title {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const ColumnSettings: React.FC<{
|
|
||||||
width?: number;
|
|
||||||
fixedColumns?: string[];
|
|
||||||
tableName: string;
|
|
||||||
contentHeight: number;
|
|
||||||
columns: {
|
|
||||||
title: React.ReactNode;
|
|
||||||
dataIndex?: string;
|
|
||||||
children?: { title: React.ReactNode; dataIndex?: string }[];
|
|
||||||
}[];
|
|
||||||
selectedColumns?: string[];
|
|
||||||
defaultSelectedColumns?: string[];
|
|
||||||
grouped?: boolean;
|
|
||||||
onReset?: () => void;
|
|
||||||
onChange?: (selectedColumns: string[]) => void;
|
|
||||||
}> = (props) => {
|
|
||||||
const intl = useIntl();
|
|
||||||
const {
|
|
||||||
tableName,
|
|
||||||
contentHeight,
|
|
||||||
width = 420,
|
|
||||||
columns,
|
|
||||||
selectedColumns,
|
|
||||||
defaultSelectedColumns,
|
|
||||||
grouped,
|
|
||||||
onReset,
|
|
||||||
onChange,
|
|
||||||
fixedColumns
|
|
||||||
} = props;
|
|
||||||
const [open, setOpen] = React.useState(false);
|
|
||||||
const [innerSelectedColumns, setInnerSelectedColumns] = React.useState<
|
|
||||||
string[]
|
|
||||||
>(defaultSelectedColumns || []);
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (open) {
|
|
||||||
setInnerSelectedColumns(selectedColumns ?? defaultSelectedColumns ?? []);
|
|
||||||
}
|
|
||||||
}, [open, selectedColumns, defaultSelectedColumns]);
|
|
||||||
|
|
||||||
const handleToggle = () => {
|
|
||||||
setOpen(!open);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDraftChange = (columns: string[]) => {
|
|
||||||
setInnerSelectedColumns(columns);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSelectAll = () => {
|
|
||||||
if (grouped) {
|
|
||||||
const allCols: string[] = [];
|
|
||||||
columns.forEach((group) => {
|
|
||||||
group.children?.forEach((col) => {
|
|
||||||
if (col.dataIndex) {
|
|
||||||
allCols.push(col.dataIndex);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
handleDraftChange(allCols);
|
|
||||||
} else {
|
|
||||||
const allCols = columns
|
|
||||||
.map((col) => col.dataIndex)
|
|
||||||
.filter((dataIndex): dataIndex is string => Boolean(dataIndex));
|
|
||||||
handleDraftChange(allCols);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleConfirm = () => {
|
|
||||||
setOpen(false);
|
|
||||||
writeColumnSettings(tableName, innerSelectedColumns);
|
|
||||||
onChange?.(innerSelectedColumns);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleReset = () => {
|
|
||||||
const resetCols = defaultSelectedColumns ?? [];
|
|
||||||
writeColumnSettings(tableName, resetCols);
|
|
||||||
onChange?.(resetCols);
|
|
||||||
setInnerSelectedColumns(resetCols);
|
|
||||||
onReset?.();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleOpenChange = (isOpen: boolean) => {
|
|
||||||
setOpen(isOpen);
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const initColumns = async () => {
|
|
||||||
const stored = await readColumnSettings(tableName);
|
|
||||||
if (stored && stored.length > 0) {
|
|
||||||
setInnerSelectedColumns(stored);
|
|
||||||
onChange?.(stored);
|
|
||||||
} else {
|
|
||||||
setInnerSelectedColumns(defaultSelectedColumns || []);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
initColumns();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const contentRender = () => {
|
|
||||||
return (
|
|
||||||
<Container>
|
|
||||||
{!grouped && (
|
|
||||||
<div className="title">
|
|
||||||
{intl.formatMessage({ id: 'benchmark.table.columnSettings' })}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<OverlayScroller
|
|
||||||
maxHeight={contentHeight}
|
|
||||||
styles={{
|
|
||||||
wrapper: {
|
|
||||||
paddingInlineStart: 0
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Checkbox.Group
|
|
||||||
value={innerSelectedColumns}
|
|
||||||
onChange={handleDraftChange}
|
|
||||||
>
|
|
||||||
<>
|
|
||||||
{grouped ? (
|
|
||||||
columns.map((row, index) => (
|
|
||||||
<div key={index}>
|
|
||||||
<Title>{row.title}</Title>
|
|
||||||
<Row>
|
|
||||||
{row.children?.map((col) => (
|
|
||||||
<Col key={col.dataIndex} span={12}>
|
|
||||||
<Checkbox
|
|
||||||
disabled={fixedColumns?.includes(
|
|
||||||
col.dataIndex || ''
|
|
||||||
)}
|
|
||||||
value={col.dataIndex}
|
|
||||||
style={{ marginBottom: 8 }}
|
|
||||||
>
|
|
||||||
<LabelWrapper>{col.title}</LabelWrapper>
|
|
||||||
</Checkbox>
|
|
||||||
</Col>
|
|
||||||
))}
|
|
||||||
</Row>
|
|
||||||
</div>
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<Row>
|
|
||||||
{columns.map((col) => (
|
|
||||||
<Col key={col.dataIndex} span={12}>
|
|
||||||
<Checkbox
|
|
||||||
disabled={fixedColumns?.includes(col.dataIndex || '')}
|
|
||||||
value={col.dataIndex}
|
|
||||||
style={{ marginBottom: 8 }}
|
|
||||||
>
|
|
||||||
<LabelWrapper>{col.title}</LabelWrapper>
|
|
||||||
</Checkbox>
|
|
||||||
</Col>
|
|
||||||
))}
|
|
||||||
</Row>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
</Checkbox.Group>
|
|
||||||
</OverlayScroller>
|
|
||||||
|
|
||||||
<div className="btn-wrapper">
|
|
||||||
<Button size="middle" onClick={handleReset}>
|
|
||||||
{intl.formatMessage({ id: 'common.button.resetdefault' })}
|
|
||||||
</Button>
|
|
||||||
<div className="buttons">
|
|
||||||
<Button size="middle" type="primary" onClick={handleSelectAll}>
|
|
||||||
{intl.formatMessage({ id: 'common.checkbox.all' })}
|
|
||||||
</Button>
|
|
||||||
<Button size="middle" type="primary" onClick={handleConfirm}>
|
|
||||||
{intl.formatMessage({ id: 'common.button.save' })}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Container>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Popover
|
|
||||||
open={open}
|
|
||||||
onOpenChange={handleOpenChange}
|
|
||||||
trigger={'click'}
|
|
||||||
arrow={false}
|
|
||||||
placement="bottomRight"
|
|
||||||
content={contentRender()}
|
|
||||||
styles={{
|
|
||||||
root: {
|
|
||||||
width: width
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Tooltip
|
|
||||||
title={intl.formatMessage({ id: 'benchmark.table.columnSettings' })}
|
|
||||||
>
|
|
||||||
<Button onClick={handleToggle} icon={<SettingOutlined />}></Button>
|
|
||||||
</Tooltip>
|
|
||||||
</Popover>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ColumnSettings;
|
|
||||||
@@ -1,99 +0,0 @@
|
|||||||
import { useOverlayScroller } from '@gpustack/core-ui';
|
|
||||||
import React, { useCallback } from 'react';
|
|
||||||
import styled from 'styled-components';
|
|
||||||
import { WrapperContext } from './use-wrapper-context';
|
|
||||||
|
|
||||||
const Wrapper = styled.div`
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: space-between;
|
|
||||||
position: relative;
|
|
||||||
width: 100%;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const ContentWrapper = styled.div`
|
|
||||||
flex: 1;
|
|
||||||
position: relative;
|
|
||||||
overflow-y: auto;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const Footer = styled.div`
|
|
||||||
padding-block: 0;
|
|
||||||
background-color: var(--ant-color-bg-elevated);
|
|
||||||
`;
|
|
||||||
|
|
||||||
interface ColumnWrapperProps {
|
|
||||||
children: React.ReactNode;
|
|
||||||
footer?: React.ReactNode;
|
|
||||||
maxHeight?: string | number;
|
|
||||||
paddingBottom?: number;
|
|
||||||
styles?: {
|
|
||||||
wrapper?: React.CSSProperties;
|
|
||||||
container?: React.CSSProperties;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const ColumnWrapper: React.FC<ColumnWrapperProps> = ({
|
|
||||||
children,
|
|
||||||
footer,
|
|
||||||
maxHeight,
|
|
||||||
styles = {}
|
|
||||||
}) => {
|
|
||||||
const scroller = React.useRef<any>(null);
|
|
||||||
const footerRef = React.useRef<HTMLDivElement>(null);
|
|
||||||
const contentRef = React.useRef<HTMLDivElement>(null);
|
|
||||||
const {
|
|
||||||
initialize,
|
|
||||||
instance,
|
|
||||||
scrollEventElement,
|
|
||||||
scrollToBottom,
|
|
||||||
scrollToTarget,
|
|
||||||
getScrollElementScrollableHeight
|
|
||||||
} = useOverlayScroller({
|
|
||||||
options: {
|
|
||||||
scrollbars: {
|
|
||||||
autoHide: 'move'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (scroller.current) {
|
|
||||||
initialize(scroller.current);
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const setContentPaddingBottom = useCallback((padding: number) => {
|
|
||||||
contentRef.current!.style.paddingBottom = `${padding}px`;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<WrapperContext.Provider
|
|
||||||
value={{
|
|
||||||
scroller: scroller,
|
|
||||||
osInstance: instance,
|
|
||||||
scrollEventElement,
|
|
||||||
getScrollElementScrollableHeight,
|
|
||||||
scrollToBottom,
|
|
||||||
scrollToTarget,
|
|
||||||
setSScrollContentPaddingBottom: setContentPaddingBottom
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Wrapper style={{ height: maxHeight || '100%', ...styles.wrapper }}>
|
|
||||||
<ContentWrapper
|
|
||||||
ref={scroller}
|
|
||||||
style={{
|
|
||||||
padding: '16px 24px',
|
|
||||||
...styles.container
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div ref={contentRef}>{children}</div>
|
|
||||||
</ContentWrapper>
|
|
||||||
{footer && <Footer ref={footerRef}>{footer}</Footer>}
|
|
||||||
</Wrapper>
|
|
||||||
</WrapperContext.Provider>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ColumnWrapper;
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
import { createContext, useContext } from 'react';
|
|
||||||
|
|
||||||
interface WrapperContextProps {
|
|
||||||
osInstance?: any;
|
|
||||||
scroller?: any;
|
|
||||||
scrollEventElement?: any;
|
|
||||||
scrollToBottom?: () => void;
|
|
||||||
scrollToTop?: () => void;
|
|
||||||
getScrollElementScrollableHeight?: () => {
|
|
||||||
scrollHeight: number;
|
|
||||||
scrollTop: number;
|
|
||||||
};
|
|
||||||
scrollToTarget?: (target: any, offset?: number) => void;
|
|
||||||
getScrollElement?: () => HTMLElement | null;
|
|
||||||
setSScrollContentPaddingBottom?: (padding: number) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const WrapperContext = createContext<WrapperContextProps>(
|
|
||||||
{} as WrapperContextProps
|
|
||||||
);
|
|
||||||
|
|
||||||
export const useWrapperContext = () => {
|
|
||||||
const context = useContext(WrapperContext);
|
|
||||||
if (!context) {
|
|
||||||
throw new Error('useWrapperContext must be used within a WrapperProvider');
|
|
||||||
}
|
|
||||||
return context;
|
|
||||||
};
|
|
||||||
@@ -1,155 +0,0 @@
|
|||||||
import { CloseOutlined } from '@ant-design/icons';
|
|
||||||
import { OverlayScroller } from '@gpustack/core-ui';
|
|
||||||
import { useIntl } from '@umijs/max';
|
|
||||||
import { Button, Form } from 'antd';
|
|
||||||
import classNames from 'classnames';
|
|
||||||
import React, { forwardRef, useImperativeHandle } from 'react';
|
|
||||||
import filterFormCss from '../styles/filter-form.less';
|
|
||||||
|
|
||||||
const FilterForm: React.FC<
|
|
||||||
React.PropsWithChildren & {
|
|
||||||
ref?: any;
|
|
||||||
width?: number;
|
|
||||||
contentHeight?: number | string;
|
|
||||||
initialValues?: any;
|
|
||||||
hasFilters?: boolean;
|
|
||||||
open?: boolean;
|
|
||||||
onClear?: () => void;
|
|
||||||
onClose?: () => void;
|
|
||||||
onValuesChange?: (ChangeValues: any, allValues: any) => void;
|
|
||||||
styles?: {
|
|
||||||
container?: React.CSSProperties;
|
|
||||||
wrapper?: React.CSSProperties;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
> = forwardRef(
|
|
||||||
(
|
|
||||||
{
|
|
||||||
children,
|
|
||||||
open,
|
|
||||||
width = 300,
|
|
||||||
contentHeight = 400,
|
|
||||||
initialValues = {},
|
|
||||||
styles,
|
|
||||||
onClose,
|
|
||||||
onClear,
|
|
||||||
onValuesChange
|
|
||||||
},
|
|
||||||
ref
|
|
||||||
) => {
|
|
||||||
const intl = useIntl();
|
|
||||||
const [form] = Form.useForm();
|
|
||||||
|
|
||||||
const handleOnReset = () => {
|
|
||||||
form.resetFields(Object.keys(initialValues));
|
|
||||||
form.resetFields();
|
|
||||||
onValuesChange?.({}, form.getFieldsValue());
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleOnClose = () => {
|
|
||||||
onClose?.();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleOnValuesChange = (changedValues: any, allValues: any) => {
|
|
||||||
onValuesChange?.(changedValues, allValues);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleOnClear = () => {
|
|
||||||
handleOnReset();
|
|
||||||
};
|
|
||||||
|
|
||||||
const filtersCount = Object.values(form.getFieldsValue()).filter(
|
|
||||||
(value) => value !== undefined && value !== null && value !== ''
|
|
||||||
).length;
|
|
||||||
|
|
||||||
const renderFooter = () => {
|
|
||||||
return (
|
|
||||||
<div className={filterFormCss['btn-wrapper']}>
|
|
||||||
<Button size="middle" onClick={handleOnReset}>
|
|
||||||
{intl.formatMessage({ id: 'common.button.reset' })}
|
|
||||||
</Button>
|
|
||||||
<div className={filterFormCss.buttons}>
|
|
||||||
<Button size="middle" type="primary" onClick={handleOnClose}>
|
|
||||||
{intl.formatMessage({ id: 'common.button.close' })}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
useImperativeHandle(ref, () => ({
|
|
||||||
form,
|
|
||||||
reset: handleOnReset,
|
|
||||||
getValues: () => form.getFieldsValue(),
|
|
||||||
setValues: (values: any) => form.setFieldsValue(values)
|
|
||||||
}));
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={classNames(filterFormCss.wrapper, {
|
|
||||||
[filterFormCss.show]: open
|
|
||||||
})}
|
|
||||||
style={{
|
|
||||||
width: open ? width : 0,
|
|
||||||
height: contentHeight,
|
|
||||||
...styles?.wrapper
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
style={{ width: width, ...styles?.container }}
|
|
||||||
className={filterFormCss.container}
|
|
||||||
>
|
|
||||||
<div className={filterFormCss.title}>
|
|
||||||
<span
|
|
||||||
style={{
|
|
||||||
fontWeight: 500,
|
|
||||||
color: 'var(--ant-color-text-tertiary)'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{intl.formatMessage({ id: 'common.filter.label' })}
|
|
||||||
</span>
|
|
||||||
<Button
|
|
||||||
size="small"
|
|
||||||
icon={<CloseOutlined />}
|
|
||||||
onClick={handleOnClose}
|
|
||||||
type="text"
|
|
||||||
style={{
|
|
||||||
color: 'var(--ant-color-text-secondary)'
|
|
||||||
}}
|
|
||||||
></Button>
|
|
||||||
</div>
|
|
||||||
<OverlayScroller
|
|
||||||
maxHeight={contentHeight}
|
|
||||||
styles={{
|
|
||||||
wrapper: {
|
|
||||||
paddingInline: 8
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Form
|
|
||||||
onValuesChange={handleOnValuesChange}
|
|
||||||
initialValues={initialValues}
|
|
||||||
form={form}
|
|
||||||
layout="vertical"
|
|
||||||
styles={{
|
|
||||||
label: {
|
|
||||||
lineHeight: 1,
|
|
||||||
height: 'auto',
|
|
||||||
marginBottom: 8,
|
|
||||||
fontWeight: 500
|
|
||||||
},
|
|
||||||
content: {
|
|
||||||
minHeight: 0
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</Form>
|
|
||||||
</OverlayScroller>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
export default FilterForm;
|
|
||||||
@@ -1,90 +0,0 @@
|
|||||||
import { ColumnWrapper, GSDrawer, ModalFooter } from '@gpustack/core-ui';
|
|
||||||
import type { DrawerProps } from 'antd';
|
|
||||||
import { Tag } from 'antd';
|
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
const ModalFooterStyle = {
|
|
||||||
padding: '16px 24px 8px',
|
|
||||||
display: 'flex',
|
|
||||||
justifyContent: 'flex-end'
|
|
||||||
};
|
|
||||||
|
|
||||||
type AddModalProps = {
|
|
||||||
title: React.ReactNode;
|
|
||||||
open: boolean;
|
|
||||||
onCancel?: () => void;
|
|
||||||
children?: React.ReactNode;
|
|
||||||
onSubmit?: () => void;
|
|
||||||
width?: number | string;
|
|
||||||
footer?: React.ReactNode;
|
|
||||||
subTitle?: React.ReactNode;
|
|
||||||
push?: DrawerProps['push'];
|
|
||||||
};
|
|
||||||
const FormDrawer: React.FC<AddModalProps> = ({
|
|
||||||
title,
|
|
||||||
open,
|
|
||||||
onCancel,
|
|
||||||
onSubmit,
|
|
||||||
children,
|
|
||||||
width = 600,
|
|
||||||
subTitle,
|
|
||||||
footer,
|
|
||||||
push
|
|
||||||
}) => {
|
|
||||||
return (
|
|
||||||
<GSDrawer
|
|
||||||
title={
|
|
||||||
<>
|
|
||||||
{title}
|
|
||||||
{subTitle && (
|
|
||||||
<Tag
|
|
||||||
variant="outlined"
|
|
||||||
style={{
|
|
||||||
fontSize: 12,
|
|
||||||
fontWeight: 400,
|
|
||||||
marginLeft: 8,
|
|
||||||
borderRadius: 4,
|
|
||||||
borderColor: 'var(--ant-color-border-secondary)',
|
|
||||||
color: 'var(--ant-color-text-secondary)'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{subTitle}
|
|
||||||
</Tag>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
open={open}
|
|
||||||
onClose={onCancel}
|
|
||||||
destroyOnHidden={true}
|
|
||||||
closeIcon={false}
|
|
||||||
mask={{
|
|
||||||
closable: false
|
|
||||||
}}
|
|
||||||
keyboard={false}
|
|
||||||
push={push}
|
|
||||||
styles={{
|
|
||||||
wrapper: { width }
|
|
||||||
}}
|
|
||||||
footer={false}
|
|
||||||
>
|
|
||||||
<ColumnWrapper
|
|
||||||
styles={{
|
|
||||||
container: { paddingBlock: 0 }
|
|
||||||
}}
|
|
||||||
footer={
|
|
||||||
footer ?? (
|
|
||||||
<ModalFooter
|
|
||||||
onOk={onSubmit}
|
|
||||||
onCancel={onCancel}
|
|
||||||
style={ModalFooterStyle}
|
|
||||||
></ModalFooter>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</ColumnWrapper>
|
|
||||||
</GSDrawer>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default FormDrawer;
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
.mask {
|
|
||||||
position: fixed;
|
|
||||||
inset: 0;
|
|
||||||
z-index: 1004;
|
|
||||||
height: 100%;
|
|
||||||
background-color: var(--ant-color-bg-mask);
|
|
||||||
opacity: 0;
|
|
||||||
transition: opacity var(--ant-motion-duration-slow)
|
|
||||||
var(--ant-motion-ease-in-out);
|
|
||||||
}
|
|
||||||
|
|
||||||
.maskOpen {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.overlay {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: auto;
|
|
||||||
left: auto;
|
|
||||||
z-index: 1005;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
height: 100vh;
|
|
||||||
overflow: hidden;
|
|
||||||
background: var(--ant-color-bg-elevated);
|
|
||||||
border-radius: var(--ant-border-radius-lg) 0 0 var(--ant-border-radius-lg);
|
|
||||||
box-shadow:
|
|
||||||
-6px 0 16px 0 rgb(0 0 0 / 8%),
|
|
||||||
-3px 0 6px -4px rgb(0 0 0 / 12%),
|
|
||||||
-9px 0 28px 8px rgb(0 0 0 / 5%);
|
|
||||||
transform: translateX(100%);
|
|
||||||
transition: transform var(--ant-motion-duration-slow)
|
|
||||||
var(--ant-motion-ease-in-out);
|
|
||||||
}
|
|
||||||
|
|
||||||
.overlayOpen {
|
|
||||||
transform: translateX(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
.header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 12px;
|
|
||||||
min-height: 56px;
|
|
||||||
padding: var(--ant-padding) var(--ant-padding-lg);
|
|
||||||
border-bottom: 1px solid var(--ant-color-split);
|
|
||||||
color: var(--ant-color-text);
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 600;
|
|
||||||
line-height: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title {
|
|
||||||
display: flex;
|
|
||||||
min-width: 0;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.subTitle {
|
|
||||||
margin-left: 8px;
|
|
||||||
border-color: var(--ant-color-border-secondary);
|
|
||||||
border-radius: 4px;
|
|
||||||
color: var(--ant-color-text-secondary);
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 400;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content {
|
|
||||||
padding-block: 16px;
|
|
||||||
flex: 1;
|
|
||||||
height: calc(100vh - 57px);
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer {
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
padding: 16px 24px 8px;
|
|
||||||
}
|
|
||||||
@@ -1,122 +0,0 @@
|
|||||||
import { ColumnWrapper, IconFont } from '@gpustack/core-ui';
|
|
||||||
import { Button, Tag } from 'antd';
|
|
||||||
import classNames from 'classnames';
|
|
||||||
import React from 'react';
|
|
||||||
import { createPortal } from 'react-dom';
|
|
||||||
import styles from './form-overlay-view.module.less';
|
|
||||||
|
|
||||||
type FormOverlayViewProps = {
|
|
||||||
title: React.ReactNode;
|
|
||||||
open: boolean;
|
|
||||||
onCancel?: () => void;
|
|
||||||
children?: React.ReactNode;
|
|
||||||
onSubmit?: () => void;
|
|
||||||
footer?: React.ReactNode;
|
|
||||||
subTitle?: React.ReactNode;
|
|
||||||
width?: number | string;
|
|
||||||
className?: string;
|
|
||||||
style?: React.CSSProperties;
|
|
||||||
maskClosable?: boolean;
|
|
||||||
getContainer?: () => HTMLElement | null | undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
const FormOverlayView: React.FC<FormOverlayViewProps> = ({
|
|
||||||
title,
|
|
||||||
open,
|
|
||||||
onCancel,
|
|
||||||
onSubmit,
|
|
||||||
children,
|
|
||||||
subTitle,
|
|
||||||
footer,
|
|
||||||
width = 600,
|
|
||||||
className,
|
|
||||||
style,
|
|
||||||
maskClosable = false,
|
|
||||||
getContainer
|
|
||||||
}) => {
|
|
||||||
const [container, setContainer] = React.useState<HTMLElement | null>(null);
|
|
||||||
const [mounted, setMounted] = React.useState(false);
|
|
||||||
const [active, setActive] = React.useState(false);
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (open) {
|
|
||||||
setContainer(getContainer?.() ?? null);
|
|
||||||
setMounted(true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setActive(false);
|
|
||||||
}, [getContainer, open]);
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (!mounted || !container) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const id = requestAnimationFrame(() => setActive(true));
|
|
||||||
return () => cancelAnimationFrame(id);
|
|
||||||
}, [mounted, container]);
|
|
||||||
|
|
||||||
const handleTransitionEnd = (e: React.TransitionEvent<HTMLDivElement>) => {
|
|
||||||
if (e.target !== e.currentTarget || e.propertyName !== 'transform') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!open && !active) {
|
|
||||||
setMounted(false);
|
|
||||||
setContainer(null);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!mounted || !container) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return createPortal(
|
|
||||||
<>
|
|
||||||
<div
|
|
||||||
className={classNames(styles.mask, { [styles.maskOpen]: active })}
|
|
||||||
onClick={maskClosable ? onCancel : undefined}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className={classNames(
|
|
||||||
styles.overlay,
|
|
||||||
{ [styles.overlayOpen]: active },
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
style={{ width, ...style }}
|
|
||||||
role="dialog"
|
|
||||||
aria-modal="true"
|
|
||||||
onTransitionEnd={handleTransitionEnd}
|
|
||||||
>
|
|
||||||
<div className={styles.header}>
|
|
||||||
<Button
|
|
||||||
type="text"
|
|
||||||
size="small"
|
|
||||||
style={{ fontWeight: 600, fontSize: 16 }}
|
|
||||||
icon={<IconFont type="icon-down2" rotate={90} />}
|
|
||||||
onClick={onCancel}
|
|
||||||
/>
|
|
||||||
<div className={styles.title}>
|
|
||||||
{title}
|
|
||||||
{subTitle && (
|
|
||||||
<Tag variant="outlined" className={styles.subTitle}>
|
|
||||||
{subTitle}
|
|
||||||
</Tag>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<ColumnWrapper
|
|
||||||
styles={{
|
|
||||||
container: { paddingBlock: 16 }
|
|
||||||
}}
|
|
||||||
footer={footer}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</ColumnWrapper>
|
|
||||||
</div>
|
|
||||||
</>,
|
|
||||||
container
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default FormOverlayView;
|
|
||||||
File diff suppressed because one or more lines are too long
@@ -1,26 +0,0 @@
|
|||||||
import { update } from 'jdenticon';
|
|
||||||
import React, { useEffect, useRef } from 'react';
|
|
||||||
|
|
||||||
interface IdenticonProps {
|
|
||||||
value: string;
|
|
||||||
size: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
const Identicon: React.FC<IdenticonProps> = ({
|
|
||||||
value = 'test',
|
|
||||||
size = 28
|
|
||||||
}: IdenticonProps) => {
|
|
||||||
const icon = useRef<any>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
update(icon.current, value);
|
|
||||||
}, [value]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<svg data-jdenticon-value={value} height={size} ref={icon} width={size} />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Identicon;
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
import {
|
|
||||||
digitReg,
|
|
||||||
lowercaseReg,
|
|
||||||
specialCharacterReg,
|
|
||||||
uppercaseReg
|
|
||||||
} from '@/config';
|
|
||||||
import { CheckCircleFilled, CloseCircleFilled } from '@ant-design/icons';
|
|
||||||
import { useIntl } from '@umijs/max';
|
|
||||||
import { Space } from 'antd';
|
|
||||||
|
|
||||||
const PasswordValidate: React.FC<{ value: string }> = ({ value = '' }) => {
|
|
||||||
const intl = useIntl();
|
|
||||||
|
|
||||||
const renderIcon = ({ valid, text }: { valid: boolean; text: string }) => {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{valid ? (
|
|
||||||
<CheckCircleFilled style={{ color: 'green' }} />
|
|
||||||
) : (
|
|
||||||
<CloseCircleFilled style={{ color: 'red' }} />
|
|
||||||
)}
|
|
||||||
<span
|
|
||||||
className="m-l-5"
|
|
||||||
style={{ color: 'var(--ant-color-text-description)' }}
|
|
||||||
>
|
|
||||||
{text}
|
|
||||||
</span>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<Space direction="vertical" style={{ paddingTop: '10px' }}>
|
|
||||||
<span>
|
|
||||||
{renderIcon({
|
|
||||||
valid: uppercaseReg.test(value),
|
|
||||||
text: intl.formatMessage({ id: 'users.password.uppcase' })
|
|
||||||
})}
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
{renderIcon({
|
|
||||||
valid: lowercaseReg.test(value),
|
|
||||||
text: intl.formatMessage({ id: 'users.password.lowercase' })
|
|
||||||
})}
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span>
|
|
||||||
{renderIcon({
|
|
||||||
valid: digitReg.test(value),
|
|
||||||
text: intl.formatMessage({ id: 'users.password.number' })
|
|
||||||
})}
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
{renderIcon({
|
|
||||||
valid: value.length >= 6 && value.length <= 12,
|
|
||||||
text: intl.formatMessage({ id: 'users.password.length' })
|
|
||||||
})}
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
{renderIcon({
|
|
||||||
valid: specialCharacterReg.test(value),
|
|
||||||
text: intl.formatMessage({ id: 'users.password.special' })
|
|
||||||
})}
|
|
||||||
</span>
|
|
||||||
</Space>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default PasswordValidate;
|
|
||||||
@@ -2,7 +2,13 @@ import useCoolColors from '@/hooks/use-cool-colors';
|
|||||||
import { Chart } from '@gpustack/core-ui';
|
import { Chart } from '@gpustack/core-ui';
|
||||||
import { formatLargeNumber } from '@gpustack/core-ui/utils';
|
import { formatLargeNumber } from '@gpustack/core-ui/utils';
|
||||||
import { Empty, Spin, theme } from 'antd';
|
import { Empty, Spin, theme } from 'antd';
|
||||||
import React, { useMemo, useRef } from 'react';
|
import React, {
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useRef,
|
||||||
|
useState
|
||||||
|
} from 'react';
|
||||||
|
|
||||||
export interface PieChartItem {
|
export interface PieChartItem {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -37,6 +43,26 @@ const PieChart: React.FC<PieChartProps> = ({
|
|||||||
const chartRef = useRef<{ chart: any } | null>(null);
|
const chartRef = useRef<{ chart: any } | null>(null);
|
||||||
const generateCoolColors = useCoolColors();
|
const generateCoolColors = useCoolColors();
|
||||||
|
|
||||||
|
// ECharts reads the DOM width at init time; with a "100%" width it can pick
|
||||||
|
// up a stale/tiny value while layout is still resolving, briefly rendering
|
||||||
|
// the donut undersized before its internal (throttled) ResizeObserver
|
||||||
|
// corrects it — a visible flash. We measure the container via a callback ref
|
||||||
|
// (fires during commit, before paint) and feed ECharts an explicit pixel
|
||||||
|
// width so the first render is already correct. The ResizeObserver keeps it
|
||||||
|
// in sync afterwards.
|
||||||
|
const resizeObserverRef = useRef<ResizeObserver | null>(null);
|
||||||
|
const [measuredWidth, setMeasuredWidth] = useState(0);
|
||||||
|
const measureRef = useCallback((node: HTMLDivElement | null) => {
|
||||||
|
resizeObserverRef.current?.disconnect();
|
||||||
|
if (!node) return;
|
||||||
|
setMeasuredWidth(node.clientWidth);
|
||||||
|
resizeObserverRef.current = new ResizeObserver(() => {
|
||||||
|
setMeasuredWidth(node.clientWidth);
|
||||||
|
});
|
||||||
|
resizeObserverRef.current.observe(node);
|
||||||
|
}, []);
|
||||||
|
useEffect(() => () => resizeObserverRef.current?.disconnect(), []);
|
||||||
|
|
||||||
const colors = useMemo(() => {
|
const colors = useMemo(() => {
|
||||||
const generatedColors = generateCoolColors(data.length + colorOffset);
|
const generatedColors = generateCoolColors(data.length + colorOffset);
|
||||||
return generatedColors.slice(colorOffset);
|
return generatedColors.slice(colorOffset);
|
||||||
@@ -142,12 +168,12 @@ const PieChart: React.FC<PieChartProps> = ({
|
|||||||
const displayTotal = total ?? data.reduce((sum, item) => sum + item.value, 0);
|
const displayTotal = total ?? data.reduce((sum, item) => sum + item.value, 0);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ width, height, position: 'relative' }}>
|
<div ref={measureRef} style={{ width, height, position: 'relative' }}>
|
||||||
<Chart
|
<Chart
|
||||||
ref={chartRef as any}
|
ref={chartRef as any}
|
||||||
options={options as any}
|
options={options as any}
|
||||||
height={height}
|
height={height}
|
||||||
width={width}
|
width={measuredWidth || width}
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
|
|||||||
@@ -1,60 +0,0 @@
|
|||||||
import { AutoTooltip } from '@gpustack/core-ui';
|
|
||||||
import React from 'react';
|
|
||||||
import pillButtonCss from './styles.less';
|
|
||||||
type Option = {
|
|
||||||
label: string;
|
|
||||||
value: string | number | null;
|
|
||||||
icon?: React.ReactNode;
|
|
||||||
};
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
value?: string | number;
|
|
||||||
onChange?: (value: string | number | undefined | null) => void;
|
|
||||||
options: Option[];
|
|
||||||
disabled?: boolean;
|
|
||||||
variant?: 'filled' | 'outlined' | 'solid';
|
|
||||||
};
|
|
||||||
|
|
||||||
const PillButtonGroup: React.FC<Props> = ({
|
|
||||||
value,
|
|
||||||
onChange,
|
|
||||||
options,
|
|
||||||
disabled,
|
|
||||||
variant = 'outlined'
|
|
||||||
}) => {
|
|
||||||
return (
|
|
||||||
<div className={pillButtonCss['wrapper']}>
|
|
||||||
{options.map((item) => {
|
|
||||||
const active = value === item.value;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AutoTooltip
|
|
||||||
ghost
|
|
||||||
title={item.label}
|
|
||||||
key={item.value}
|
|
||||||
maxWidth={'100%'}
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
className={`${pillButtonCss['pill-item']} ${active ? pillButtonCss['active'] : ''}`}
|
|
||||||
key={item.value}
|
|
||||||
color="default"
|
|
||||||
onClick={() => {
|
|
||||||
if (item.value !== value) {
|
|
||||||
onChange?.(item.value);
|
|
||||||
}
|
|
||||||
if (item.value === value) {
|
|
||||||
onChange?.(undefined);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{item.icon}
|
|
||||||
{item.label}
|
|
||||||
</span>
|
|
||||||
</AutoTooltip>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default PillButtonGroup;
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
.pill-item {
|
|
||||||
height: 28px;
|
|
||||||
line-height: 28px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
justify-content: center;
|
|
||||||
border-radius: var(--ant-border-radius);
|
|
||||||
background-color: var(--ant-color-fill-tertiary);
|
|
||||||
color: var(--ant-color-text-secondary);
|
|
||||||
padding: 0 8px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.3s;
|
|
||||||
|
|
||||||
&.active {
|
|
||||||
background-color: var(--ant-color-fill);
|
|
||||||
color: var(--ant-color-text);
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:not(.active):hover {
|
|
||||||
background-color: var(--ant-color-fill-secondary);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.wrapper {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr 1fr 1fr;
|
|
||||||
gap: 8px;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
@@ -1,115 +0,0 @@
|
|||||||
import { SegmentLine } from '@gpustack/core-ui';
|
|
||||||
import { useMemoizedFn } from 'ahooks';
|
|
||||||
import _ from 'lodash';
|
|
||||||
import React, { forwardRef, useImperativeHandle } from 'react';
|
|
||||||
import styled from 'styled-components';
|
|
||||||
import useFieldScroll from './use-field-scroll';
|
|
||||||
|
|
||||||
const SegmentedHeader = styled.div<{ $top?: number }>`
|
|
||||||
position: sticky;
|
|
||||||
top: ${(props) => props.$top || 0}px;
|
|
||||||
z-index: 10;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
border-bottom: 1px solid var(--ant-color-split);
|
|
||||||
background-color: var(--ant-color-bg-elevated);
|
|
||||||
`;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* ScrollSpyTabs component
|
|
||||||
* defaultTarget: The default active target tab
|
|
||||||
* segmentedTop: { // Mostlty, It's always a constants.
|
|
||||||
* top: number; // The top offset for the sticky header
|
|
||||||
* offsetTop: number; // The offset top for the target
|
|
||||||
* }
|
|
||||||
* getScrollElementScrollableHeight: function to get the scrollable height of the scroll element
|
|
||||||
* segmentOptions.field: The target data-field={segmentOptions.field} to scroll to
|
|
||||||
* activeKey: The current active keys for collapsible sections
|
|
||||||
* setActiveKey: The function to set active keys for collapsible sections
|
|
||||||
*/
|
|
||||||
interface ScrollSpyTabsProps {
|
|
||||||
ref?: any;
|
|
||||||
children?: React.ReactNode;
|
|
||||||
defaultTarget?: string;
|
|
||||||
segmentedTop: {
|
|
||||||
top: number;
|
|
||||||
offsetTop: number;
|
|
||||||
};
|
|
||||||
activeKey: string[];
|
|
||||||
setActiveKey: (keys: string[]) => void;
|
|
||||||
getScrollElementScrollableHeight?: () => {
|
|
||||||
scrollHeight: number;
|
|
||||||
scrollTop: number;
|
|
||||||
};
|
|
||||||
segmentOptions: {
|
|
||||||
label: string;
|
|
||||||
value: string;
|
|
||||||
icon?: React.ReactNode;
|
|
||||||
field: string;
|
|
||||||
}[];
|
|
||||||
}
|
|
||||||
|
|
||||||
const ScrollSpyTabs: React.FC<ScrollSpyTabsProps> = forwardRef(
|
|
||||||
(
|
|
||||||
{
|
|
||||||
getScrollElementScrollableHeight,
|
|
||||||
segmentedTop,
|
|
||||||
segmentOptions,
|
|
||||||
defaultTarget,
|
|
||||||
activeKey,
|
|
||||||
setActiveKey,
|
|
||||||
children
|
|
||||||
},
|
|
||||||
ref
|
|
||||||
) => {
|
|
||||||
const [target, setTarget] = React.useState<string>(
|
|
||||||
defaultTarget || segmentOptions[0]?.value || ''
|
|
||||||
);
|
|
||||||
|
|
||||||
const { scrollToSegment, holderHeight } = useFieldScroll({
|
|
||||||
activeKey,
|
|
||||||
setActiveKey,
|
|
||||||
segmentOptions,
|
|
||||||
segmentedTop: segmentedTop,
|
|
||||||
getScrollElementScrollableHeight: getScrollElementScrollableHeight
|
|
||||||
});
|
|
||||||
|
|
||||||
const throttleScrollToSegment = useMemoizedFn(
|
|
||||||
_.throttle(
|
|
||||||
async (val: string) => {
|
|
||||||
setTarget(val);
|
|
||||||
scrollToSegment(val, { offsetTop: segmentedTop.offsetTop });
|
|
||||||
},
|
|
||||||
500,
|
|
||||||
{ trailing: true }
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleTargetChange = async (val: any) => {
|
|
||||||
throttleScrollToSegment(val);
|
|
||||||
};
|
|
||||||
|
|
||||||
useImperativeHandle(ref, () => ({
|
|
||||||
handleTargetChange
|
|
||||||
}));
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
{segmentOptions.length > 0 && (
|
|
||||||
<SegmentedHeader $top={segmentedTop.top}>
|
|
||||||
<SegmentLine
|
|
||||||
theme={'light'}
|
|
||||||
defaultValue={target}
|
|
||||||
value={target}
|
|
||||||
onChange={handleTargetChange}
|
|
||||||
options={segmentOptions}
|
|
||||||
/>
|
|
||||||
</SegmentedHeader>
|
|
||||||
)}
|
|
||||||
{children}
|
|
||||||
<div className="holder" style={{ height: holderHeight }}></div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
export default ScrollSpyTabs;
|
|
||||||
@@ -1,118 +0,0 @@
|
|||||||
import { useMemoizedFn } from 'ahooks';
|
|
||||||
import { useCallback, useRef, useState } from 'react';
|
|
||||||
|
|
||||||
interface ScrollOptions {
|
|
||||||
wait?: number;
|
|
||||||
behavior?: 'smooth' | 'auto';
|
|
||||||
block?: 'start' | 'end' | 'center';
|
|
||||||
offsetTop?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function useScrollAfterExpand({
|
|
||||||
activeKey,
|
|
||||||
setActiveKey,
|
|
||||||
segmentOptions,
|
|
||||||
defaultWait = 300,
|
|
||||||
segmentedTop = { top: 0, offsetTop: 96 },
|
|
||||||
getScrollElementScrollableHeight
|
|
||||||
}: {
|
|
||||||
activeKey: string[];
|
|
||||||
setActiveKey: (keys: string[]) => void;
|
|
||||||
segmentOptions: { value: string; field: string }[];
|
|
||||||
getScrollElementScrollableHeight?: () => {
|
|
||||||
scrollHeight: number;
|
|
||||||
scrollTop: number;
|
|
||||||
};
|
|
||||||
defaultWait?: number;
|
|
||||||
segmentedTop: {
|
|
||||||
top: number; // The top offset for the sticky header
|
|
||||||
offsetTop: number; // The offset top for the target
|
|
||||||
};
|
|
||||||
}) {
|
|
||||||
const [holderHeight, setHolderHeight] = useState<number>(0);
|
|
||||||
const boxHeightRef = useRef<number>(0);
|
|
||||||
|
|
||||||
const scrollToElement = useCallback(
|
|
||||||
(
|
|
||||||
el: HTMLElement,
|
|
||||||
{ behavior = 'smooth', offsetTop = 0 }: ScrollOptions = {}
|
|
||||||
) => {
|
|
||||||
// find the nearest scrollable parent
|
|
||||||
const scrollParent = (() => {
|
|
||||||
let node: HTMLElement | null = el;
|
|
||||||
while (node) {
|
|
||||||
const { overflowY } = getComputedStyle(node);
|
|
||||||
if (overflowY === 'auto' || overflowY === 'scroll') return node;
|
|
||||||
node = node.parentElement;
|
|
||||||
}
|
|
||||||
return document.scrollingElement || document.documentElement;
|
|
||||||
})();
|
|
||||||
|
|
||||||
const parentRect = scrollParent.getBoundingClientRect();
|
|
||||||
const elRect = el.getBoundingClientRect();
|
|
||||||
const top =
|
|
||||||
elRect.top - parentRect.top + scrollParent.scrollTop - offsetTop;
|
|
||||||
|
|
||||||
scrollParent.scrollTo({ top, behavior });
|
|
||||||
},
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* due to the scrollheight changes after expanding the segment and including the holder height.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
const scrollToSegment = useMemoizedFn(
|
|
||||||
async (val: string, options?: ScrollOptions) => {
|
|
||||||
if (!activeKey.includes(val)) {
|
|
||||||
setActiveKey([...activeKey, val]);
|
|
||||||
await new Promise((r) => {
|
|
||||||
setTimeout(r, options?.wait ?? defaultWait);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const current = segmentOptions.find((item) => item.value === val);
|
|
||||||
if (!current?.field) return;
|
|
||||||
|
|
||||||
await new Promise(requestAnimationFrame);
|
|
||||||
|
|
||||||
const el: HTMLElement | null = document.querySelector(
|
|
||||||
`[data-field="${current.field}"]`
|
|
||||||
) as HTMLElement | null;
|
|
||||||
|
|
||||||
const targetRectTop = el?.getBoundingClientRect().top || 0;
|
|
||||||
|
|
||||||
const scroller = getScrollElementScrollableHeight?.() || {
|
|
||||||
scrollHeight: 0,
|
|
||||||
scrollTop: 0
|
|
||||||
};
|
|
||||||
|
|
||||||
// remaining scroll height
|
|
||||||
const remainingScrollHeight = scroller.scrollHeight - scroller.scrollTop;
|
|
||||||
|
|
||||||
// total distance from the top of the scroller to the target element
|
|
||||||
const offsetDistance =
|
|
||||||
targetRectTop - segmentedTop.offsetTop - segmentedTop.top;
|
|
||||||
|
|
||||||
let boxHeight = 0;
|
|
||||||
|
|
||||||
// verify boxHeight is correct, if setting the boxHeight causes the element to be hidden, use the previous boxHeight
|
|
||||||
if (offsetDistance <= 0) {
|
|
||||||
boxHeight = boxHeightRef.current;
|
|
||||||
} else {
|
|
||||||
boxHeight =
|
|
||||||
offsetDistance - remainingScrollHeight + boxHeightRef.current;
|
|
||||||
}
|
|
||||||
|
|
||||||
// update boxHeightRef
|
|
||||||
boxHeightRef.current = boxHeight;
|
|
||||||
|
|
||||||
setHolderHeight(boxHeight);
|
|
||||||
await new Promise(requestAnimationFrame);
|
|
||||||
|
|
||||||
if (el) scrollToElement(el, options);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return { scrollToSegment, holderHeight };
|
|
||||||
}
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
interface FinishFailedOptions {
|
|
||||||
requiredFields: {
|
|
||||||
[tab: string]: {
|
|
||||||
sort: number;
|
|
||||||
fields: string[];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
onTargetChange: (key: string) => void;
|
|
||||||
updateActiveKey: (key: string[]) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const useFinishFailed = (options: FinishFailedOptions) => {
|
|
||||||
const { requiredFields, onTargetChange, updateActiveKey } = options;
|
|
||||||
const handleOnFinishFailed = (errorInfo: any) => {
|
|
||||||
const { errorFields } = errorInfo;
|
|
||||||
|
|
||||||
console.log('Finish failed:', errorInfo);
|
|
||||||
|
|
||||||
if (errorFields && errorFields.length > 0) {
|
|
||||||
const collapseKeys: { sort: number; key: string }[] = [];
|
|
||||||
const names = errorFields.map((item: any) => item.name[0]);
|
|
||||||
Object.entries(requiredFields).forEach(([tab, { fields, sort }]) => {
|
|
||||||
const hasError = fields.some((field: string) => names.includes(field));
|
|
||||||
if (hasError) {
|
|
||||||
collapseKeys.push({ sort, key: tab });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (collapseKeys.length > 0) {
|
|
||||||
const keys = collapseKeys
|
|
||||||
.sort((a, b) => a.sort - b.sort)
|
|
||||||
.map((item) => item.key);
|
|
||||||
|
|
||||||
updateActiveKey(keys);
|
|
||||||
onTargetChange(collapseKeys[0].key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
handleOnFinishFailed
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default useFinishFailed;
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
import { useState } from 'react';
|
|
||||||
|
|
||||||
const useScrollActiveChange = (options: {
|
|
||||||
initalActiveKeys: string[];
|
|
||||||
initialCollapseKeys?: string[];
|
|
||||||
}) => {
|
|
||||||
const [activeKey, setActiveKey] = useState<string[]>(
|
|
||||||
options.initalActiveKeys
|
|
||||||
);
|
|
||||||
const [collapseKeys, setCollapseKeys] = useState<string[]>(
|
|
||||||
options.initialCollapseKeys || options.initalActiveKeys
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleActiveChange = (key: string[]) => {
|
|
||||||
setActiveKey(key);
|
|
||||||
setCollapseKeys(key);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleOnCollapseChange = (keys: string | string[]) => {
|
|
||||||
const keysArray = Array.isArray(keys) ? keys : [keys];
|
|
||||||
setActiveKey(keysArray);
|
|
||||||
setCollapseKeys(keysArray);
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateActiveKey = (keys: string[]) => {
|
|
||||||
setActiveKey((prev: string[]) => [...new Set([...prev, ...keys])]);
|
|
||||||
setCollapseKeys((prev) => [...new Set([...prev, ...keys])]);
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
activeKey,
|
|
||||||
collapseKeys,
|
|
||||||
setCollapseKeys,
|
|
||||||
handleActiveChange,
|
|
||||||
handleOnCollapseChange,
|
|
||||||
updateActiveKey
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default useScrollActiveChange;
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
import { CloseCircleFilled } from '@ant-design/icons';
|
|
||||||
import { Button } from 'antd';
|
|
||||||
import styled from 'styled-components';
|
|
||||||
|
|
||||||
const StyledButton = styled(Button)`
|
|
||||||
background-color: transparent !important;
|
|
||||||
padding: 0;
|
|
||||||
.anticon {
|
|
||||||
color: var(--ant-color-text-quaternary);
|
|
||||||
font-size: 12px !important;
|
|
||||||
transition: color 0.3s ease;
|
|
||||||
}
|
|
||||||
&:hover {
|
|
||||||
.anticon {
|
|
||||||
color: var(--ant-color-text-tertiary);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
interface SmallCloseButtonProps {
|
|
||||||
onClick?: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const SmallCloseButton: React.FC<SmallCloseButtonProps> = ({ onClick }) => {
|
|
||||||
return (
|
|
||||||
<StyledButton
|
|
||||||
icon={<CloseCircleFilled />}
|
|
||||||
shape="circle"
|
|
||||||
type="text"
|
|
||||||
onClick={onClick}
|
|
||||||
size="small"
|
|
||||||
></StyledButton>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default SmallCloseButton;
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
.wrapper {
|
|
||||||
flex-shrink: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
width: 0;
|
|
||||||
transition: all var(--ant-motion-duration-slow) var(--ant-motion-ease-in-out);
|
|
||||||
border-color: var(--ant-color-split);
|
|
||||||
|
|
||||||
&.show {
|
|
||||||
width: 232px;
|
|
||||||
border-right: 1px solid var(--ant-color-split);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.container {
|
|
||||||
padding: 0 16px;
|
|
||||||
|
|
||||||
.title {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
margin-top: 8px;
|
|
||||||
padding-inline: 8px;
|
|
||||||
font-weight: 500;
|
|
||||||
color: var(--ant-color-text-tertiary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-wrapper {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
padding-top: 24px;
|
|
||||||
padding-right: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.buttons {
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
justify-content: flex-end;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,109 +0,0 @@
|
|||||||
import { MoreOutlined } from '@ant-design/icons';
|
|
||||||
import { useIntl } from '@umijs/max';
|
|
||||||
import { Transfer, TransferProps } from 'antd';
|
|
||||||
import styled from 'styled-components';
|
|
||||||
|
|
||||||
type TransferKey = string | number | bigint;
|
|
||||||
|
|
||||||
const TransferWrap = styled.div`
|
|
||||||
.ant-transfer-section {
|
|
||||||
width: 100%;
|
|
||||||
height: 300px;
|
|
||||||
.anticon-more {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
.ant-input-outlined {
|
|
||||||
height: 32px;
|
|
||||||
padding-block: 4px;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.ant-transfer-actions {
|
|
||||||
margin: 0 16px;
|
|
||||||
gap: 12px;
|
|
||||||
.ant-btn-icon-only {
|
|
||||||
width: 32px;
|
|
||||||
height: 32px;
|
|
||||||
border-radius: 50%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.ant-transfer-list-content {
|
|
||||||
&::-webkit-scrollbar {
|
|
||||||
width: var(--scrollbar-size);
|
|
||||||
}
|
|
||||||
|
|
||||||
&::-webkit-scrollbar-thumb {
|
|
||||||
background-color: transparent;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::-webkit-scrollbar-track {
|
|
||||||
background-color: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
&::-webkit-scrollbar-thumb {
|
|
||||||
background-color: var(--color-scrollbar-thumb);
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.ant-transfer-list-content-item {
|
|
||||||
&:hover {
|
|
||||||
background-color: var(--ant-control-item-bg-hover);
|
|
||||||
}
|
|
||||||
&.ant-transfer-list-content-item-checked {
|
|
||||||
background-color: unset;
|
|
||||||
&:hover {
|
|
||||||
background-color: var(--ant-control-item-bg-hover);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.ant-pagination {
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
interface TransferInnerProps extends TransferProps {
|
|
||||||
total?: number;
|
|
||||||
perPage?: number;
|
|
||||||
onPageChange?: (page: number, perPage?: number) => void;
|
|
||||||
dataSource?: Array<{ key: TransferKey; title: string }>;
|
|
||||||
targetKeys?: TransferKey[];
|
|
||||||
}
|
|
||||||
|
|
||||||
const TransferInner: React.FC<TransferInnerProps> = (props) => {
|
|
||||||
const intl = useIntl();
|
|
||||||
|
|
||||||
const renderAllLabels = (info: {
|
|
||||||
selectedCount: number;
|
|
||||||
totalCount: number;
|
|
||||||
}) => {
|
|
||||||
if (info.selectedCount) {
|
|
||||||
return (
|
|
||||||
<span style={{ color: 'var(--ant-color-text-secondary)' }}>
|
|
||||||
{intl.formatMessage(
|
|
||||||
{ id: 'common.select.count' },
|
|
||||||
{ count: info.selectedCount }
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<TransferWrap>
|
|
||||||
<Transfer
|
|
||||||
{...props}
|
|
||||||
selectAllLabels={
|
|
||||||
props.selectAllLabels || [renderAllLabels, renderAllLabels]
|
|
||||||
}
|
|
||||||
selectionsIcon={
|
|
||||||
<MoreOutlined style={{ fontSize: 14, marginBottom: 3 }} />
|
|
||||||
}
|
|
||||||
></Transfer>
|
|
||||||
</TransferWrap>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default TransferInner;
|
|
||||||
@@ -47,7 +47,12 @@ const APIKeyForm: React.FC<{
|
|||||||
|
|
||||||
<PluginExtraFields
|
<PluginExtraFields
|
||||||
name="CreateOrgScopeField"
|
name="CreateOrgScopeField"
|
||||||
context={{ action, allowPersonal: true }}
|
context={{
|
||||||
|
action,
|
||||||
|
allowPersonal: true,
|
||||||
|
allowGlobal: true,
|
||||||
|
globalLabelId: 'scope.global'
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Form.Item<FormData>
|
<Form.Item<FormData>
|
||||||
|
|||||||
@@ -6,10 +6,10 @@ export interface ListItem {
|
|||||||
masked_value?: string;
|
masked_value?: string;
|
||||||
user_id?: number;
|
user_id?: number;
|
||||||
user_name?: string;
|
user_name?: string;
|
||||||
// The owning principal — an Org, or a USER principal when the key
|
// The owning principal — an Org, or a USER principal for a
|
||||||
// was created in someone's Personal Org. Read by the enterprise
|
// personal-scope key, or NULL for an admin "All" mode key (no
|
||||||
// plugin's Organization column in the admin All-org view.
|
// tenant pinning).
|
||||||
owner_principal_id?: number;
|
owner_principal_id?: number | null;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
expires_at: string;
|
expires_at: string;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { PageActionType } from '@/config/types';
|
|||||||
import useSubmitLock from '@/hooks/use-submit-lock';
|
import useSubmitLock from '@/hooks/use-submit-lock';
|
||||||
import {
|
import {
|
||||||
AlertBlockInfo,
|
AlertBlockInfo,
|
||||||
|
ColumnWrapper,
|
||||||
GSDrawer,
|
GSDrawer,
|
||||||
IconFont,
|
IconFont,
|
||||||
ModalFooter,
|
ModalFooter,
|
||||||
@@ -13,7 +14,6 @@ import { Tabs } from 'antd';
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import React, { useEffect, useId, useMemo, useRef, useState } from 'react';
|
import React, { useEffect, useId, useMemo, useRef, useState } from 'react';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import ColumnWrapper from '../../_components/column-wrapper';
|
|
||||||
import {
|
import {
|
||||||
BackendSourceValueMap,
|
BackendSourceValueMap,
|
||||||
builtInBackendFields,
|
builtInBackendFields,
|
||||||
|
|||||||
@@ -237,6 +237,10 @@ const BackendList = () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const handleRefresh = () => {
|
||||||
|
fetchData({ query: { ...queryParams, page: 1 } });
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageBox>
|
<PageBox>
|
||||||
<FilterBar
|
<FilterBar
|
||||||
@@ -251,7 +255,7 @@ const BackendList = () => {
|
|||||||
selectHolder={intl.formatMessage({ id: 'backend.filter.source' })}
|
selectHolder={intl.formatMessage({ id: 'backend.filter.source' })}
|
||||||
buttonText={intl.formatMessage({ id: 'backend.button.add' })}
|
buttonText={intl.formatMessage({ id: 'backend.button.add' })}
|
||||||
handleClickPrimary={handleAddBackend}
|
handleClickPrimary={handleAddBackend}
|
||||||
handleSearch={handleSearch}
|
handleSearch={handleRefresh}
|
||||||
handleSelectChange={handleFilterBySource}
|
handleSelectChange={handleFilterBySource}
|
||||||
handleInputChange={handleNameChange}
|
handleInputChange={handleNameChange}
|
||||||
rowSelection={rowSelection}
|
rowSelection={rowSelection}
|
||||||
|
|||||||
@@ -124,6 +124,23 @@ const Instance: React.FC = () => {
|
|||||||
</Flex>
|
</Flex>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: '2',
|
||||||
|
label: intl.formatMessage({ id: 'models.form.env' }),
|
||||||
|
children: (
|
||||||
|
<Flex gap={8} wrap="wrap">
|
||||||
|
{instanceData?.env
|
||||||
|
? Object.entries(instanceData?.env || {}).map(
|
||||||
|
([key, value], index: number) => (
|
||||||
|
<Tag key={index} style={{ margin: 0 }}>
|
||||||
|
{`${key}=${value}`}
|
||||||
|
</Tag>
|
||||||
|
)
|
||||||
|
)
|
||||||
|
: '-'}
|
||||||
|
</Flex>
|
||||||
|
)
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: '4',
|
key: '4',
|
||||||
label: intl.formatMessage({
|
label: intl.formatMessage({
|
||||||
@@ -159,23 +176,6 @@ const Instance: React.FC = () => {
|
|||||||
)}
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
)
|
)
|
||||||
},
|
|
||||||
{
|
|
||||||
key: '2',
|
|
||||||
label: intl.formatMessage({ id: 'models.form.env' }),
|
|
||||||
children: (
|
|
||||||
<Flex gap={8} wrap="wrap">
|
|
||||||
{instanceData?.env
|
|
||||||
? Object.entries(instanceData?.env || {}).map(
|
|
||||||
([key, value], index: number) => (
|
|
||||||
<Tag key={index} style={{ margin: 0 }}>
|
|
||||||
{`${key}=${value}`}
|
|
||||||
</Tag>
|
|
||||||
)
|
|
||||||
)
|
|
||||||
: '-'}
|
|
||||||
</Flex>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
}, [detailData]);
|
}, [detailData]);
|
||||||
|
|||||||
@@ -17,6 +17,9 @@ const ViewLogsModal: React.FC<ViewModalProps> = (props) => {
|
|||||||
const logsViewerRef = React.useRef<any>(null);
|
const logsViewerRef = React.useRef<any>(null);
|
||||||
const requestRef = React.useRef<any>(null);
|
const requestRef = React.useRef<any>(null);
|
||||||
const contentRef = React.useRef<any>(null);
|
const contentRef = React.useRef<any>(null);
|
||||||
|
const [params, setParams] = React.useState<any>({
|
||||||
|
follow: true
|
||||||
|
});
|
||||||
|
|
||||||
const handleCancel = useCallback(() => {
|
const handleCancel = useCallback(() => {
|
||||||
logsViewerRef.current?.abort();
|
logsViewerRef.current?.abort();
|
||||||
@@ -94,9 +97,7 @@ const ViewLogsModal: React.FC<ViewModalProps> = (props) => {
|
|||||||
tail={undefined}
|
tail={undefined}
|
||||||
enableScorllLoad={true}
|
enableScorllLoad={true}
|
||||||
isDownloading={false}
|
isDownloading={false}
|
||||||
params={{
|
params={params}
|
||||||
follow: true
|
|
||||||
}}
|
|
||||||
></LogsViewer>
|
></LogsViewer>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ const Billing: React.FC = () => {
|
|||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
<div style={{ textAlign: 'center' }}>
|
<div style={{ textAlign: 'center', marginTop: 56 }}>
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
href="https://gpustack.ai/enterprise"
|
href="https://gpustack.ai/enterprise"
|
||||||
|
|||||||
@@ -59,9 +59,16 @@ const ClusterCreate: React.FC<{
|
|||||||
// empty-state CTAs that already know which kind of cluster the user
|
// empty-state CTAs that already know which kind of cluster the user
|
||||||
// is heading for (e.g. GPU Service's "Add a Kubernetes Cluster").
|
// is heading for (e.g. GPU Service's "Add a Kubernetes Cluster").
|
||||||
providerHint?: string;
|
providerHint?: string;
|
||||||
|
presetClusterType?: 'model' | 'gpu';
|
||||||
setCurrentTitle?: (title: string) => void;
|
setCurrentTitle?: (title: string) => void;
|
||||||
onClose?: () => void;
|
onClose?: () => void;
|
||||||
}> = ({ onClose, action, providerHint, setCurrentTitle }) => {
|
}> = ({
|
||||||
|
onClose,
|
||||||
|
action,
|
||||||
|
providerHint,
|
||||||
|
presetClusterType,
|
||||||
|
setCurrentTitle
|
||||||
|
}) => {
|
||||||
const stepList = useStepList();
|
const stepList = useStepList();
|
||||||
const [systemConfigState] = useAtom(systemConfigAtom);
|
const [systemConfigState] = useAtom(systemConfigAtom);
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
@@ -377,6 +384,7 @@ const ClusterCreate: React.FC<{
|
|||||||
)}
|
)}
|
||||||
<StepsContext.Provider
|
<StepsContext.Provider
|
||||||
value={{
|
value={{
|
||||||
|
presetClusterType: presetClusterType,
|
||||||
formValues: formValues,
|
formValues: formValues,
|
||||||
systemConfig: systemConfigState
|
systemConfig: systemConfigState
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ const ClusterDetailModal = () => {
|
|||||||
items={[
|
items={[
|
||||||
{
|
{
|
||||||
key: 'workers',
|
key: 'workers',
|
||||||
label: `Workers`,
|
label: intl.formatMessage({ id: 'resources.nodes' }),
|
||||||
icon: <IconFont type="icon-resources" />,
|
icon: <IconFont type="icon-resources" />,
|
||||||
children: (
|
children: (
|
||||||
<WorkerList clusterId={Number(id)} source="clusterDetail" />
|
<WorkerList clusterId={Number(id)} source="clusterDetail" />
|
||||||
@@ -68,7 +68,7 @@ const ClusterDetailModal = () => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'gpus',
|
key: 'gpus',
|
||||||
label: `GPUs`,
|
label: intl.formatMessage({ id: 'menu.resources.gpus' }),
|
||||||
icon: <IconFont type="icon-gpu1" />,
|
icon: <IconFont type="icon-gpu1" />,
|
||||||
children: <GPUList clusterId={Number(id)} source="clusterDetail" />
|
children: <GPUList clusterId={Number(id)} source="clusterDetail" />
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -6,10 +6,10 @@ import ClusterCreate from './cluster-create';
|
|||||||
interface ClusterModalProps {
|
interface ClusterModalProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
title: string;
|
title: string;
|
||||||
// When set, ClusterCreate preselects this provider and skips the
|
pendingProviderHint?: {
|
||||||
// provider-catalog step. Used by feature pages (e.g. GPU Service)
|
|
||||||
// whose empty state already implies which kind of cluster is needed.
|
|
||||||
providerHint?: string;
|
providerHint?: string;
|
||||||
|
presetClusterType?: 'model' | 'gpu';
|
||||||
|
};
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -17,7 +17,7 @@ const ClusterModal: React.FC<ClusterModalProps> = ({
|
|||||||
open,
|
open,
|
||||||
onClose,
|
onClose,
|
||||||
title,
|
title,
|
||||||
providerHint
|
pendingProviderHint
|
||||||
}) => {
|
}) => {
|
||||||
const [currentTitle, setCurrentTitle] = React.useState<string>(title);
|
const [currentTitle, setCurrentTitle] = React.useState<string>(title);
|
||||||
const handleCancel = () => {
|
const handleCancel = () => {
|
||||||
@@ -46,7 +46,8 @@ const ClusterModal: React.FC<ClusterModalProps> = ({
|
|||||||
<ClusterCreate
|
<ClusterCreate
|
||||||
onClose={handleCancel}
|
onClose={handleCancel}
|
||||||
action={PageAction.CREATE}
|
action={PageAction.CREATE}
|
||||||
providerHint={providerHint}
|
providerHint={pendingProviderHint?.providerHint}
|
||||||
|
presetClusterType={pendingProviderHint?.presetClusterType}
|
||||||
setCurrentTitle={setCurrentTitle}
|
setCurrentTitle={setCurrentTitle}
|
||||||
></ClusterCreate>
|
></ClusterCreate>
|
||||||
</GSDrawer>
|
</GSDrawer>
|
||||||
|
|||||||
@@ -194,7 +194,11 @@ const Clusters: React.FC = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSelect = useMemoizedFn((val: any, row: ListItem) => {
|
const handleSelect = useMemoizedFn((val: any, row: ListItem, item?: any) => {
|
||||||
|
if (item?.onClick) {
|
||||||
|
item.onClick(row);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (val === 'edit') {
|
if (val === 'edit') {
|
||||||
handleEditCluster(row);
|
handleEditCluster(row);
|
||||||
} else if (val === 'delete') {
|
} else if (val === 'delete') {
|
||||||
@@ -321,13 +325,20 @@ const Clusters: React.FC = () => {
|
|||||||
// the session atom is cleared right after we open the modal, but
|
// the session atom is cleared right after we open the modal, but
|
||||||
// ClusterCreate mounts a tick later and needs the value to skip the
|
// ClusterCreate mounts a tick later and needs the value to skip the
|
||||||
// provider-catalog step. Cache it locally and clear on close.
|
// provider-catalog step. Cache it locally and clear on close.
|
||||||
const [pendingProviderHint, setPendingProviderHint] = useState<
|
const [pendingProviderHint, setPendingProviderHint] = useState<{
|
||||||
string | undefined
|
providerHint?: string;
|
||||||
>(undefined);
|
presetClusterType?: 'model' | 'gpu';
|
||||||
|
}>({
|
||||||
|
providerHint: undefined,
|
||||||
|
presetClusterType: undefined
|
||||||
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (clusterSession?.firstAddCluster && dataSource.loadend) {
|
if (clusterSession?.firstAddCluster && dataSource.loadend) {
|
||||||
setPendingProviderHint(clusterSession.providerHint);
|
setPendingProviderHint({
|
||||||
|
providerHint: clusterSession.providerHint,
|
||||||
|
presetClusterType: clusterSession.presetClusterType
|
||||||
|
});
|
||||||
openClusterModal();
|
openClusterModal();
|
||||||
// reset session
|
// reset session
|
||||||
setClusterSession(null);
|
setClusterSession(null);
|
||||||
@@ -335,7 +346,10 @@ const Clusters: React.FC = () => {
|
|||||||
}, [clusterSession, dataSource.loadend]);
|
}, [clusterSession, dataSource.loadend]);
|
||||||
|
|
||||||
const handleClusterModalClose = () => {
|
const handleClusterModalClose = () => {
|
||||||
setPendingProviderHint(undefined);
|
setPendingProviderHint({
|
||||||
|
providerHint: undefined,
|
||||||
|
presetClusterType: undefined
|
||||||
|
});
|
||||||
closeClusterModal();
|
closeClusterModal();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -464,7 +478,7 @@ const Clusters: React.FC = () => {
|
|||||||
id: 'menu.resources.clusterCreate'
|
id: 'menu.resources.clusterCreate'
|
||||||
})}
|
})}
|
||||||
open={clusterModalStatus.open}
|
open={clusterModalStatus.open}
|
||||||
providerHint={pendingProviderHint}
|
pendingProviderHint={pendingProviderHint}
|
||||||
onClose={handleClusterModalClose}
|
onClose={handleClusterModalClose}
|
||||||
></ClusterModal>
|
></ClusterModal>
|
||||||
{AddWorkerModal}
|
{AddWorkerModal}
|
||||||
|
|||||||
@@ -16,9 +16,9 @@ import { useClusterDetail } from '../../services/use-cluster-detail';
|
|||||||
|
|
||||||
const Container = styled.div`
|
const Container = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
height: 168px;
|
height: 146px;
|
||||||
.left {
|
.left {
|
||||||
padding: 16px 24px;
|
padding: 16px 0px;
|
||||||
width: 124px;
|
width: 124px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -56,7 +56,7 @@ const Resources = styled.div`
|
|||||||
gap: 24px;
|
gap: 24px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
line-height: 22px;
|
line-height: 22px;
|
||||||
margin-top: 16px;
|
margin-top: 24px;
|
||||||
.item {
|
.item {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -134,7 +134,6 @@ const ClusterBasic: React.FC<{ clusterId: number }> = ({ clusterId }) => {
|
|||||||
)}
|
)}
|
||||||
</Title>
|
</Title>
|
||||||
}
|
}
|
||||||
layout="vertical"
|
|
||||||
items={items}
|
items={items}
|
||||||
/>
|
/>
|
||||||
<Resources>
|
<Resources>
|
||||||
|
|||||||
@@ -47,10 +47,21 @@ const ClusterSystemLoad: React.FC<{ clusterId: number }> = ({ clusterId }) => {
|
|||||||
}
|
}
|
||||||
}, [clusterId]);
|
}, [clusterId]);
|
||||||
|
|
||||||
|
const generateStrokeColor = (percent: number) => {
|
||||||
|
if (percent <= 50) {
|
||||||
|
return 'var(--ant-color-success)';
|
||||||
|
}
|
||||||
|
if (percent <= 80) {
|
||||||
|
return 'var(--ant-color-warning)';
|
||||||
|
}
|
||||||
|
return 'var(--ant-color-error)';
|
||||||
|
};
|
||||||
|
|
||||||
const renderStepsProgress = (
|
const renderStepsProgress = (
|
||||||
percent: number,
|
percent: number,
|
||||||
tag: { color: string; text: string }
|
tag: { color: string; text: string }
|
||||||
) => {
|
) => {
|
||||||
|
const strokeColor = generateStrokeColor(percent);
|
||||||
return (
|
return (
|
||||||
<Progress
|
<Progress
|
||||||
percent={percent}
|
percent={percent}
|
||||||
@@ -58,6 +69,7 @@ const ClusterSystemLoad: React.FC<{ clusterId: number }> = ({ clusterId }) => {
|
|||||||
size={50}
|
size={50}
|
||||||
strokeWidth={8}
|
strokeWidth={8}
|
||||||
showInfo={true}
|
showInfo={true}
|
||||||
|
strokeColor={strokeColor}
|
||||||
format={() => (
|
format={() => (
|
||||||
<Tag
|
<Tag
|
||||||
color={tag?.color || 'blue'}
|
color={tag?.color || 'blue'}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { Form } from 'antd';
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import React, { useEffect, useId, useMemo } from 'react';
|
import React, { useEffect, useId, useMemo } from 'react';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
import { useStepsContext } from '../config/steps-context';
|
||||||
import { ClusterListItem as ListItem } from '../config/types';
|
import { ClusterListItem as ListItem } from '../config/types';
|
||||||
import ImageCredential from './image-credential';
|
import ImageCredential from './image-credential';
|
||||||
import K8SVolumeMount from './k8s-volume-mount';
|
import K8SVolumeMount from './k8s-volume-mount';
|
||||||
@@ -175,6 +176,7 @@ const RadioDot = styled.span<{ $active: boolean }>`
|
|||||||
export const ClusterTypeSelector: React.FC = () => {
|
export const ClusterTypeSelector: React.FC = () => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const form = Form.useFormInstance();
|
const form = Form.useFormInstance();
|
||||||
|
const { presetClusterType } = useStepsContext();
|
||||||
const labelId = useId();
|
const labelId = useId();
|
||||||
const gpuInstanceOptions = Form.useWatch(GPU_INSTANCE_OPTIONS_PATH, {
|
const gpuInstanceOptions = Form.useWatch(GPU_INSTANCE_OPTIONS_PATH, {
|
||||||
form,
|
form,
|
||||||
@@ -211,6 +213,12 @@ export const ClusterTypeSelector: React.FC = () => {
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (presetClusterType) {
|
||||||
|
handleSelect(presetClusterType);
|
||||||
|
}
|
||||||
|
}, [presetClusterType]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ClusterTypeWrap>
|
<ClusterTypeWrap>
|
||||||
<ClusterTypeLabel id={labelId}>
|
<ClusterTypeLabel id={labelId}>
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
import { createContext, useContext } from 'react';
|
import { createContext, useContext } from 'react';
|
||||||
|
|
||||||
export interface StepsContextProps {
|
export interface StepsContextProps {
|
||||||
|
presetClusterType?: 'model' | 'gpu';
|
||||||
formValues: Record<string, any>;
|
formValues: Record<string, any>;
|
||||||
systemConfig?: Record<string, any>;
|
systemConfig?: Record<string, any>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const StepsContext = createContext<StepsContextProps>({
|
export const StepsContext = createContext<StepsContextProps>({
|
||||||
formValues: {},
|
formValues: {},
|
||||||
systemConfig: {}
|
systemConfig: {},
|
||||||
|
presetClusterType: undefined
|
||||||
});
|
});
|
||||||
|
|
||||||
export const useStepsContext = () => useContext(StepsContext);
|
export const useStepsContext = () => useContext(StepsContext);
|
||||||
|
|||||||
@@ -24,15 +24,18 @@ import {
|
|||||||
ProviderValueMap
|
ProviderValueMap
|
||||||
} from '../config';
|
} from '../config';
|
||||||
import { ClusterListItem } from '../config/types';
|
import { ClusterListItem } from '../config/types';
|
||||||
|
|
||||||
const clusterActionList = [
|
const clusterActionList = [
|
||||||
{
|
{
|
||||||
key: 'edit',
|
key: 'edit',
|
||||||
label: 'common.button.edit',
|
label: 'common.button.edit',
|
||||||
|
order: 0,
|
||||||
icon: icons.EditOutlined
|
icon: icons.EditOutlined
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'resources.metrics.details',
|
label: 'resources.metrics.details',
|
||||||
key: 'metrics',
|
key: 'metrics',
|
||||||
|
order: 10,
|
||||||
icon: (
|
icon: (
|
||||||
<span className="flex-center">
|
<span className="flex-center">
|
||||||
<GrafanaIcon style={{ width: 14, height: 14 }}></GrafanaIcon>
|
<GrafanaIcon style={{ width: 14, height: 14 }}></GrafanaIcon>
|
||||||
@@ -44,6 +47,7 @@ const clusterActionList = [
|
|||||||
label: 'resources.button.create',
|
label: 'resources.button.create',
|
||||||
provider: ProviderValueMap.Docker,
|
provider: ProviderValueMap.Docker,
|
||||||
locale: true,
|
locale: true,
|
||||||
|
order: 20,
|
||||||
icon: icons.DockerOutlined
|
icon: icons.DockerOutlined
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -51,6 +55,7 @@ const clusterActionList = [
|
|||||||
label: 'clusters.button.register',
|
label: 'clusters.button.register',
|
||||||
provider: ProviderValueMap.Kubernetes,
|
provider: ProviderValueMap.Kubernetes,
|
||||||
locale: true,
|
locale: true,
|
||||||
|
order: 30,
|
||||||
icon: icons.KubernetesOutlined
|
icon: icons.KubernetesOutlined
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -58,16 +63,20 @@ const clusterActionList = [
|
|||||||
label: 'clusters.button.addNodePool',
|
label: 'clusters.button.addNodePool',
|
||||||
provider: ProviderValueMap.DigitalOcean,
|
provider: ProviderValueMap.DigitalOcean,
|
||||||
locale: true,
|
locale: true,
|
||||||
|
order: 40,
|
||||||
icon: icons.Catalog1
|
icon: icons.Catalog1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'isDefault',
|
key: 'isDefault',
|
||||||
label: 'clusters.form.setDefault',
|
label: 'clusters.form.setDefault',
|
||||||
|
locale: true,
|
||||||
|
order: 50,
|
||||||
icon: icons.StarOutlined
|
icon: icons.StarOutlined
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'delete',
|
key: 'delete',
|
||||||
label: 'common.button.delete',
|
label: 'common.button.delete',
|
||||||
|
order: 999,
|
||||||
icon: icons.DeleteOutlined,
|
icon: icons.DeleteOutlined,
|
||||||
props: {
|
props: {
|
||||||
danger: true
|
danger: true
|
||||||
@@ -76,7 +85,7 @@ const clusterActionList = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
const useClusterColumns = (
|
const useClusterColumns = (
|
||||||
handleSelect: (val: string, record: ClusterListItem) => void,
|
handleSelect: (val: string, record: ClusterListItem, item?: any) => void,
|
||||||
onCellClick?: (record: ClusterListItem, dataIndex: string) => void
|
onCellClick?: (record: ClusterListItem, dataIndex: string) => void
|
||||||
): SealColumnProps[] => {
|
): SealColumnProps[] => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
@@ -88,11 +97,15 @@ const useClusterColumns = (
|
|||||||
// `clusterDetail.linkableName`. Without a plugin we render the
|
// `clusterDetail.linkableName`. Without a plugin we render the
|
||||||
// name as plain text (matches the pre-restore behaviour); with one
|
// name as plain text (matches the pre-restore behaviour); with one
|
||||||
// we use Typography.Link wired to the parent's `onCellClick`.
|
// we use Typography.Link wired to the parent's `onCellClick`.
|
||||||
const nameLinkable: boolean =
|
|
||||||
!!getGPUStackPlugin()?.clusterDetail?.linkableName;
|
const { linkableName: nameLinkable, useGenerateActions } =
|
||||||
|
getGPUStackPlugin()?.clusterDetail || {};
|
||||||
|
|
||||||
|
const actionList =
|
||||||
|
useGenerateActions?.({ actions: clusterActionList }) || clusterActionList;
|
||||||
|
|
||||||
const setActionsItems = (row: ClusterListItem) => {
|
const setActionsItems = (row: ClusterListItem) => {
|
||||||
return clusterActionList.filter((item) => {
|
return actionList.filter((item: any) => {
|
||||||
if (item.provider) {
|
if (item.provider) {
|
||||||
return item.provider === row.provider;
|
return item.provider === row.provider;
|
||||||
}
|
}
|
||||||
@@ -182,7 +195,7 @@ const useClusterColumns = (
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: intl.formatMessage({ id: 'menu.resources.gpus' }),
|
title: intl.formatMessage({ id: 'dashboard.totalgpus' }),
|
||||||
dataIndex: 'gpus',
|
dataIndex: 'gpus',
|
||||||
span: 2,
|
span: 2,
|
||||||
sorter: tableSorter(3),
|
sorter: tableSorter(3),
|
||||||
@@ -238,7 +251,9 @@ const useClusterColumns = (
|
|||||||
render: (value: string, record: ClusterListItem) => (
|
render: (value: string, record: ClusterListItem) => (
|
||||||
<DropdownButtons
|
<DropdownButtons
|
||||||
items={setActionsItems(record)}
|
items={setActionsItems(record)}
|
||||||
onSelect={(val) => handleSelect(val, record)}
|
onSelect={(val: string, item: any) =>
|
||||||
|
handleSelect(val, record, item)
|
||||||
|
}
|
||||||
></DropdownButtons>
|
></DropdownButtons>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export const useQueryClusterList = (options?: { useStateData?: boolean }) => {
|
|||||||
const { useStateData = true } = options || {};
|
const { useStateData = true } = options || {};
|
||||||
const axiosTokenRef = useRef<CancelTokenSource | null>(null);
|
const axiosTokenRef = useRef<CancelTokenSource | null>(null);
|
||||||
const [dataList, setDataList] = useState<
|
const [dataList, setDataList] = useState<
|
||||||
Array<Partial<ClusterListItem> & { label: string; value: number }>
|
Array<ClusterListItem & { label: string; value: number }>
|
||||||
>([]);
|
>([]);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
|||||||
@@ -225,29 +225,77 @@ export const toUsagePieData = (
|
|||||||
return aggregateUsageByGroup(data, groupBy, metric);
|
return aggregateUsageByGroup(data, groupBy, metric);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const toUsageRankData = (
|
export type UsageTokenMetric = 'input_tokens' | 'output_tokens';
|
||||||
|
|
||||||
|
export interface UsageTokenSeriesDef {
|
||||||
|
name: string;
|
||||||
|
key: UsageTokenMetric;
|
||||||
|
color: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aggregates each group's input/output tokens into a stacked HBarChart shape,
|
||||||
|
// so a single bar shows prompt (input) and completion (output) tokens side by
|
||||||
|
// side. Rows sharing a group (e.g. the same user across dates) are summed, then
|
||||||
|
// ranked by combined tokens and capped at the top 10.
|
||||||
|
export const toUsageTokenBreakdownData = (
|
||||||
data: UsageBreakdownResponse | null | undefined,
|
data: UsageBreakdownResponse | null | undefined,
|
||||||
groupBy: UsageGroupBy,
|
groupBy: UsageGroupBy,
|
||||||
seriesName: string,
|
seriesDefs: UsageTokenSeriesDef[]
|
||||||
color: string
|
|
||||||
) => {
|
) => {
|
||||||
const items = aggregateUsageByGroup(data, groupBy, 'total_tokens');
|
const itemMap = new Map<string, { total: number; values: number[] }>();
|
||||||
const names = items.map((item) => item.name);
|
const items = getUsageResponseItems(data);
|
||||||
|
|
||||||
|
items.forEach((item: BreakdownItem) => {
|
||||||
|
const name = buildUsageLabel(item, groupBy);
|
||||||
|
const entry = itemMap.get(name) || {
|
||||||
|
total: 0,
|
||||||
|
values: seriesDefs.map(() => 0)
|
||||||
|
};
|
||||||
|
|
||||||
|
seriesDefs.forEach((def, index) => {
|
||||||
|
const value = Number((item as any)?.[def.key] ?? 0);
|
||||||
|
entry.values[index] += value;
|
||||||
|
entry.total += value;
|
||||||
|
});
|
||||||
|
|
||||||
|
itemMap.set(name, entry);
|
||||||
|
});
|
||||||
|
|
||||||
|
const ranked = Array.from(itemMap.entries())
|
||||||
|
.filter(([, entry]) => entry.total > 0)
|
||||||
|
.sort((a, b) => b[1].total - a[1].total)
|
||||||
|
.slice(0, 10);
|
||||||
|
|
||||||
|
const names = ranked.map(([name]) => name);
|
||||||
|
|
||||||
|
// Round only the outer ends of the stacked bar; the seam where the input and
|
||||||
|
// output segments meet stays square ([topLeft, topRight, bottomRight, bottomLeft]).
|
||||||
|
const radius = 2;
|
||||||
|
const getBorderRadius = (index: number, count: number) => {
|
||||||
|
if (count <= 1) {
|
||||||
|
return [radius, radius, radius, radius];
|
||||||
|
}
|
||||||
|
if (index === 0) {
|
||||||
|
return [radius, 0, 0, radius];
|
||||||
|
}
|
||||||
|
if (index === count - 1) {
|
||||||
|
return [0, radius, radius, 0];
|
||||||
|
}
|
||||||
|
return [0, 0, 0, 0];
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
names,
|
names,
|
||||||
series: [
|
series: seriesDefs.map((def, index) => ({
|
||||||
{
|
name: def.name,
|
||||||
name: seriesName,
|
color: def.color,
|
||||||
color,
|
data: ranked.map(([name, entry]) => ({
|
||||||
data: items.map((item) => ({
|
name,
|
||||||
name: item.name,
|
value: entry.values[index],
|
||||||
value: item.value,
|
|
||||||
itemStyle: {
|
itemStyle: {
|
||||||
borderRadius: [2, 2, 2, 2]
|
borderRadius: getBorderRadius(index, seriesDefs.length)
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
}
|
}))
|
||||||
]
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,20 +1,17 @@
|
|||||||
import useQueryTimeSeriesData from '@/pages/usage/services/use-query-timeseries-data';
|
import useQueryTimeSeriesData from '@/pages/usage/services/use-query-timeseries-data';
|
||||||
import { useIntl } from '@umijs/max';
|
|
||||||
import { useEffect, useMemo } from 'react';
|
import { useEffect, useMemo } from 'react';
|
||||||
import {
|
import {
|
||||||
baseColorMap,
|
baseColorMap,
|
||||||
DashboardUsageCommonParams,
|
DashboardUsageCommonParams,
|
||||||
toUsageRankData
|
toUsageTokenBreakdownData
|
||||||
} from '../config';
|
} from '../config';
|
||||||
|
|
||||||
export default function useTopTokenUsageByUser(
|
export default function useTopTokenUsageByUser(
|
||||||
commonParams: DashboardUsageCommonParams
|
commonParams: DashboardUsageCommonParams
|
||||||
) {
|
) {
|
||||||
const intl = useIntl();
|
|
||||||
const query = useQueryTimeSeriesData({
|
const query = useQueryTimeSeriesData({
|
||||||
key: 'topTokenUsageByUserData'
|
key: 'topTokenUsageByUserData'
|
||||||
});
|
});
|
||||||
const tokenUsageText = intl.formatMessage({ id: 'dashboard.tokens' });
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
query
|
query
|
||||||
@@ -31,13 +28,19 @@ export default function useTopTokenUsageByUser(
|
|||||||
|
|
||||||
const rankData = useMemo(
|
const rankData = useMemo(
|
||||||
() =>
|
() =>
|
||||||
toUsageRankData(
|
toUsageTokenBreakdownData(query.detailData, 'user', [
|
||||||
query.detailData,
|
{
|
||||||
'user',
|
name: 'Prompt Tokens',
|
||||||
tokenUsageText,
|
key: 'input_tokens',
|
||||||
baseColorMap.base
|
color: baseColorMap.base
|
||||||
),
|
},
|
||||||
[query.detailData, tokenUsageText]
|
{
|
||||||
|
name: 'Completion Tokens',
|
||||||
|
key: 'output_tokens',
|
||||||
|
color: baseColorMap.baseR3
|
||||||
|
}
|
||||||
|
]),
|
||||||
|
[query.detailData]
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { PageAction } from '@/config';
|
import { PageAction } from '@/config';
|
||||||
import { PageActionType } from '@/config/types';
|
import { PageActionType } from '@/config/types';
|
||||||
import useSubmitLock from '@/hooks/use-submit-lock';
|
import useSubmitLock from '@/hooks/use-submit-lock';
|
||||||
import { useQueryClusterList } from '@/pages/cluster-management/services/use-query-cluster-list';
|
|
||||||
import useUserDirectory from '@/pages/gpu-service/hooks/use-user-directory';
|
import useUserDirectory from '@/pages/gpu-service/hooks/use-user-directory';
|
||||||
import Separator from '@/pages/llmodels/components/separator';
|
import Separator from '@/pages/llmodels/components/separator';
|
||||||
import { getGPUStackPlugin } from '@/plugins';
|
import { getGPUStackPlugin } from '@/plugins';
|
||||||
@@ -13,7 +12,7 @@ import {
|
|||||||
ModalFooter
|
ModalFooter
|
||||||
} from '@gpustack/core-ui';
|
} from '@gpustack/core-ui';
|
||||||
import { useIntl, useModel } from '@umijs/max';
|
import { useIntl, useModel } from '@umijs/max';
|
||||||
import { Empty, Input, Typography } from 'antd';
|
import { Input, Typography } from 'antd';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { ListItem as TemplateItem } from '../../templates/config/types';
|
import { ListItem as TemplateItem } from '../../templates/config/types';
|
||||||
@@ -31,6 +30,12 @@ type AddModalProps = {
|
|||||||
open: boolean;
|
open: boolean;
|
||||||
width?: number | string;
|
width?: number | string;
|
||||||
realAction?: string;
|
realAction?: string;
|
||||||
|
clusterList?: Array<{
|
||||||
|
label: string;
|
||||||
|
value: number;
|
||||||
|
id: number;
|
||||||
|
owner_principal_id?: number;
|
||||||
|
}>;
|
||||||
onOk: (values: FormData) => void;
|
onOk: (values: FormData) => void;
|
||||||
data?: ListItem | null;
|
data?: ListItem | null;
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
@@ -78,6 +83,7 @@ const AddModal: React.FC<AddModalProps> = ({
|
|||||||
data,
|
data,
|
||||||
onCancel,
|
onCancel,
|
||||||
width,
|
width,
|
||||||
|
clusterList = [],
|
||||||
realAction
|
realAction
|
||||||
}) => {
|
}) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
@@ -100,16 +106,18 @@ const AddModal: React.FC<AddModalProps> = ({
|
|||||||
const [instanceKeyword, setInstanceKeyword] = useState('');
|
const [instanceKeyword, setInstanceKeyword] = useState('');
|
||||||
const [templateKeyword, setTemplateKeyword] = useState('');
|
const [templateKeyword, setTemplateKeyword] = useState('');
|
||||||
const { loading, guard, run, release } = useSubmitLock();
|
const { loading, guard, run, release } = useSubmitLock();
|
||||||
const initializedRef = useRef(false);
|
const [initialized, setInitialized] = useState(false);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
detailData: instanceTypeList,
|
detailData: instanceTypeList,
|
||||||
loading: instanceTypesLoading,
|
loading: instanceTypesLoading,
|
||||||
fetchData
|
fetchData
|
||||||
} = useQueryInstanceTypes();
|
} = useQueryInstanceTypes();
|
||||||
const { detailData: templatesData, fetchData: fetchTemplates } =
|
const {
|
||||||
useQueryTemplates();
|
detailData: templatesData,
|
||||||
const { clusterList, fetchClusterList } = useQueryClusterList();
|
loading: templateLoading,
|
||||||
|
fetchData: fetchTemplates
|
||||||
|
} = useQueryTemplates();
|
||||||
// Set by the create-scope picker (admin "All" view) via onScopeChange.
|
// Set by the create-scope picker (admin "All" view) via onScopeChange.
|
||||||
// undefined = no picker (org context) → no client-side scoping.
|
// undefined = no picker (org context) → no client-side scoping.
|
||||||
const [scopeOrgId, setScopeOrgId] = useState<number | null | undefined>(
|
const [scopeOrgId, setScopeOrgId] = useState<number | null | undefined>(
|
||||||
@@ -125,18 +133,13 @@ const AddModal: React.FC<AddModalProps> = ({
|
|||||||
// fetched list client-side, so it doesn't rely on the request scope.
|
// fetched list client-side, so it doesn't rely on the request scope.
|
||||||
const filterTypesByOwner = (
|
const filterTypesByOwner = (
|
||||||
types: InstanceTypeItem[],
|
types: InstanceTypeItem[],
|
||||||
clusters: Array<{
|
|
||||||
id?: number;
|
|
||||||
value?: number;
|
|
||||||
owner_principal_id?: number;
|
|
||||||
}>,
|
|
||||||
orgId?: number | null
|
orgId?: number | null
|
||||||
): InstanceTypeItem[] => {
|
): InstanceTypeItem[] => {
|
||||||
if (orgId == null) return types;
|
if (orgId == null) return types;
|
||||||
const owned = new Set(
|
const owned = new Set(
|
||||||
(clusters || [])
|
(clusterList || [])
|
||||||
.filter((c) => c.owner_principal_id === orgId)
|
.filter((c) => c.owner_principal_id === orgId)
|
||||||
.map((c) => c.id ?? c.value)
|
.map((c) => c.id || c.value)
|
||||||
);
|
);
|
||||||
return types
|
return types
|
||||||
.map((it) => ({
|
.map((it) => ({
|
||||||
@@ -157,7 +160,7 @@ const AddModal: React.FC<AddModalProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const ownedInstanceTypes = useMemo(
|
const ownedInstanceTypes = useMemo(
|
||||||
() => filterTypesByOwner(instanceTypeList, clusterList as any, scopeOrgId),
|
() => filterTypesByOwner(instanceTypeList, scopeOrgId),
|
||||||
|
|
||||||
[instanceTypeList, clusterList, scopeOrgId]
|
[instanceTypeList, clusterList, scopeOrgId]
|
||||||
);
|
);
|
||||||
@@ -279,11 +282,6 @@ const AddModal: React.FC<AddModalProps> = ({
|
|||||||
const applyAutoSelection = (
|
const applyAutoSelection = (
|
||||||
instanceTypes: InstanceTypeItem[],
|
instanceTypes: InstanceTypeItem[],
|
||||||
templates: TemplateItem[],
|
templates: TemplateItem[],
|
||||||
clusters?: Array<{
|
|
||||||
id?: number;
|
|
||||||
value?: number;
|
|
||||||
owner_principal_id?: number;
|
|
||||||
}>,
|
|
||||||
orgId?: number | null
|
orgId?: number | null
|
||||||
) => {
|
) => {
|
||||||
// On edit / view, surface the persisted selection in the card list.
|
// On edit / view, surface the persisted selection in the card list.
|
||||||
@@ -303,7 +301,7 @@ const AddModal: React.FC<AddModalProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Scope to clusters the chosen org owns (admin "All" view).
|
// Scope to clusters the chosen org owns (admin "All" view).
|
||||||
const owned = filterTypesByOwner(instanceTypes, clusters || [], orgId);
|
const owned = filterTypesByOwner(instanceTypes, orgId);
|
||||||
|
|
||||||
// On create, auto-select the first available instance type (clears the
|
// On create, auto-select the first available instance type (clears the
|
||||||
// selection when the chosen org has none).
|
// selection when the chosen org has none).
|
||||||
@@ -314,26 +312,22 @@ const AddModal: React.FC<AddModalProps> = ({
|
|||||||
// The query hook cancels any in-flight request on each new call, so when
|
// The query hook cancels any in-flight request on each new call, so when
|
||||||
// this runs twice in quick succession (drawer open, then the scope
|
// this runs twice in quick succession (drawer open, then the scope
|
||||||
// picker settling on its default) the latest scope's result wins.
|
// picker settling on its default) the latest scope's result wins.
|
||||||
const loadCreateResources = (orgId?: number | null) => {
|
const loadCreateResources = async (orgId?: number | null) => {
|
||||||
const session = ++sessionRef.current;
|
const session = ++sessionRef.current;
|
||||||
try {
|
try {
|
||||||
Promise.all([
|
const [instanceResItems, templatesRes] = await Promise.all([
|
||||||
fetchData({ page: -1 }),
|
fetchData({ page: -1 }),
|
||||||
fetchTemplates({ page: -1 }),
|
fetchTemplates({ page: -1 })
|
||||||
fetchClusterList({ page: -1 })
|
]);
|
||||||
]).then(([instanceResItems, templatesRes, clusters]) => {
|
|
||||||
if (sessionRef.current !== session) return;
|
if (sessionRef.current !== session) return;
|
||||||
applyAutoSelection(
|
applyAutoSelection(
|
||||||
instanceResItems || [],
|
instanceResItems || [],
|
||||||
templatesRes?.items || [],
|
templatesRes?.items || [],
|
||||||
(Array.isArray(clusters) ? clusters : (clusters as any)?.items) || [],
|
|
||||||
orgId
|
orgId
|
||||||
);
|
);
|
||||||
initializedRef.current = true;
|
setInitialized(true);
|
||||||
});
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
} finally {
|
setInitialized(true);
|
||||||
initializedRef.current = true;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -352,13 +346,13 @@ const AddModal: React.FC<AddModalProps> = ({
|
|||||||
// owner-scoped auto-selection re-fills them from the new org, or leaves
|
// owner-scoped auto-selection re-fills them from the new org, or leaves
|
||||||
// them empty (blocking submit) when the chosen org has no clusters.
|
// them empty (blocking submit) when the chosen org has no clusters.
|
||||||
clearSelection();
|
clearSelection();
|
||||||
initializedRef.current = false;
|
setInitialized(false);
|
||||||
loadCreateResources(orgId);
|
loadCreateResources(orgId);
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!open) {
|
if (!open) {
|
||||||
initializedRef.current = false;
|
setInitialized(false);
|
||||||
sessionRef.current += 1;
|
sessionRef.current += 1;
|
||||||
setInstanceTypeSelection({
|
setInstanceTypeSelection({
|
||||||
instanceType: undefined,
|
instanceType: undefined,
|
||||||
@@ -619,15 +613,12 @@ const AddModal: React.FC<AddModalProps> = ({
|
|||||||
onChange={(e) => setTemplateKeyword(e.target.value)}
|
onChange={(e) => setTemplateKeyword(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{filteredTemplates.length > 0 && initializedRef.current ? (
|
|
||||||
<TemplateSelector
|
<TemplateSelector
|
||||||
value={templateId}
|
value={templateId}
|
||||||
|
loading={templateLoading || !initialized}
|
||||||
groups={templateGroups}
|
groups={templateGroups}
|
||||||
onChange={handleTemplateChange}
|
onChange={handleTemplateChange}
|
||||||
/>
|
/>
|
||||||
) : (
|
|
||||||
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</ColumnWrapper>
|
</ColumnWrapper>
|
||||||
<Separator></Separator>
|
<Separator></Separator>
|
||||||
|
|||||||
@@ -1,17 +1,10 @@
|
|||||||
import FileSkeleton from '@/pages/llmodels/components/model-source/file-skeleton';
|
import { FileSkeletonRows } from '@/pages/llmodels/components/model-source/file-skeleton';
|
||||||
import { TemplateCard } from '@gpustack/core-ui';
|
import { TemplateCard } from '@gpustack/core-ui';
|
||||||
import { Empty, Spin } from 'antd';
|
import { Empty, Flex, Spin } from 'antd';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import styled from 'styled-components';
|
|
||||||
import { InstanceTypeItem as InstanceTypeItemModel } from '../config/types';
|
import { InstanceTypeItem as InstanceTypeItemModel } from '../config/types';
|
||||||
import InstanceTypeItem from './instance-type-item';
|
import InstanceTypeItem from './instance-type-item';
|
||||||
|
|
||||||
const TypeGrid = styled.div`
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 16px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
interface InstanceTypeListProps {
|
interface InstanceTypeListProps {
|
||||||
value?: string;
|
value?: string;
|
||||||
onChange?: (item: InstanceTypeItemModel) => void;
|
onChange?: (item: InstanceTypeItemModel) => void;
|
||||||
@@ -34,11 +27,11 @@ const InstanceTypeList: React.FC<InstanceTypeListProps> = ({
|
|||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<Spin spinning size="middle">
|
<Spin spinning size="middle">
|
||||||
<TypeGrid style={{ minHeight: 200 }}>
|
<Flex orientation="vertical" gap={16} style={{ minHeight: 200 }}>
|
||||||
{_.times(6, (index: number) => (
|
{_.times(6, (index: number) => (
|
||||||
<FileSkeleton key={index} counts={3} itemHeight={106} />
|
<FileSkeletonRows key={index} counts={2} itemHeight={106} />
|
||||||
))}
|
))}
|
||||||
</TypeGrid>
|
</Flex>
|
||||||
</Spin>
|
</Spin>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -46,7 +39,7 @@ const InstanceTypeList: React.FC<InstanceTypeListProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TypeGrid>
|
<Flex orientation="vertical" gap={16}>
|
||||||
{dataList.map((item) => {
|
{dataList.map((item) => {
|
||||||
const name = item.name;
|
const name = item.name;
|
||||||
return (
|
return (
|
||||||
@@ -64,7 +57,7 @@ const InstanceTypeList: React.FC<InstanceTypeListProps> = ({
|
|||||||
</TemplateCard>
|
</TemplateCard>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</TypeGrid>
|
</Flex>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -70,9 +70,9 @@ export const status: Record<string, StatusType> = {
|
|||||||
[InstanceStatusValueMap.NotReady]: StatusMaps.error,
|
[InstanceStatusValueMap.NotReady]: StatusMaps.error,
|
||||||
[InstanceStatusValueMap.Ready]: StatusMaps.success,
|
[InstanceStatusValueMap.Ready]: StatusMaps.success,
|
||||||
[InstanceStatusValueMap.Starting]: StatusMaps.transitioning,
|
[InstanceStatusValueMap.Starting]: StatusMaps.transitioning,
|
||||||
[InstanceStatusValueMap.Deleting]: StatusMaps.error,
|
[InstanceStatusValueMap.Deleting]: StatusMaps.warning,
|
||||||
[InstanceStatusValueMap.Stopping]: StatusMaps.error,
|
[InstanceStatusValueMap.Stopping]: StatusMaps.transitioning,
|
||||||
[InstanceStatusValueMap.Stopped]: StatusMaps.warning,
|
[InstanceStatusValueMap.Stopped]: StatusMaps.inactive,
|
||||||
[InstanceStatusValueMap.CreateFailed]: StatusMaps.error,
|
[InstanceStatusValueMap.CreateFailed]: StatusMaps.error,
|
||||||
[InstanceStatusValueMap.SSHPublicKeyCreateFailed]: StatusMaps.error,
|
[InstanceStatusValueMap.SSHPublicKeyCreateFailed]: StatusMaps.error,
|
||||||
[InstanceStatusValueMap.PersistentVolumeTypeCreateFailed]: StatusMaps.error,
|
[InstanceStatusValueMap.PersistentVolumeTypeCreateFailed]: StatusMaps.error,
|
||||||
|
|||||||
@@ -111,6 +111,7 @@ export interface ListItem extends FormData {
|
|||||||
updated_at: string;
|
updated_at: string;
|
||||||
deleted_at?: string | null;
|
deleted_at?: string | null;
|
||||||
creator_id?: number | null;
|
creator_id?: number | null;
|
||||||
|
clusterId: number;
|
||||||
status?: InstanceStatus | null;
|
status?: InstanceStatus | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -129,6 +129,7 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
|
|||||||
// exists/changes when a platform admin retargets the form. Watch it
|
// exists/changes when a platform admin retargets the form. Watch it
|
||||||
// so the parent can re-scope offerings (see onScopeChange).
|
// so the parent can re-scope offerings (see onScopeChange).
|
||||||
const scopeOrgId = Form.useWatch('organization_id', form);
|
const scopeOrgId = Form.useWatch('organization_id', form);
|
||||||
|
const ports = Form.useWatch(['spec', 'ports'], form) || [];
|
||||||
const scopeInitRef = useRef(true);
|
const scopeInitRef = useRef(true);
|
||||||
// Keep the latest callback in a ref so the scope-change effect can call it
|
// Keep the latest callback in a ref so the scope-change effect can call it
|
||||||
// without depending on its identity (parent may pass a new fn each render).
|
// without depending on its identity (parent may pass a new fn each render).
|
||||||
@@ -151,17 +152,31 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
|
|||||||
: [TABKeysMap.INSTANCE_TYPE, TABKeysMap.TEMPLATE, TABKeysMap.STORAGE]
|
: [TABKeysMap.INSTANCE_TYPE, TABKeysMap.TEMPLATE, TABKeysMap.STORAGE]
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const hasSSHPort = useMemo(
|
||||||
|
() =>
|
||||||
|
ports.some(
|
||||||
|
(item: any) => item?.protocol === 'TCP' && item?.port === SSH_PORT
|
||||||
|
),
|
||||||
|
[ports]
|
||||||
|
);
|
||||||
|
|
||||||
const isGPUType = useMemo(() => {
|
const isGPUType = useMemo(() => {
|
||||||
const spec = parseJsonSafe(description || '{}', {} as any)?.spec;
|
const spec = parseJsonSafe(description || '{}', {} as any)?.spec;
|
||||||
console.log('derived spec from description', spec);
|
|
||||||
return spec?.acceleratable;
|
return spec?.acceleratable;
|
||||||
}, [description]);
|
}, [description]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (open) {
|
if (open) {
|
||||||
fetchSSHData({ page: 1, perPage: 100 });
|
const initSSHKeys = async () => {
|
||||||
|
// await 200 ms
|
||||||
|
await new Promise((resolve) => {
|
||||||
|
setTimeout(resolve, 200);
|
||||||
|
});
|
||||||
|
fetchSSHData({ page: -1 });
|
||||||
|
};
|
||||||
|
initSSHKeys();
|
||||||
}
|
}
|
||||||
}, [open, action]);
|
}, [open]);
|
||||||
|
|
||||||
// Skip the first run (initial mount value); thereafter notify the
|
// Skip the first run (initial mount value); thereafter notify the
|
||||||
// parent whenever the chosen create scope changes so it can reload
|
// parent whenever the chosen create scope changes so it can reload
|
||||||
@@ -174,16 +189,6 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
|
|||||||
onScopeChangeRef.current?.(scopeOrgId);
|
onScopeChangeRef.current?.(scopeOrgId);
|
||||||
}, [scopeOrgId]);
|
}, [scopeOrgId]);
|
||||||
|
|
||||||
const ports = Form.useWatch(['spec', 'ports'], form) || [];
|
|
||||||
|
|
||||||
const hasSSHPort = useMemo(
|
|
||||||
() =>
|
|
||||||
ports.some(
|
|
||||||
(item: any) => item?.protocol === 'TCP' && item?.port === SSH_PORT
|
|
||||||
),
|
|
||||||
[ports]
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
if (
|
||||||
hasSSHPort &&
|
hasSSHPort &&
|
||||||
@@ -554,7 +559,13 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
|
|||||||
image: '',
|
image: '',
|
||||||
imagePullPolicy: DefaultImagePullPolicy,
|
imagePullPolicy: DefaultImagePullPolicy,
|
||||||
command: [],
|
command: [],
|
||||||
ports: [],
|
ports: [
|
||||||
|
{
|
||||||
|
protocol: 'TCP',
|
||||||
|
port: SSH_PORT,
|
||||||
|
name: 'SSH'
|
||||||
|
}
|
||||||
|
],
|
||||||
env: [],
|
env: [],
|
||||||
volumeMount: '',
|
volumeMount: '',
|
||||||
resources: {
|
resources: {
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { PageAction } from '@/config';
|
import { PageAction } from '@/config';
|
||||||
import FormOverlayView from '@/pages/_components/form-overlay-view';
|
import { ModalFooter, SubDrawer } from '@gpustack/core-ui';
|
||||||
import { ModalFooter } from '@gpustack/core-ui';
|
|
||||||
import { useIntl } from '@umijs/max';
|
import { useIntl } from '@umijs/max';
|
||||||
import { useRef, useState } from 'react';
|
import { useRef, useState } from 'react';
|
||||||
import { FormData as PublicKeyFormData } from '../../public-keys/config/types';
|
import { FormData as PublicKeyFormData } from '../../public-keys/config/types';
|
||||||
@@ -45,7 +44,7 @@ const PublicKeyOverlay: React.FC<PublicKeyOverlayProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormOverlayView
|
<SubDrawer
|
||||||
title={intl.formatMessage({ id: 'gpuservice.publicKey.add' })}
|
title={intl.formatMessage({ id: 'gpuservice.publicKey.add' })}
|
||||||
open={open}
|
open={open}
|
||||||
width={drawerWidth}
|
width={drawerWidth}
|
||||||
@@ -70,7 +69,7 @@ const PublicKeyOverlay: React.FC<PublicKeyOverlayProps> = ({
|
|||||||
open={open}
|
open={open}
|
||||||
onFinish={handleFinish}
|
onFinish={handleFinish}
|
||||||
/>
|
/>
|
||||||
</FormOverlayView>
|
</SubDrawer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import { PageAction } from '@/config';
|
import { PageAction } from '@/config';
|
||||||
import FormOverlayView from '@/pages/_components/form-overlay-view';
|
|
||||||
import { FormContext } from '@/pages/gpu-service/storage/config/form-context';
|
import { FormContext } from '@/pages/gpu-service/storage/config/form-context';
|
||||||
import useQueryStorageClass from '@/pages/gpu-service/storage/services/use-query-storage-class';
|
import useQueryStorageClass from '@/pages/gpu-service/storage/services/use-query-storage-class';
|
||||||
import { ModalFooter } from '@gpustack/core-ui';
|
import { ModalFooter, SubDrawer } from '@gpustack/core-ui';
|
||||||
import { useIntl } from '@umijs/max';
|
import { useIntl } from '@umijs/max';
|
||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import { FormData as StorageFormData } from '../../storage/config/types';
|
import { FormData as StorageFormData } from '../../storage/config/types';
|
||||||
@@ -52,7 +51,7 @@ const StorageOverlay: React.FC<StorageOverlayProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormOverlayView
|
<SubDrawer
|
||||||
title={intl.formatMessage({ id: 'gpuservice.storage.add' })}
|
title={intl.formatMessage({ id: 'gpuservice.storage.add' })}
|
||||||
open={open}
|
open={open}
|
||||||
width={drawerWidth}
|
width={drawerWidth}
|
||||||
@@ -79,7 +78,7 @@ const StorageOverlay: React.FC<StorageOverlayProps> = ({
|
|||||||
onFinish={handleFinish}
|
onFinish={handleFinish}
|
||||||
/>
|
/>
|
||||||
</FormContext.Provider>
|
</FormContext.Provider>
|
||||||
</FormOverlayView>
|
</SubDrawer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -36,7 +36,13 @@ const StorageVolume = ({
|
|||||||
const [overlayOpen, setOverlayOpen] = useState(false);
|
const [overlayOpen, setOverlayOpen] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchStorage({ page: 1, perPage: 100 });
|
const initStorage = async () => {
|
||||||
|
await new Promise((resolve) => {
|
||||||
|
setTimeout(resolve, 200);
|
||||||
|
});
|
||||||
|
fetchStorage({ page: -1 });
|
||||||
|
};
|
||||||
|
initStorage();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const storageOptions = useMemo(
|
const storageOptions = useMemo(
|
||||||
|
|||||||
@@ -1,15 +1,12 @@
|
|||||||
|
import { FileSkeletonRows } from '@/pages/llmodels/components/model-source/file-skeleton';
|
||||||
import { AutoTooltip, IconFont, TemplateCard } from '@gpustack/core-ui';
|
import { AutoTooltip, IconFont, TemplateCard } from '@gpustack/core-ui';
|
||||||
import { useIntl } from '@umijs/max';
|
import { useIntl } from '@umijs/max';
|
||||||
|
import { Empty, Flex, Spin } from 'antd';
|
||||||
|
import _ from 'lodash';
|
||||||
import { Fragment } from 'react';
|
import { Fragment } from 'react';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import { ListItem as TemplateItem } from '../../templates/config/types';
|
import { ListItem as TemplateItem } from '../../templates/config/types';
|
||||||
|
|
||||||
const TemplateGrid = styled.div`
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 16px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const GroupTitle = styled.div`
|
const GroupTitle = styled.div`
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
@@ -49,6 +46,12 @@ const TemplateContent = styled.div`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const TypeGrid = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
`;
|
||||||
|
|
||||||
export interface TemplateGroup {
|
export interface TemplateGroup {
|
||||||
key: string;
|
key: string;
|
||||||
label: React.ReactNode;
|
label: React.ReactNode;
|
||||||
@@ -57,6 +60,7 @@ export interface TemplateGroup {
|
|||||||
|
|
||||||
interface TemplateSelectorProps {
|
interface TemplateSelectorProps {
|
||||||
value?: number;
|
value?: number;
|
||||||
|
loading?: boolean;
|
||||||
onChange?: (value: number, item: TemplateItem) => void;
|
onChange?: (value: number, item: TemplateItem) => void;
|
||||||
groups?: TemplateGroup[];
|
groups?: TemplateGroup[];
|
||||||
}
|
}
|
||||||
@@ -64,6 +68,7 @@ interface TemplateSelectorProps {
|
|||||||
const TemplateSelector: React.FC<TemplateSelectorProps> = ({
|
const TemplateSelector: React.FC<TemplateSelectorProps> = ({
|
||||||
value,
|
value,
|
||||||
onChange,
|
onChange,
|
||||||
|
loading,
|
||||||
groups = []
|
groups = []
|
||||||
}) => {
|
}) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
@@ -73,6 +78,22 @@ const TemplateSelector: React.FC<TemplateSelectorProps> = ({
|
|||||||
onChange?.(item.id, item);
|
onChange?.(item.id, item);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<Spin spinning size="middle">
|
||||||
|
<Flex orientation="vertical" gap={16} style={{ minHeight: 200 }}>
|
||||||
|
{_.times(6, (index: number) => (
|
||||||
|
<FileSkeletonRows key={index} counts={2} itemHeight={106} />
|
||||||
|
))}
|
||||||
|
</Flex>
|
||||||
|
</Spin>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!groups.length) {
|
||||||
|
return <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />;
|
||||||
|
}
|
||||||
|
|
||||||
const renderItem = (item: TemplateItem) => (
|
const renderItem = (item: TemplateItem) => (
|
||||||
<TemplateCard
|
<TemplateCard
|
||||||
key={item.id}
|
key={item.id}
|
||||||
@@ -120,14 +141,14 @@ const TemplateSelector: React.FC<TemplateSelectorProps> = ({
|
|||||||
const showGroupTitles = groups.length > 1;
|
const showGroupTitles = groups.length > 1;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TemplateGrid>
|
<Flex orientation="vertical" gap={16}>
|
||||||
{groups.map((group) => (
|
{groups.map((group) => (
|
||||||
<Fragment key={group.key}>
|
<Fragment key={group.key}>
|
||||||
{showGroupTitles && <GroupTitle>{group.label}</GroupTitle>}
|
{showGroupTitles && <GroupTitle>{group.label}</GroupTitle>}
|
||||||
{group.items.map(renderItem)}
|
{group.items.map(renderItem)}
|
||||||
</Fragment>
|
</Fragment>
|
||||||
))}
|
))}
|
||||||
</TemplateGrid>
|
</Flex>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ const GPUService: React.FC = () => {
|
|||||||
setClusterSession({
|
setClusterSession({
|
||||||
firstAddWorker: false,
|
firstAddWorker: false,
|
||||||
firstAddCluster: true,
|
firstAddCluster: true,
|
||||||
|
presetClusterType: 'gpu',
|
||||||
providerHint: ProviderValueMap.Kubernetes
|
providerHint: ProviderValueMap.Kubernetes
|
||||||
});
|
});
|
||||||
navigate('/resources/clusters/list');
|
navigate('/resources/clusters/list');
|
||||||
@@ -111,7 +112,7 @@ const GPUService: React.FC = () => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchClusterList({ page: -1 });
|
fetchClusterList({ page: -1 });
|
||||||
(async () => {
|
const fetchPVCapacities = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await queryGPUServiceStorage({ page: -1 } as any);
|
const res = await queryGPUServiceStorage({ page: -1 } as any);
|
||||||
const map: Record<string, string> = {};
|
const map: Record<string, string> = {};
|
||||||
@@ -124,7 +125,8 @@ const GPUService: React.FC = () => {
|
|||||||
} catch {
|
} catch {
|
||||||
// best-effort; the popover falls back to the PV name
|
// best-effort; the popover falls back to the PV name
|
||||||
}
|
}
|
||||||
})();
|
};
|
||||||
|
fetchPVCapacities();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const hasK8sCluster = useMemo(
|
const hasK8sCluster = useMemo(
|
||||||
@@ -387,6 +389,7 @@ const GPUService: React.FC = () => {
|
|||||||
data={openInstanceModalStatus.currentData}
|
data={openInstanceModalStatus.currentData}
|
||||||
width={openInstanceModalStatus.width}
|
width={openInstanceModalStatus.width}
|
||||||
realAction={openInstanceModalStatus.realAction}
|
realAction={openInstanceModalStatus.realAction}
|
||||||
|
clusterList={clusterList}
|
||||||
onCancel={closeInstanceModal}
|
onCancel={closeInstanceModal}
|
||||||
onOk={handleModalOk}
|
onOk={handleModalOk}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -85,6 +85,9 @@ const formatResources = (
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Spec popover categories, in render order.
|
||||||
|
type SpecCategory = 'gpu' | 'cpu' | 'ram' | 'disk';
|
||||||
|
|
||||||
export const renderInstanceType = (
|
export const renderInstanceType = (
|
||||||
record: ListItem,
|
record: ListItem,
|
||||||
options: {
|
options: {
|
||||||
@@ -92,22 +95,31 @@ export const renderInstanceType = (
|
|||||||
// name → capacity (e.g. "20Gi") for referenced persistent volumes, so the
|
// name → capacity (e.g. "20Gi") for referenced persistent volumes, so the
|
||||||
// Disk → Persistent row can show the size instead of just the PV name.
|
// Disk → Persistent row can show the size instead of just the PV name.
|
||||||
pvCapacityByName?: Record<string, string>;
|
pvCapacityByName?: Record<string, string>;
|
||||||
|
// Limit the spec popover to these categories (default: all). The Instance
|
||||||
|
// Types breakdown only wants CPU + RAM, for example.
|
||||||
|
categories?: SpecCategory[];
|
||||||
|
// Override the primary label (default: derived "<product> x <count>" /
|
||||||
|
// "CPU Only"). The Instance Types breakdown keeps its plain product name.
|
||||||
|
title?: string;
|
||||||
}
|
}
|
||||||
) => {
|
) => {
|
||||||
const { intl, pvCapacityByName } = options;
|
const { intl, pvCapacityByName, categories } = options;
|
||||||
const description =
|
const description =
|
||||||
parseJsonSafe<any>(record?.description || '{}', {}).spec || {};
|
parseJsonSafe<any>(record?.description || '{}', {}).spec || {};
|
||||||
const resources = formatResources({ spec: description }, record);
|
const resources = formatResources({ spec: description }, record);
|
||||||
const accelerator = record.spec?.resources?.accelerator;
|
const accelerator = record.spec?.resources?.accelerator;
|
||||||
const title = description.acceleratable
|
const title =
|
||||||
|
options.title ??
|
||||||
|
(description.acceleratable
|
||||||
? `${description.product} x ${accelerator}`
|
? `${description.product} x ${accelerator}`
|
||||||
: 'CPU Only';
|
: 'CPU Only');
|
||||||
|
|
||||||
const volume = (record.spec as any)?.volume;
|
const volume = (record.spec as any)?.volume;
|
||||||
// Spec popover grouped by category (GPU / CPU / Memory / Disk), mirroring
|
// Spec popover grouped by category (GPU / CPU / Memory / Disk), mirroring
|
||||||
// the Deployments instance info icon: dark tooltip, per-category icon,
|
// the Deployments instance info icon: dark tooltip, per-category icon,
|
||||||
// instance name as the title. Rows with no value are dropped.
|
// instance name as the title. Rows with no value are dropped.
|
||||||
type Section = {
|
type Section = {
|
||||||
|
key: SpecCategory;
|
||||||
icon: string;
|
icon: string;
|
||||||
name: string;
|
name: string;
|
||||||
rows: [string | null, string | undefined][];
|
rows: [string | null, string | undefined][];
|
||||||
@@ -115,6 +127,7 @@ export const renderInstanceType = (
|
|||||||
const sections: Section[] = [];
|
const sections: Section[] = [];
|
||||||
if (description.acceleratable) {
|
if (description.acceleratable) {
|
||||||
sections.push({
|
sections.push({
|
||||||
|
key: 'gpu',
|
||||||
icon: 'icon-gpu',
|
icon: 'icon-gpu',
|
||||||
name: 'GPU',
|
name: 'GPU',
|
||||||
rows: [
|
rows: [
|
||||||
@@ -134,16 +147,19 @@ export const renderInstanceType = (
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
sections.push({
|
sections.push({
|
||||||
|
key: 'cpu',
|
||||||
icon: 'icon-cpu',
|
icon: 'icon-cpu',
|
||||||
name: 'CPU',
|
name: 'CPU',
|
||||||
rows: [[null, resources.cpu]]
|
rows: [[null, resources.cpu]]
|
||||||
});
|
});
|
||||||
sections.push({
|
sections.push({
|
||||||
|
key: 'ram',
|
||||||
icon: 'icon-ram-02',
|
icon: 'icon-ram-02',
|
||||||
name: intl.formatMessage({ id: 'gpuservice.instance.ram' }),
|
name: intl.formatMessage({ id: 'gpuservice.instance.ram' }),
|
||||||
rows: [[null, resources.ram]]
|
rows: [[null, resources.ram]]
|
||||||
});
|
});
|
||||||
sections.push({
|
sections.push({
|
||||||
|
key: 'disk',
|
||||||
icon: 'icon-hard-disk',
|
icon: 'icon-hard-disk',
|
||||||
name: intl.formatMessage({ id: 'gpuservice.instance.disk' }),
|
name: intl.formatMessage({ id: 'gpuservice.instance.disk' }),
|
||||||
rows: [
|
rows: [
|
||||||
@@ -170,8 +186,16 @@ export const renderInstanceType = (
|
|||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const visibleSections = categories
|
||||||
|
? sections.filter((s) => categories.includes(s.key))
|
||||||
|
: sections;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<InstanceTypeCell title={title} name={record.name} sections={sections} />
|
<InstanceTypeCell
|
||||||
|
title={title}
|
||||||
|
name={record.name}
|
||||||
|
sections={visibleSections}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import { PageActionType } from '@/config/types';
|
import { PageActionType } from '@/config/types';
|
||||||
import useSubmitLock from '@/hooks/use-submit-lock';
|
import useSubmitLock from '@/hooks/use-submit-lock';
|
||||||
import { ModalFooter } from '@gpustack/core-ui';
|
import { FormDrawer, ModalFooter } from '@gpustack/core-ui';
|
||||||
import { useRef } from 'react';
|
import { useRef } from 'react';
|
||||||
import FormDrawer from '../../../_components/form-drawer';
|
|
||||||
import { FormData, ListItem } from '../config/types';
|
import { FormData, ListItem } from '../config/types';
|
||||||
import GPUServicePublicKeyForm from '../forms';
|
import GPUServicePublicKeyForm from '../forms';
|
||||||
|
|
||||||
|
|||||||
@@ -87,10 +87,25 @@ const GPUServicePublicKeys: React.FC = () => {
|
|||||||
if (val === 'edit') {
|
if (val === 'edit') {
|
||||||
handleEdit(row);
|
handleEdit(row);
|
||||||
} else if (val === 'delete') {
|
} else if (val === 'delete') {
|
||||||
handleDelete({ ...row, name: row.name as string });
|
handleDelete(
|
||||||
|
{ ...row, name: row.name as string },
|
||||||
|
{
|
||||||
|
tips: intl.formatMessage({
|
||||||
|
id: 'gpuservice.publicKey.delete.tips'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const handleDeleteByBatch = () => {
|
||||||
|
handleDeleteBatch({
|
||||||
|
tips: intl.formatMessage({
|
||||||
|
id: 'gpuservice.publicKey.delete.tips'
|
||||||
|
})
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const renderEmpty = (type?: string) => {
|
const renderEmpty = (type?: string) => {
|
||||||
if (type !== 'Table') return;
|
if (type !== 'Table') return;
|
||||||
return (
|
return (
|
||||||
@@ -130,7 +145,7 @@ const GPUServicePublicKeys: React.FC = () => {
|
|||||||
})}
|
})}
|
||||||
buttonText={intl.formatMessage({ id: 'gpuservice.publicKey.add' })}
|
buttonText={intl.formatMessage({ id: 'gpuservice.publicKey.add' })}
|
||||||
handleSearch={handleSearch}
|
handleSearch={handleSearch}
|
||||||
handleDeleteByBatch={handleDeleteBatch}
|
handleDeleteByBatch={handleDeleteByBatch}
|
||||||
handleClickPrimary={handleAdd}
|
handleClickPrimary={handleAdd}
|
||||||
handleInputChange={handleNameChange}
|
handleInputChange={handleNameChange}
|
||||||
rowSelection={rowSelection}
|
rowSelection={rowSelection}
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import { PageActionType } from '@/config/types';
|
import { PageActionType } from '@/config/types';
|
||||||
import useSubmitLock from '@/hooks/use-submit-lock';
|
import useSubmitLock from '@/hooks/use-submit-lock';
|
||||||
import { ModalFooter } from '@gpustack/core-ui';
|
import { FormDrawer, ModalFooter } from '@gpustack/core-ui';
|
||||||
import { useRef } from 'react';
|
import { useRef } from 'react';
|
||||||
import FormDrawer from '../../../_components/form-drawer';
|
|
||||||
import { FormData, ListItem } from '../config/types';
|
import { FormData, ListItem } from '../config/types';
|
||||||
import GPUServiceStorageTypeForm from '../forms';
|
import GPUServiceStorageTypeForm from '../forms';
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { FormData } from '../config/types';
|
|||||||
const NFSForm = ({ action }: { action: string }) => {
|
const NFSForm = ({ action }: { action: string }) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const { getRuleMessage } = useAppUtils();
|
const { getRuleMessage } = useAppUtils();
|
||||||
|
const disabled = action === PageAction.EDIT;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -24,6 +25,7 @@ const NFSForm = ({ action }: { action: string }) => {
|
|||||||
>
|
>
|
||||||
<CInput.Input
|
<CInput.Input
|
||||||
required
|
required
|
||||||
|
disabled={disabled}
|
||||||
description={intl.formatMessage({
|
description={intl.formatMessage({
|
||||||
id: 'gpuservice.storageType.nfs.server.tips'
|
id: 'gpuservice.storageType.nfs.server.tips'
|
||||||
})}
|
})}
|
||||||
@@ -43,6 +45,7 @@ const NFSForm = ({ action }: { action: string }) => {
|
|||||||
>
|
>
|
||||||
<CInput.Input
|
<CInput.Input
|
||||||
required
|
required
|
||||||
|
disabled={disabled}
|
||||||
description={intl.formatMessage({
|
description={intl.formatMessage({
|
||||||
id: 'gpuservice.storageType.nfs.share.tips'
|
id: 'gpuservice.storageType.nfs.share.tips'
|
||||||
})}
|
})}
|
||||||
@@ -51,6 +54,7 @@ const NFSForm = ({ action }: { action: string }) => {
|
|||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item<FormData> name={['spec', 'nfs', 'subDirectory']}>
|
<Form.Item<FormData> name={['spec', 'nfs', 'subDirectory']}>
|
||||||
<CInput.Input
|
<CInput.Input
|
||||||
|
disabled={disabled}
|
||||||
label={intl.formatMessage({
|
label={intl.formatMessage({
|
||||||
id: 'gpuservice.storageType.nfs.subDirectory'
|
id: 'gpuservice.storageType.nfs.subDirectory'
|
||||||
})}
|
})}
|
||||||
@@ -58,6 +62,7 @@ const NFSForm = ({ action }: { action: string }) => {
|
|||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item<FormData> name={['spec', 'nfs', 'mountPermissions']}>
|
<Form.Item<FormData> name={['spec', 'nfs', 'mountPermissions']}>
|
||||||
<CInput.Input
|
<CInput.Input
|
||||||
|
disabled={disabled}
|
||||||
placeholder="0755,0777,..."
|
placeholder="0755,0777,..."
|
||||||
description={intl.formatMessage({
|
description={intl.formatMessage({
|
||||||
id: 'gpuservice.storageType.nfs.mountPermissions.tips'
|
id: 'gpuservice.storageType.nfs.mountPermissions.tips'
|
||||||
@@ -69,7 +74,7 @@ const NFSForm = ({ action }: { action: string }) => {
|
|||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item<FormData> name={['spec', 'nfs', 'mountOptions']}>
|
<Form.Item<FormData> name={['spec', 'nfs', 'mountOptions']}>
|
||||||
<ListInput
|
<ListInput
|
||||||
disabled={action === PageAction.EDIT}
|
disabled={disabled}
|
||||||
label={intl.formatMessage({
|
label={intl.formatMessage({
|
||||||
id: 'gpuservice.storageType.mountOptions'
|
id: 'gpuservice.storageType.mountOptions'
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ const S3Form = ({ action }: { action: string }) => {
|
|||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const form = Form.useFormInstance<FormData>();
|
const form = Form.useFormInstance<FormData>();
|
||||||
const { getRuleMessage } = useAppUtils();
|
const { getRuleMessage } = useAppUtils();
|
||||||
|
const disabled = action === PageAction.EDIT;
|
||||||
|
|
||||||
const handleEndpointBlur = (e: any) => {
|
const handleEndpointBlur = (e: any) => {
|
||||||
const value: string = e.target.value;
|
const value: string = e.target.value;
|
||||||
@@ -51,6 +52,7 @@ const S3Form = ({ action }: { action: string }) => {
|
|||||||
>
|
>
|
||||||
<CInput.Input
|
<CInput.Input
|
||||||
required
|
required
|
||||||
|
disabled={disabled}
|
||||||
label={intl.formatMessage({
|
label={intl.formatMessage({
|
||||||
id: 'gpuservice.storageType.s3.endpoint'
|
id: 'gpuservice.storageType.s3.endpoint'
|
||||||
})}
|
})}
|
||||||
@@ -65,6 +67,7 @@ const S3Form = ({ action }: { action: string }) => {
|
|||||||
<div style={{ flex: 1 }}>
|
<div style={{ flex: 1 }}>
|
||||||
<Form.Item<FormData> name={['spec', 's3', 'region']}>
|
<Form.Item<FormData> name={['spec', 's3', 'region']}>
|
||||||
<CInput.Input
|
<CInput.Input
|
||||||
|
disabled={disabled}
|
||||||
label={intl.formatMessage({
|
label={intl.formatMessage({
|
||||||
id: 'gpuservice.storageType.s3.region'
|
id: 'gpuservice.storageType.s3.region'
|
||||||
})}
|
})}
|
||||||
@@ -86,6 +89,7 @@ const S3Form = ({ action }: { action: string }) => {
|
|||||||
>
|
>
|
||||||
<CInput.Input
|
<CInput.Input
|
||||||
required
|
required
|
||||||
|
disabled={disabled}
|
||||||
description={
|
description={
|
||||||
<Flex orientation="vertical" gap={4} align="start">
|
<Flex orientation="vertical" gap={4} align="start">
|
||||||
<span>
|
<span>
|
||||||
@@ -111,6 +115,7 @@ const S3Form = ({ action }: { action: string }) => {
|
|||||||
</Flex>
|
</Flex>
|
||||||
<Form.Item<FormData> name={['spec', 's3', 'accessKey']}>
|
<Form.Item<FormData> name={['spec', 's3', 'accessKey']}>
|
||||||
<CInput.Input
|
<CInput.Input
|
||||||
|
disabled={disabled}
|
||||||
label={intl.formatMessage({
|
label={intl.formatMessage({
|
||||||
id: 'gpuservice.storageType.s3.accessKey'
|
id: 'gpuservice.storageType.s3.accessKey'
|
||||||
})}
|
})}
|
||||||
@@ -118,6 +123,7 @@ const S3Form = ({ action }: { action: string }) => {
|
|||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item<FormData> name={['spec', 's3', 'secretKey']}>
|
<Form.Item<FormData> name={['spec', 's3', 'secretKey']}>
|
||||||
<CInput.Password
|
<CInput.Password
|
||||||
|
disabled={disabled}
|
||||||
label={intl.formatMessage({
|
label={intl.formatMessage({
|
||||||
id: 'gpuservice.storageType.s3.secretKey'
|
id: 'gpuservice.storageType.s3.secretKey'
|
||||||
})}
|
})}
|
||||||
@@ -129,6 +135,7 @@ const S3Form = ({ action }: { action: string }) => {
|
|||||||
style={{ marginBottom: 12 }}
|
style={{ marginBottom: 12 }}
|
||||||
>
|
>
|
||||||
<CheckboxField
|
<CheckboxField
|
||||||
|
disabled={disabled}
|
||||||
description={intl.formatMessage({
|
description={intl.formatMessage({
|
||||||
id: 'gpuservice.storageType.s3.insecure.tips'
|
id: 'gpuservice.storageType.s3.insecure.tips'
|
||||||
})}
|
})}
|
||||||
@@ -139,7 +146,7 @@ const S3Form = ({ action }: { action: string }) => {
|
|||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item<FormData> name={['spec', 's3', 'mountOptions']}>
|
<Form.Item<FormData> name={['spec', 's3', 'mountOptions']}>
|
||||||
<ListInput
|
<ListInput
|
||||||
disabled={action === PageAction.EDIT}
|
disabled={disabled}
|
||||||
label={intl.formatMessage({
|
label={intl.formatMessage({
|
||||||
id: 'gpuservice.storageType.mountOptions'
|
id: 'gpuservice.storageType.mountOptions'
|
||||||
})}
|
})}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user