Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3a1565690d | ||
|
|
1bb9fd73db | ||
|
|
39024b1d46 | ||
|
|
4ecaa0a326 | ||
|
|
e1af27e11c | ||
|
|
f4ebd8d7d1 | ||
|
|
bd58a911d5 | ||
|
|
5321973ebe | ||
|
|
0a1f875590 | ||
|
|
ea2c9870a7 | ||
|
|
23f0e4398a | ||
|
|
b342174fc2 | ||
|
|
d07d3b2c99 | ||
|
|
9d0a5d01b0 | ||
|
|
1e97d29fca | ||
|
|
350f398cde | ||
|
|
8a15172316 | ||
|
|
185e3475e7 | ||
|
|
64720ccbfa | ||
|
|
db17192e55 | ||
|
|
a1dd1ec861 | ||
|
|
a516e6ce72 | ||
|
|
2e375c6603 | ||
|
|
7d94c77c15 | ||
|
|
457d2f2f72 | ||
|
|
5e7d83e5dd | ||
|
|
b1cffe047c | ||
|
|
6509f2a4ff | ||
|
|
f719c11606 | ||
|
|
b8d7873b77 | ||
|
|
a9248a98a5 | ||
|
|
8f85e9a082 | ||
|
|
1a6d1654b2 | ||
|
|
abe705c034 | ||
|
|
8b34f68824 | ||
|
|
e37828a2cd | ||
|
|
4709661f93 | ||
|
|
1d7543f19c | ||
|
|
1cf146ca32 | ||
|
|
b05c510776 | ||
|
|
e79d5f8962 | ||
|
|
5503fdc21e | ||
|
|
36a1038d12 | ||
|
|
ee42a9d0ed | ||
|
|
28976dea4b | ||
|
|
22e5cc3aa5 | ||
|
|
3052c0b23e | ||
|
|
1c9cabf7e7 | ||
|
|
03842d1741 | ||
|
|
84304eadb7 | ||
|
|
ab3739219e | ||
|
|
7cf4932f3a | ||
|
|
032f1eefef | ||
|
|
e35be94f7c | ||
|
|
e76e2ad85a | ||
|
|
cb19cd2476 | ||
|
|
c532732ce9 | ||
|
|
2bc10dbb2e | ||
|
|
b634a03ef9 | ||
|
|
ac64bb98ad | ||
|
|
aa49539210 | ||
|
|
1f5678f781 | ||
|
|
7adc089e94 | ||
|
|
6e1e9c07ee | ||
|
|
1d55382f3b | ||
|
|
e68bb0bc4e | ||
|
|
c8e14dade5 | ||
|
|
e4c1a3f5c2 | ||
|
|
cdd6ef9cb3 | ||
|
|
737d43fd16 | ||
|
|
9f5163531c | ||
|
|
df5cf0f4e5 | ||
|
|
e160b64697 | ||
|
|
7d36c6ac4a | ||
|
|
0820edf55f | ||
|
|
12e324893f | ||
|
|
4017c4dd33 | ||
|
|
17b65d80e1 | ||
|
|
47cb3321ab | ||
|
|
547b9f617b | ||
|
|
9e7bd78693 | ||
|
|
c53d60b628 | ||
|
|
9566e0cdb2 | ||
|
|
ed3e779d22 | ||
|
|
3e2133753d | ||
|
|
736a853192 | ||
|
|
08ef2ffa9a | ||
|
|
6cf512cccd | ||
|
|
6f56e614f3 | ||
|
|
8c06862705 | ||
|
|
48159171c2 | ||
|
|
d5d0749b72 | ||
|
|
dc4dd81e11 | ||
|
|
c736e2186d | ||
|
|
7a76bda484 | ||
|
|
778ea36a9d | ||
|
|
04e1e1e566 | ||
|
|
01f66085ab | ||
|
|
ddf723525a | ||
|
|
7b18a92388 | ||
|
|
b985dcee63 | ||
|
|
77e92432ed | ||
|
|
a7b33a928b | ||
|
|
1218c6d096 | ||
|
|
6337431ed4 | ||
|
|
2ba5118bc2 | ||
|
|
12649c7ccf | ||
|
|
5d96932870 | ||
|
|
e6cfa6cf77 | ||
|
|
55d16c5681 | ||
|
|
ab75685955 | ||
|
|
bd0bf0bf63 | ||
|
|
fd050b33d9 | ||
|
|
698090b17a | ||
|
|
6f96260993 | ||
|
|
98a6b19298 | ||
|
|
ded270ff25 | ||
|
|
c8860207e8 | ||
|
|
8d045ffd8b | ||
|
|
fd7437e74a | ||
|
|
807ba866d3 | ||
|
|
5e2808d7c4 | ||
|
|
a26a3ff8f9 | ||
|
|
a2e9f69b96 | ||
|
|
ac800a303a | ||
|
|
dd542a9f29 | ||
|
|
26ce456bde | ||
|
|
7ce1faaf6c | ||
|
|
878bdaa5a7 | ||
|
|
68a7e3f66a | ||
|
|
feccc81c5f | ||
|
|
620045d7a3 | ||
|
|
6dc28d61b0 | ||
|
|
754cc29793 | ||
|
|
ed525509ea | ||
|
|
2b6f26e6c8 | ||
|
|
09821aab85 | ||
|
|
636190b8e6 | ||
|
|
e420c22a45 | ||
|
|
993f6edcc4 | ||
|
|
bda8264788 | ||
|
|
ab8e792276 | ||
|
|
0e4be63b6b | ||
|
|
d3ebea9090 | ||
|
|
15a9e23243 | ||
|
|
eab7d9c77f | ||
|
|
4a86b71018 | ||
|
|
6350f7f11a | ||
|
|
ce6388253c | ||
|
|
d8f6064d5e | ||
|
|
9f5d95f565 | ||
|
|
0cabbeb81c | ||
|
|
76618aa3a1 | ||
|
|
b19db0f67d | ||
|
|
76f228bfe7 | ||
|
|
be40692773 | ||
|
|
42ac377c17 | ||
|
|
46f859cc3c | ||
|
|
f2bff0d1fb | ||
|
|
0bd98b9662 | ||
|
|
1ac2c02197 | ||
|
|
907187d53a | ||
|
|
faf6246fe6 | ||
|
|
0c16e2be7a | ||
|
|
3212882895 | ||
|
|
62f34ffcaf | ||
|
|
50601b0aff | ||
|
|
4c5d42cb13 | ||
|
|
85268b8917 | ||
|
|
973d2c4529 | ||
|
|
ab766b8c54 | ||
|
|
4945242c87 | ||
|
|
c4a72eb375 | ||
|
|
b12d11728d | ||
|
|
8f0e3a7576 | ||
|
|
4810ff2dbe | ||
|
|
a221b22be9 | ||
|
|
24b458530d | ||
|
|
caea43590a | ||
|
|
c3d7a0b85e | ||
|
|
5c6f28027d | ||
|
|
dc04529f5b | ||
|
|
2bb04a419d | ||
|
|
8e31b71668 | ||
|
|
f055453889 | ||
|
|
820a6cecdd | ||
|
|
c3a779e4c8 | ||
|
|
00008abf03 | ||
|
|
d9c6f2f780 | ||
|
|
9a4d77e869 | ||
|
|
2407416e33 | ||
|
|
234e42ccfa | ||
|
|
4426b8dc30 | ||
|
|
9541f49f39 | ||
|
|
9f7a99f370 | ||
|
|
12f0bb2f98 | ||
|
|
ac45a72e0e | ||
|
|
9705818b15 | ||
|
|
39374eafda | ||
|
|
110bc3205e | ||
|
|
57fec89f2d | ||
|
|
72e794c281 | ||
|
|
fadbcc3e44 | ||
|
|
9e30e964e6 | ||
|
|
fbb707d014 | ||
|
|
d2d82ee5ee | ||
|
|
9061fa08ba | ||
|
|
f29236518e | ||
|
|
6537de5224 | ||
|
|
0413d470a8 | ||
|
|
c01f4d3ce8 | ||
|
|
dd9d359bb0 | ||
|
|
b444c35643 | ||
|
|
abd0d52fad | ||
|
|
c8771dd3b9 | ||
|
|
6d16f56183 | ||
|
|
95ffe0350e | ||
|
|
e314f25a77 | ||
|
|
65ea65e9ae | ||
|
|
4ce975562e | ||
|
|
07e625f2bc | ||
|
|
d95fd8dc90 | ||
|
|
55028cf3a3 | ||
|
|
16a97772d3 | ||
|
|
d33167d070 | ||
|
|
f62e0ba372 | ||
|
|
1b6466ae78 | ||
|
|
29c5987bf1 | ||
|
|
fcd1d01e83 | ||
|
|
a80b889760 | ||
|
|
f71bf1b074 | ||
|
|
7e048ee537 | ||
|
|
34098e8950 | ||
|
|
faf0d4d605 | ||
|
|
85ade88d55 | ||
|
|
7d0b94330d | ||
|
|
5cff1298fc | ||
|
|
d3a3d4e96e | ||
|
|
f99621ec92 | ||
|
|
e47a9304f4 | ||
|
|
9488fc1251 | ||
|
|
b590aa746b | ||
|
|
ef61134b8b | ||
|
|
a9daa0b586 | ||
|
|
ae0ad25ed6 | ||
|
|
a4c48c1981 | ||
|
|
3497590d2e | ||
|
|
11cf1329c8 | ||
|
|
ba5b41cf3a | ||
|
|
1d70446cc4 | ||
|
|
f15d0ab243 | ||
|
|
34be4800d7 | ||
|
|
e40b0c2e07 | ||
|
|
45e0ad9e93 | ||
|
|
96e6ddfe8a | ||
|
|
6b47bacfac | ||
|
|
099b419e34 | ||
|
|
e343ec0a1e | ||
|
|
e3d6d08916 | ||
|
|
d0bc206c83 | ||
|
|
7287f8c81b | ||
|
|
1878753cde |
@@ -0,0 +1,143 @@
|
|||||||
|
---
|
||||||
|
name: create-crud-page
|
||||||
|
description: Scaffold a CRUD list/table page module in the gpustack-ui monorepo. Use when creating a new page module, building a list/table page, adding a create/edit drawer, or setting up the components/config/forms/hooks/services structure for a feature.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Create a CRUD Table Page
|
||||||
|
|
||||||
|
## Inputs (do this first)
|
||||||
|
|
||||||
|
- The argument passed to this skill is the **module name** (e.g. `/create-crud-page api-keys` → module `api-keys`). If no name was given, ask for it.
|
||||||
|
- **Always ask the user for the API documentation before generating any code**, even if a module name was provided:
|
||||||
|
|
||||||
|
> Where is the API documentation for this module? (OpenAPI/Swagger URL, schema file path, or an interface description)
|
||||||
|
|
||||||
|
- Wait for the answer, then read/fetch it. Derive `config/types.ts` (`FormData`, `ListItem`), the `services` request hooks, and form fields from that schema. Do not guess field names or endpoints — if the doc is missing details, ask.
|
||||||
|
|
||||||
|
- **Also ask which form layout to scaffold:**
|
||||||
|
|
||||||
|
> Should the form use tabs? (1) a plain form without tabs, or (2) a tabbed form
|
||||||
|
|
||||||
|
Choose the form structure in section 3 accordingly. Default to **no tabs** unless the user picks tabs or the schema clearly has many grouped sections.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Before anything: **reuse common `components`, `hooks`, and `utils` from `@gpustack/core-ui` whenever possible.**
|
||||||
|
|
||||||
|
Reference implementation for sections below: `src/pages/model-routes`.
|
||||||
|
|
||||||
|
## Module structure
|
||||||
|
|
||||||
|
Create the module under `src/pages/{module}`:
|
||||||
|
|
||||||
|
```text
|
||||||
|
{module}
|
||||||
|
├── components
|
||||||
|
├── config
|
||||||
|
├── forms
|
||||||
|
├── hooks
|
||||||
|
├── index.tsx
|
||||||
|
└── services
|
||||||
|
```
|
||||||
|
|
||||||
|
## 1. components
|
||||||
|
|
||||||
|
Module-specific components.
|
||||||
|
|
||||||
|
- The create/edit form component is named `add-xxx-modal.tsx` (repo convention — keep the `-modal` suffix even though it is built with `FormDrawer`).
|
||||||
|
- Use `FormDrawer` from `@gpustack/core-ui`.
|
||||||
|
- If a table cell's render logic/structure is complex, extract it into `xxx-cell.tsx`.
|
||||||
|
|
||||||
|
## 2. config
|
||||||
|
|
||||||
|
```text
|
||||||
|
config
|
||||||
|
├── index.ts # static configs & constants
|
||||||
|
└── types.ts # TypeScript types
|
||||||
|
```
|
||||||
|
|
||||||
|
Naming: form types → `FormData`; table list item types → `ListItem`.
|
||||||
|
|
||||||
|
## 3. forms
|
||||||
|
|
||||||
|
Main form component goes in `forms/index.tsx`.
|
||||||
|
|
||||||
|
- **Complex interactions** (Form.Item split across components): create a dedicated Form Context and wrap with `FormContext.Provider`.
|
||||||
|
- **Tab-based forms**: use `ScrollSpyTabs` from `@gpustack/core-ui`, wrapping the `Form` or `FormContext.Provider`. Do not use tabs unless necessary.
|
||||||
|
- **Required-field validation**: use `getRuleMessage` for standard `input`/`select`.
|
||||||
|
- For cascading selectors and async race protection, follow the **form-patterns** skill.
|
||||||
|
|
||||||
|
## 4. hooks
|
||||||
|
|
||||||
|
- Table columns → `use-xxx-columns.tsx`.
|
||||||
|
- Open/close hooks for `add-xxx-modal.tsx` → `use-create-xxx.ts`.
|
||||||
|
|
||||||
|
## 5. index.tsx (list page entry)
|
||||||
|
|
||||||
|
- **Data fetching**: `useTableFetch` from `@gpustack/core-ui`.
|
||||||
|
- **Data display**:
|
||||||
|
- Standard table → Ant Design `Table`. Ref: `src/pages/users/index.tsx`.
|
||||||
|
- Expandable/collapsible rows → `Table` from `@gpustack/core-ui`. Ref: `src/pages/model-routes/index.tsx`.
|
||||||
|
- Card-style lists → use `InfiniteScrollerProvider`. Ref: `src/pages/backends/index.tsx`.
|
||||||
|
|
||||||
|
## 6. services
|
||||||
|
|
||||||
|
`request` is injected via a provider — do **not** create a centralized `apis` directory like in `gpustack-ui`. Define request hooks directly in `services`.
|
||||||
|
|
||||||
|
- Use `useRequest` from `@gpustack/core-ui`, or `useQueryData` (same underlying method).
|
||||||
|
- Ref: `src/pages/gpu-service/storage-types/services/use-create-storage-type.ts`.
|
||||||
|
|
||||||
|
## 7. Empty data
|
||||||
|
|
||||||
|
- Page table lists → `NoResult`.
|
||||||
|
- Simple (non-page) tables → `Empty` with `image={Empty.PRESENTED_IMAGE_SIMPLE}`.
|
||||||
|
|
||||||
|
## Common UI conventions
|
||||||
|
|
||||||
|
- **Drawer/Modal open/close**: use `useBodyScroll` from `@gpustack/core-ui`. Ref: `src/pages/model-routes/hooks/use-create-route.ts`.
|
||||||
|
- **Status display** (success/failed/processing/warning): use `StatusTag`, never `Tag` from `antd` directly. See **Status display** below. Ref: `src/pages/llmodels/components/table-list.tsx`.
|
||||||
|
- **Permission-gated visibility**: use `Access` / `useAccess`. Ref: `src/pages/access/index.tsx`.
|
||||||
|
- **Styles**: avoid `styled-components` for complex/large styling. Prefer `createStyles` for component-scoped dynamic styles, CSS Modules (`xxx.module.less`) for static structured styles.
|
||||||
|
|
||||||
|
## Status display
|
||||||
|
|
||||||
|
Use `StatusTag` from `@gpustack/core-ui` for status display. Do not use `Tag` from `antd` directly.
|
||||||
|
|
||||||
|
1. Define the mapping from business status values to UI status values in the module's `config/index.ts`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { StatusMaps } from '@/config';
|
||||||
|
import { StatusType } from '@/config/types';
|
||||||
|
|
||||||
|
export const XxxStatusValueMap = {
|
||||||
|
Running: 'running',
|
||||||
|
Pending: 'pending',
|
||||||
|
Failed: 'failed'
|
||||||
|
};
|
||||||
|
|
||||||
|
export const XxxStatusLabelMap: Record<string, string> = {
|
||||||
|
[XxxStatusValueMap.Running]: 'Running',
|
||||||
|
[XxxStatusValueMap.Pending]: 'Pending',
|
||||||
|
[XxxStatusValueMap.Failed]: 'Failed'
|
||||||
|
};
|
||||||
|
|
||||||
|
export const status: Record<string, StatusType> = {
|
||||||
|
[XxxStatusValueMap.Running]: StatusMaps.success,
|
||||||
|
[XxxStatusValueMap.Pending]: StatusMaps.transitioning,
|
||||||
|
[XxxStatusValueMap.Failed]: StatusMaps.error
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
2. In table columns, pass only the UI status value and display text required by `StatusTag`:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<StatusTag
|
||||||
|
statusValue={{
|
||||||
|
status: status[value],
|
||||||
|
text: XxxStatusLabelMap[value] || value,
|
||||||
|
message: record.state_message
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
`statusValue.status` must be a value mapped from `StatusMaps` (`success`, `transitioning`, `warning`, `error`, `inactive`). Do not pass business status values such as `running` or `pending` directly.
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
---
|
||||||
|
name: form-patterns
|
||||||
|
description: Patterns for forms with cascading/dependent selections in the gpustack-ui monorepo. Use when building a form where picking one field derives another (pick A → auto-pick B → write form), handling async option loading on modal open, or protecting against stale async results.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Form Patterns
|
||||||
|
|
||||||
|
Theme: keep cascading selections (pick A → derive B → write form) on a single, predictable path.
|
||||||
|
|
||||||
|
## Accessing the form: `form` vs `form.current`
|
||||||
|
|
||||||
|
This is **not** absolute — it depends on the call site:
|
||||||
|
|
||||||
|
- **Inside the form component** (`forms/index.tsx`), or anywhere holding a `Form.useForm()` instance → call it directly: `form.setFieldsValue(...)`.
|
||||||
|
- **In the outer Drawer/Modal wrapper** that opens the form and holds it via `ref={form}` (`const form = useRef(null)`), driven by an `open` prop → go through the ref: `form.current?.setFieldsValue(...)`.
|
||||||
|
|
||||||
|
The reference template below is written for the **Drawer-wrapper scenario** (it reacts to `open` and owns the shared `selection` state), so it uses `form.current?` throughout. If you lift this logic into the form body with a `useForm()` instance, drop the `.current`.
|
||||||
|
|
||||||
|
## 1. No fallback for derived selection
|
||||||
|
|
||||||
|
When "pick A then auto-pick B", match by rule and return `undefined` if no match — let the form field stay empty. Do **not** silently fall back to `list[0]`; a fallback hides data issues and fakes a valid selection.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const findB = (key, list) =>
|
||||||
|
key ? list.find((x) => x.key === key) : undefined;
|
||||||
|
```
|
||||||
|
|
||||||
|
For form fields, clear with `undefined`, not `''`. In Ant Design `undefined` restores the placeholder; `''` is treated as a real value.
|
||||||
|
|
||||||
|
## 2. Async race protection
|
||||||
|
|
||||||
|
For fetches triggered by a lifecycle entry (e.g. modal open), tag each invocation with a session ref. Discard stale results if the session rotated (modal closed and re-opened) before the response arrives.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const sessionRef = useRef(0);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open) {
|
||||||
|
sessionRef.current += 1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const session = ++sessionRef.current;
|
||||||
|
Promise.all([fetchA(), fetchB()]).then(([as, bs]) => {
|
||||||
|
if (sessionRef.current !== session) return;
|
||||||
|
applySelection(as.items[0], findB(as.items[0].key, bs.items));
|
||||||
|
});
|
||||||
|
}, [open]);
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. Reference template
|
||||||
|
|
||||||
|
Two cascading selectors backed by a single shared state, with a single atomic write (state + form together):
|
||||||
|
|
||||||
|
```ts
|
||||||
|
type Selection = { a?: string; b?: number };
|
||||||
|
|
||||||
|
const [selection, setSelection] = useState<Selection>({});
|
||||||
|
const sessionRef = useRef(0);
|
||||||
|
const form = useRef<any>(null); // wrapper holds the form via <Form ref={form} /> — see "Accessing the form" above
|
||||||
|
|
||||||
|
const findB = (key, list) =>
|
||||||
|
key ? list.find((x) => x.key === key) : undefined;
|
||||||
|
|
||||||
|
// Single atomic write: state + form together.
|
||||||
|
const applySelection = (a, b) => {
|
||||||
|
setSelection({ a: a.name, b: b?.id });
|
||||||
|
form.current?.setFieldsValue({
|
||||||
|
field: b?.field,
|
||||||
|
spec: { ...currentSpec, ...b?.spec }
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Trigger 1: modal opened
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open) {
|
||||||
|
sessionRef.current++;
|
||||||
|
setSelection({});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const session = ++sessionRef.current;
|
||||||
|
Promise.all([fetchA(), fetchB()]).then(([as, bs]) => {
|
||||||
|
if (sessionRef.current !== session) return;
|
||||||
|
const first = as.items[0];
|
||||||
|
applySelection(first, findB(first.key, bs.items));
|
||||||
|
});
|
||||||
|
}, [open]);
|
||||||
|
|
||||||
|
// Trigger 2: user picks A
|
||||||
|
const handleAChange = (a) => {
|
||||||
|
applySelection(a, findB(a.key, listB));
|
||||||
|
};
|
||||||
|
|
||||||
|
// Trigger 3: user picks B
|
||||||
|
const handleBChange = (b) => {
|
||||||
|
setSelection((prev) => ({ ...prev, b: b.id }));
|
||||||
|
form.current?.setFieldsValue({ ...b.fields });
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Related
|
||||||
|
|
||||||
|
- Module/file structure for forms lives in the **create-crud-page** skill (section 3).
|
||||||
|
- Required-field validation: use `getRuleMessage`.
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
name: CI
|
name: CI
|
||||||
|
|
||||||
on:
|
on:
|
||||||
|
workflow_dispatch: {}
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- 'main'
|
- 'main'
|
||||||
@@ -117,3 +118,20 @@ jobs:
|
|||||||
remote_path: releases/${{ steps.version.outputs.version }}.tar.gz
|
remote_path: releases/${{ steps.version.outputs.version }}.tar.gz
|
||||||
accelerate: true
|
accelerate: true
|
||||||
clean: false
|
clean: false
|
||||||
|
|
||||||
|
trigger-backend:
|
||||||
|
needs: build-publish
|
||||||
|
if: (github.event_name == 'push' && !startsWith(github.ref, 'refs/tags/')) || github.event_name == 'workflow_dispatch'
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
steps:
|
||||||
|
- name: Dispatch backend build
|
||||||
|
uses: peter-evans/repository-dispatch@v3
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.DISPATCH_PAT }}
|
||||||
|
repository: gpustack/gpustack
|
||||||
|
event-type: ui-built
|
||||||
|
client-payload: |
|
||||||
|
{
|
||||||
|
"ref": "${{ github.ref }}",
|
||||||
|
"sha": "${{ github.sha }}"
|
||||||
|
}
|
||||||
|
|||||||
@@ -13,6 +13,6 @@
|
|||||||
.swc
|
.swc
|
||||||
.DS_Store
|
.DS_Store
|
||||||
.idea
|
.idea
|
||||||
.claude
|
.claude/settings.local.json
|
||||||
/dist.zip
|
/dist.zip
|
||||||
.cache
|
.cache
|
||||||
@@ -3,7 +3,6 @@ node_modules
|
|||||||
.umi-production
|
.umi-production
|
||||||
public/static/*.js
|
public/static/*.js
|
||||||
public/static/*.css
|
public/static/*.css
|
||||||
src/components/icon-font/iconfont/iconfont.js
|
src/components/iconfont/
|
||||||
src/components/icon-font/iconfont/*.css
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -3,5 +3,5 @@ module.exports = {
|
|||||||
rules: {
|
rules: {
|
||||||
'selector-class-pattern': null
|
'selector-class-pattern': null
|
||||||
},
|
},
|
||||||
ignoreFiles: ['public/static/*.css']
|
ignoreFiles: ['public/static/*.css', 'src/components/iconfont/iconfont.css']
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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,140 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
|
## Layout
|
||||||
|
|
||||||
|
Compose layout with Ant components, not hand-written `display: flex`.
|
||||||
|
|
||||||
|
- **1D flex** (row/column with `gap`, `align`, `justify`) → `Flex`. Do not write raw `display: flex` in new code.
|
||||||
|
- **Inline sequence** of a few elements with uniform spacing → `Space`.
|
||||||
|
- **Page/grid columns** → `Row` / `Col`.
|
||||||
|
|
||||||
|
Drive spacing with the theme scale (`Flex`/`Space` `gap`, or `var(--ant-*)` spacing tokens), not scattered `px` literals.
|
||||||
|
|
||||||
|
# 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`.
|
||||||
|
- **`Select` options that need i18n**: set `label` to the message key and add `locale: true` on the option — the field translates it at render. Omit `locale` for options whose label is already final text. Ref `src/pages/benchmark/config/index.ts`.
|
||||||
|
|
||||||
|
# Common components
|
||||||
|
|
||||||
|
Always check `@gpustack/core-ui` first. Frequently reused:
|
||||||
|
|
||||||
|
- **Drawer/Modal open/close**: `useBodyScroll`.
|
||||||
|
- **Form drawer / footer**: `FormDrawer`, `ModalFooter`.
|
||||||
|
- **Delete confirmation**: `DeleteModal`.
|
||||||
|
- **Search + bulk actions bar**: `FilterBar`.
|
||||||
|
- **Form fields**: `BaseSelect`, `Input` (labeled).
|
||||||
|
- **Text overflow**: `AutoTooltip`.
|
||||||
|
- **Icons**: `IconFont`.
|
||||||
|
- **Status display** (success/failed/processing/warning): `StatusTag`.
|
||||||
|
- **Permission-gated visibility**: `Access` / `useAccess`.
|
||||||
|
- **Request hooks**: `useRequest` / `useQueryData` / `useQueryDataList`.
|
||||||
|
- **Table data fetching**: `useTableFetch`.
|
||||||
|
- **Submit guard** (prevent double-submit): `useSubmitLock`.
|
||||||
|
- **Tabbed forms**: `ScrollSpyTabs`.
|
||||||
|
|
||||||
|
# Dynamic add-item form fields
|
||||||
|
|
||||||
|
When building a form, select the add-item component from the **shape of the field's data** (its schema). Match the schema, don't hand-roll a list UI:
|
||||||
|
|
||||||
|
- **Plain object** (key→value map) → `LabelSelector`.
|
||||||
|
- **String array** → `ListInput`. Ref `src/pages/llmodels/forms/backend-parameters-list.tsx`.
|
||||||
|
- **Object array** → `MetadataList` with a custom item renderer per entry. Ref `src/pages/llmodels/forms/model-lora-list.tsx`.
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
## Create form table list
|
|
||||||
|
|
||||||
## Create a form
|
|
||||||
|
|
||||||
## StatusTag
|
|
||||||
|
|
||||||
Use `StatusTag` from `@gpustack/core-ui` for status display. Do not use `Tag` from `antd` directly.
|
|
||||||
|
|
||||||
1. Define the mapping from business status values to UI status values in the module's `config/index.ts`:
|
|
||||||
|
|
||||||
```ts
|
|
||||||
import { StatusMaps } from '@/config';
|
|
||||||
import { StatusType } from '@/config/types';
|
|
||||||
|
|
||||||
export const XxxStatusValueMap = {
|
|
||||||
Running: 'running',
|
|
||||||
Pending: 'pending',
|
|
||||||
Failed: 'failed'
|
|
||||||
};
|
|
||||||
|
|
||||||
export const XxxStatusLabelMap: Record<string, string> = {
|
|
||||||
[XxxStatusValueMap.Running]: 'Running',
|
|
||||||
[XxxStatusValueMap.Pending]: 'Pending',
|
|
||||||
[XxxStatusValueMap.Failed]: 'Failed'
|
|
||||||
};
|
|
||||||
|
|
||||||
export const status: Record<string, StatusType> = {
|
|
||||||
[XxxStatusValueMap.Running]: StatusMaps.success,
|
|
||||||
[XxxStatusValueMap.Pending]: StatusMaps.transitioning,
|
|
||||||
[XxxStatusValueMap.Failed]: StatusMaps.error
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
2. In table columns, pass only the UI status value and display text required by `StatusTag`:
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
<StatusTag
|
|
||||||
statusValue={{
|
|
||||||
status: status[value],
|
|
||||||
text: XxxStatusLabelMap[value] || value,
|
|
||||||
message: record.state_message
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
```
|
|
||||||
|
|
||||||
`statusValue.status` must be a value mapped from `StatusMaps`, such as `success`, `transitioning`, `warning`, `error`, or `inactive`. Do not pass business status values such as `running` or `pending` directly.
|
|
||||||
@@ -31,5 +31,6 @@ export default function createProxyTable(target?: string) {
|
|||||||
},
|
},
|
||||||
{}
|
{}
|
||||||
);
|
);
|
||||||
|
|
||||||
return proxyTable;
|
return proxyTable;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -140,15 +140,6 @@ const baseRoutes = [
|
|||||||
access: 'canSeeOrgAdmin',
|
access: 'canSeeOrgAdmin',
|
||||||
component: './model-routes/index'
|
component: './model-routes/index'
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: 'usage',
|
|
||||||
path: '/models/usage',
|
|
||||||
key: 'usage',
|
|
||||||
icon: 'icon-usage-outlined',
|
|
||||||
selectedIcon: 'icon-usage-filled',
|
|
||||||
defaultIcon: 'icon-usage-outlined',
|
|
||||||
component: './usage/index'
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: 'providers',
|
name: 'providers',
|
||||||
path: '/models/providers',
|
path: '/models/providers',
|
||||||
@@ -179,6 +170,26 @@ const baseRoutes = [
|
|||||||
access: 'canSeeOrgAdmin',
|
access: 'canSeeOrgAdmin',
|
||||||
hideInMenu: true,
|
hideInMenu: true,
|
||||||
component: './benchmark/details'
|
component: './benchmark/details'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'backendsList',
|
||||||
|
path: '/models/backends',
|
||||||
|
key: 'backendsList',
|
||||||
|
icon: 'icon-backend',
|
||||||
|
selectedIcon: 'icon-backend-filled',
|
||||||
|
defaultIcon: 'icon-backend',
|
||||||
|
access: 'canSeeOrgAdmin',
|
||||||
|
component: './backends/index'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'modelfiles',
|
||||||
|
path: '/models/modelfiles',
|
||||||
|
key: 'modelfiles',
|
||||||
|
icon: 'icon-files',
|
||||||
|
selectedIcon: 'icon-files-filled',
|
||||||
|
defaultIcon: 'icon-files',
|
||||||
|
access: 'canSeeOrgAdmin',
|
||||||
|
component: './resources/components/model-files'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -186,6 +197,7 @@ const baseRoutes = [
|
|||||||
name: 'gpuService',
|
name: 'gpuService',
|
||||||
path: '/gpu-service',
|
path: '/gpu-service',
|
||||||
key: 'gpuService',
|
key: 'gpuService',
|
||||||
|
access: 'canSeeGpuService',
|
||||||
routes: [
|
routes: [
|
||||||
{
|
{
|
||||||
path: '/gpu-service',
|
path: '/gpu-service',
|
||||||
@@ -223,7 +235,12 @@ const baseRoutes = [
|
|||||||
path: '/gpu-service/storage-types',
|
path: '/gpu-service/storage-types',
|
||||||
key: 'gpuServiceStorageTypes',
|
key: 'gpuServiceStorageTypes',
|
||||||
icon: 'icon-storage-outlined',
|
icon: 'icon-storage-outlined',
|
||||||
access: 'canSeeAdmin',
|
// Storage types are tenant-scoped on the backend (Org owners
|
||||||
|
// can create/list their own), so the menu shouldn't be
|
||||||
|
// platform-admin-only. ``canSeeOrgAdmin`` keeps the gate at
|
||||||
|
// "admin or current-org owner" — Org members still don't see
|
||||||
|
// it, which matches the read/write model in the route.
|
||||||
|
access: 'canSeeOrgAdmin',
|
||||||
selectedIcon: 'icon-storage-filled',
|
selectedIcon: 'icon-storage-filled',
|
||||||
defaultIcon: 'icon-storage-outlined',
|
defaultIcon: 'icon-storage-outlined',
|
||||||
component: './gpu-service/storage-types'
|
component: './gpu-service/storage-types'
|
||||||
@@ -249,6 +266,16 @@ const baseRoutes = [
|
|||||||
path: '/resources',
|
path: '/resources',
|
||||||
redirect: '/resources/workers'
|
redirect: '/resources/workers'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'clusters',
|
||||||
|
path: '/resources/clusters/list',
|
||||||
|
key: 'clusters',
|
||||||
|
icon: 'icon-cluster2-outline',
|
||||||
|
selectedIcon: 'icon-cluster2-filled',
|
||||||
|
defaultIcon: 'icon-cluster2-outline',
|
||||||
|
component: './cluster-management/clusters',
|
||||||
|
subMenu: ['/resources/clusters/detail', '/resources/clusters/create']
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'workers',
|
name: 'workers',
|
||||||
path: '/resources/workers',
|
path: '/resources/workers',
|
||||||
@@ -268,67 +295,63 @@ const baseRoutes = [
|
|||||||
component: './resources/components/gpus'
|
component: './resources/components/gpus'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'backendsList',
|
name: 'credentials',
|
||||||
path: '/resources/backends',
|
path: '/resources/credentials',
|
||||||
key: 'backendsList',
|
key: 'credentials',
|
||||||
icon: 'icon-backend',
|
icon: 'icon-credential-outline',
|
||||||
selectedIcon: 'icon-backend-filled',
|
selectedIcon: 'icon-credential-filled',
|
||||||
defaultIcon: 'icon-backend',
|
defaultIcon: 'icon-credential-outline',
|
||||||
access: 'canSeeOrgAdmin',
|
component: './cluster-management/credentials'
|
||||||
component: './backends/index'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'modelfiles',
|
|
||||||
path: '/resources/modelfiles',
|
|
||||||
key: 'modelfiles',
|
|
||||||
icon: 'icon-files',
|
|
||||||
selectedIcon: 'icon-files-filled',
|
|
||||||
defaultIcon: 'icon-files',
|
|
||||||
component: './resources/components/model-files'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'clusterManagement',
|
|
||||||
path: '/cluster-management',
|
|
||||||
key: 'clusterManagement',
|
|
||||||
access: 'canSeeOrgAdmin',
|
|
||||||
routes: [
|
|
||||||
{
|
|
||||||
path: '/cluster-management',
|
|
||||||
redirect: '/cluster-management/clusters/list'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'clusters',
|
|
||||||
path: '/cluster-management/clusters/list',
|
|
||||||
key: 'clusters',
|
|
||||||
icon: 'icon-cluster2-outline',
|
|
||||||
selectedIcon: 'icon-cluster2-filled',
|
|
||||||
defaultIcon: 'icon-cluster2-outline',
|
|
||||||
component: './cluster-management/clusters',
|
|
||||||
subMenu: [
|
|
||||||
'/cluster-management/clusters/detail',
|
|
||||||
'/cluster-management/clusters/create'
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'clusterDetail',
|
name: 'clusterDetail',
|
||||||
path: '/cluster-management/clusters/detail',
|
path: '/resources/clusters/detail',
|
||||||
key: 'clusterDetail',
|
key: 'clusterDetail',
|
||||||
icon: 'icon-cluster2-outline',
|
icon: 'icon-cluster2-outline',
|
||||||
selectedIcon: 'icon-cluster2-filled',
|
selectedIcon: 'icon-cluster2-filled',
|
||||||
defaultIcon: 'icon-cluster2-outline',
|
defaultIcon: 'icon-cluster2-outline',
|
||||||
hideInMenu: true,
|
hideInMenu: true,
|
||||||
component: './cluster-management/cluster-detail'
|
component: './cluster-management/cluster-detail'
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'credentials',
|
// Cross-resource consumption (tokens + GPU/CPU instances + storage).
|
||||||
path: '/cluster-management/credentials',
|
// A folder so it matches the other top-level groups; more usage views can
|
||||||
key: 'credentials',
|
// graduate in here later.
|
||||||
icon: 'icon-credential-outline',
|
name: 'billingAndUsage',
|
||||||
selectedIcon: 'icon-credential-filled',
|
path: '/usage',
|
||||||
defaultIcon: 'icon-credential-outline',
|
key: 'usageGroup',
|
||||||
component: './cluster-management/credentials'
|
icon: 'icon-usage-outlined',
|
||||||
|
selectedIcon: 'icon-usage-filled',
|
||||||
|
defaultIcon: 'icon-usage-outlined',
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
path: '/usage',
|
||||||
|
redirect: '/usage/overview'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'usage',
|
||||||
|
path: '/usage/overview',
|
||||||
|
key: 'usage',
|
||||||
|
icon: 'icon-usage-outlined',
|
||||||
|
selectedIcon: 'icon-usage-filled',
|
||||||
|
defaultIcon: 'icon-usage-outlined',
|
||||||
|
component: './usage/index'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'billing',
|
||||||
|
path: '/usage/billing',
|
||||||
|
key: 'billing',
|
||||||
|
icon: 'icon-billing-outlined',
|
||||||
|
selectedIcon: 'icon-billing-filled',
|
||||||
|
defaultIcon: 'icon-billing-outlined',
|
||||||
|
hideInMenu: process.env.ENABLE_ENTERPRISE === 'true',
|
||||||
|
// OSS exposes the menu as a teaser for the enterprise billing
|
||||||
|
// module. The page itself just renders an upsell notice — the real
|
||||||
|
// billing UI lives in the enterprise plugin and shadows this route
|
||||||
|
// via `routes.extensions.ts`.
|
||||||
|
component: './billing'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -341,6 +364,20 @@ const baseRoutes = [
|
|||||||
path: '/access-control',
|
path: '/access-control',
|
||||||
redirect: '/access-control/users'
|
redirect: '/access-control/users'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'organizations',
|
||||||
|
path: '/access-control/organizations',
|
||||||
|
key: 'organizations',
|
||||||
|
icon: 'icon-org-outlined',
|
||||||
|
selectedIcon: 'icon-org-filled',
|
||||||
|
defaultIcon: 'icon-org-outlined',
|
||||||
|
// OSS exposes the menu to platform admins as a teaser for the
|
||||||
|
// enterprise multi-tenancy module. The page itself just renders
|
||||||
|
// an upsell notice — the real CRUD UI lives in the enterprise
|
||||||
|
// plugin and shadows this route via `routes.extensions.ts`.
|
||||||
|
access: 'canSeeAdmin',
|
||||||
|
component: './organizations'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'users',
|
name: 'users',
|
||||||
path: '/access-control/users',
|
path: '/access-control/users',
|
||||||
@@ -374,8 +411,8 @@ const baseRoutes = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'profile',
|
name: 'profile',
|
||||||
path: '/profile',
|
path: '/preferences',
|
||||||
key: 'profile',
|
key: 'preferences',
|
||||||
hideInMenu: true,
|
hideInMenu: true,
|
||||||
component: './profile',
|
component: './profile',
|
||||||
icon: 'User'
|
icon: 'User'
|
||||||
|
|||||||
@@ -1,13 +1,27 @@
|
|||||||
const child_process = require('child_process');
|
import { execSync } from 'child_process';
|
||||||
|
|
||||||
export const getBranchInfo = () => {
|
export const getBranchInfo = () => {
|
||||||
const latestCommit = child_process
|
// git may be absent (source archive, bare container) or this tree may
|
||||||
.execSync('git rev-parse HEAD')
|
// not be a git checkout. Swallow the failure and fall back to the env
|
||||||
|
// overrides below — losing build info shouldn't fail the build.
|
||||||
|
let latestCommit = '';
|
||||||
|
let versionTag = '';
|
||||||
|
try {
|
||||||
|
latestCommit = execSync('git rev-parse HEAD').toString().trim();
|
||||||
|
versionTag = execSync(`git tag --contains ${latestCommit}`)
|
||||||
.toString()
|
.toString()
|
||||||
.trim();
|
.trim();
|
||||||
const versionTag = child_process
|
} catch {
|
||||||
.execSync(`git tag --contains ${latestCommit}`)
|
// Not a git checkout / git unavailable; rely on env overrides.
|
||||||
.toString()
|
}
|
||||||
.trim();
|
// Respect explicit GPUSTACK_UI_* overrides so a wrapping build that
|
||||||
return { version: versionTag || '', commitId: latestCommit.slice(0, 7) };
|
// checks this source tree out as a sub-package can stamp its own
|
||||||
|
// release tag and commit id onto the UI (otherwise the panel reports
|
||||||
|
// the host tree's git HEAD, which the wrapper doesn't control).
|
||||||
|
const overrideVersion = process.env.GPUSTACK_UI_VERSION?.trim();
|
||||||
|
const overrideCommitId = process.env.GPUSTACK_UI_COMMIT_ID?.trim();
|
||||||
|
return {
|
||||||
|
version: overrideVersion || versionTag || '',
|
||||||
|
commitId: overrideCommitId || latestCommit.slice(0, 7)
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -14,7 +14,8 @@ export default defineConfig([
|
|||||||
'dist',
|
'dist',
|
||||||
'src/.umi/',
|
'src/.umi/',
|
||||||
'src/.umi-production/',
|
'src/.umi-production/',
|
||||||
'src/.umi-test/'
|
'src/.umi-test/',
|
||||||
|
'src/components/iconfont/'
|
||||||
]),
|
]),
|
||||||
{
|
{
|
||||||
files: ['**/*.{ts,tsx,js,jsx}'],
|
files: ['**/*.{ts,tsx,js,jsx}'],
|
||||||
|
|||||||
@@ -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.16",
|
"@gpustack/core-ui": "^1.0.35",
|
||||||
"@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",
|
||||||
@@ -39,7 +39,6 @@
|
|||||||
"culori": "^4.0.2",
|
"culori": "^4.0.2",
|
||||||
"dayjs": "^1.11.11",
|
"dayjs": "^1.11.11",
|
||||||
"dompurify": "^3.2.6",
|
"dompurify": "^3.2.6",
|
||||||
"driver.js": "^1.3.1",
|
|
||||||
"echarts": "^5.5.1",
|
"echarts": "^5.5.1",
|
||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
"has-ansi": "^5.0.1",
|
"has-ansi": "^5.0.1",
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ export default (api: IApi) => {
|
|||||||
const info = JSON.parse(process.env.VERSION || '{}');
|
const info = JSON.parse(process.env.VERSION || '{}');
|
||||||
const env = process.env.NODE_ENV;
|
const env = process.env.NODE_ENV;
|
||||||
|
|
||||||
|
$('html').attr('lang', 'en');
|
||||||
|
|
||||||
$('html').attr('data-env', env);
|
$('html').attr('data-env', env);
|
||||||
|
|
||||||
$('html').attr(
|
$('html').attr(
|
||||||
|
|||||||
@@ -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.16
|
specifier: ^1.0.35
|
||||||
version: 1.0.16(czdvzceysqw7iv6pct2ucnb23e)
|
version: 1.0.35(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)
|
||||||
@@ -89,9 +89,6 @@ importers:
|
|||||||
dompurify:
|
dompurify:
|
||||||
specifier: ^3.2.6
|
specifier: ^3.2.6
|
||||||
version: 3.4.2
|
version: 3.4.2
|
||||||
driver.js:
|
|
||||||
specifier: ^1.3.1
|
|
||||||
version: 1.4.0
|
|
||||||
echarts:
|
echarts:
|
||||||
specifier: ^5.5.1
|
specifier: ^5.5.1
|
||||||
version: 5.6.0
|
version: 5.6.0
|
||||||
@@ -109,7 +106,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 +202,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 +230,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 +1481,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.16':
|
'@gpustack/core-ui@1.0.35':
|
||||||
resolution: {integrity: sha512-wFKDv7X0FXRmAmZPt4WKYV0+aGHcx82/3EZSJkrut/49XQd5XdrqKaimGtlFWFCNEWqsouwzs4uqaC7JFqfOkQ==, tarball: https://registry.npmjs.org/@gpustack/core-ui/-/core-ui-1.0.16.tgz}
|
resolution: {integrity: sha512-MaEmCM3FikeKZdUgDSyUtIfXtNMbNOLeFiTynl6DcKEraUOpk6tgQXPDginBY3uXuisG4hzoIQbpHHkzaYiQew==, tarball: https://registry.npmjs.org/@gpustack/core-ui/-/core-ui-1.0.35.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}
|
||||||
@@ -4320,9 +4317,6 @@ packages:
|
|||||||
dot-case@3.0.4:
|
dot-case@3.0.4:
|
||||||
resolution: {integrity: sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==, tarball: https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz}
|
resolution: {integrity: sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==, tarball: https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz}
|
||||||
|
|
||||||
driver.js@1.4.0:
|
|
||||||
resolution: {integrity: sha512-Gm64jm6PmcU+si21sQhBrTAM1JvUrR0QhNmjkprNLxohOBzul9+pNHXgQaT9lW84gwg9GMLB3NZGuGolsz5uew==, tarball: https://registry.npmjs.org/driver.js/-/driver.js-1.4.0.tgz}
|
|
||||||
|
|
||||||
duck@0.1.12:
|
duck@0.1.12:
|
||||||
resolution: {integrity: sha512-wkctla1O6VfP89gQ+J/yDesM0S7B7XLXjKGzXxMDVFg7uEn706niAtyYovKbyq1oT9YwDcly721/iUWoc8MVRg==, tarball: https://registry.npmjs.org/duck/-/duck-0.1.12.tgz}
|
resolution: {integrity: sha512-wkctla1O6VfP89gQ+J/yDesM0S7B7XLXjKGzXxMDVFg7uEn706niAtyYovKbyq1oT9YwDcly721/iUWoc8MVRg==, tarball: https://registry.npmjs.org/duck/-/duck-0.1.12.tgz}
|
||||||
|
|
||||||
@@ -9257,14 +9251,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 +10128,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 +10802,7 @@ snapshots:
|
|||||||
|
|
||||||
'@formatjs/intl-utils@2.3.0': {}
|
'@formatjs/intl-utils@2.3.0': {}
|
||||||
|
|
||||||
'@gpustack/core-ui@1.0.16(czdvzceysqw7iv6pct2ucnb23e)':
|
'@gpustack/core-ui@1.0.35(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)
|
||||||
@@ -11829,7 +11823,7 @@ snapshots:
|
|||||||
|
|
||||||
'@types/history@5.0.0':
|
'@types/history@5.0.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
history: 4.10.1
|
history: 5.3.0
|
||||||
|
|
||||||
'@types/hoist-non-react-statics@3.3.7(@types/react@18.3.28)':
|
'@types/hoist-non-react-statics@3.3.7(@types/react@18.3.28)':
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -11915,7 +11909,7 @@ snapshots:
|
|||||||
'@types/history': 4.7.11
|
'@types/history': 4.7.11
|
||||||
'@types/react': 18.3.29
|
'@types/react': 18.3.29
|
||||||
'@types/react-router': 5.1.20
|
'@types/react-router': 5.1.20
|
||||||
redux: 3.7.2
|
redux: 4.2.1
|
||||||
|
|
||||||
'@types/react-router@5.1.20':
|
'@types/react-router@5.1.20':
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -12281,11 +12275,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 +12547,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 +12636,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 +12651,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 +12681,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 +12696,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 +12726,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 +12741,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 +12771,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 +12781,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 +12864,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 +12886,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 +13012,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 +13033,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 +13549,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 +13595,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 +13609,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 +13621,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: {}
|
||||||
|
|
||||||
@@ -14554,8 +14548,6 @@ snapshots:
|
|||||||
no-case: 3.0.4
|
no-case: 3.0.4
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
|
|
||||||
driver.js@1.4.0: {}
|
|
||||||
|
|
||||||
duck@0.1.12:
|
duck@0.1.12:
|
||||||
dependencies:
|
dependencies:
|
||||||
underscore: 1.13.8
|
underscore: 1.13.8
|
||||||
@@ -16410,9 +16402,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 +19797,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 +20156,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 +20185,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 +20239,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)
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
import { applyAccessExtensions } from './access.extensions';
|
import { applyAccessExtensions } from './access.extensions';
|
||||||
|
|
||||||
export default (initialState: { currentUser?: Global.UserInfo }) => {
|
export default (initialState: {
|
||||||
|
currentUser?: Global.UserInfo;
|
||||||
|
hasKubernetesCluster?: boolean;
|
||||||
|
hasResourceEvents?: boolean;
|
||||||
|
}) => {
|
||||||
const isPlatformAdmin = !!(
|
const isPlatformAdmin = !!(
|
||||||
initialState &&
|
initialState &&
|
||||||
initialState.currentUser &&
|
initialState.currentUser &&
|
||||||
@@ -11,6 +15,16 @@ export default (initialState: { currentUser?: Global.UserInfo }) => {
|
|||||||
initialState.currentUser &&
|
initialState.currentUser &&
|
||||||
!initialState.currentUser.is_admin
|
!initialState.currentUser.is_admin
|
||||||
);
|
);
|
||||||
|
// GPU Service is Kubernetes-only. We only gate visibility down when
|
||||||
|
// the probe in `getInitialState` came back with a definitive answer;
|
||||||
|
// `undefined` (probe failed / not yet ready) collapses to the
|
||||||
|
// role-based default so a transient network blip can't lock anyone
|
||||||
|
// out of the menu.
|
||||||
|
const hasKubernetesCluster = initialState?.hasKubernetesCluster;
|
||||||
|
// Having run GPU/CPU instances or storage (any resource_events) also unlocks
|
||||||
|
// GPU Service / the full Usage page — a user who used it keeps seeing it even
|
||||||
|
// without a current cluster. MaaS-only users (no cluster, no events) don't.
|
||||||
|
const hasResourceEvents = !!initialState?.hasResourceEvents;
|
||||||
|
|
||||||
// Predicate roles, top-down by strictness:
|
// Predicate roles, top-down by strictness:
|
||||||
// * `canSeeAdmin` — strictly platform admin (`users.is_admin`).
|
// * `canSeeAdmin` — strictly platform admin (`users.is_admin`).
|
||||||
@@ -18,6 +32,11 @@ export default (initialState: { currentUser?: Global.UserInfo }) => {
|
|||||||
// * `canSeeOrgAdmin` — admin-style menus that work cross-org
|
// * `canSeeOrgAdmin` — admin-style menus that work cross-org
|
||||||
// (Dashboard, Resources, Models, Cluster Management). Defaults
|
// (Dashboard, Resources, Models, Cluster Management). Defaults
|
||||||
// to platform admin; extensions widen to include org admins.
|
// to platform admin; extensions widen to include org admins.
|
||||||
|
// * `canSeeGpuService` — GPU Service menu. Anyone allowed to
|
||||||
|
// manage clusters (admins, Org owners) sees it; non-admins fall
|
||||||
|
// through to "show only if a Kubernetes cluster is actually
|
||||||
|
// reachable" so Org members without scheduling access don't see
|
||||||
|
// a dead-end menu item.
|
||||||
// * `canManageCurrentOrg` — pages that only make sense inside a
|
// * `canManageCurrentOrg` — pages that only make sense inside a
|
||||||
// specific org context (member / group management). Defaults to
|
// specific org context (member / group management). Defaults to
|
||||||
// `false`; extensions widen when both an org is selected AND
|
// `false`; extensions widen when both an org is selected AND
|
||||||
@@ -27,6 +46,8 @@ export default (initialState: { currentUser?: Global.UserInfo }) => {
|
|||||||
return applyAccessExtensions({
|
return applyAccessExtensions({
|
||||||
canSeeAdmin: isPlatformAdmin,
|
canSeeAdmin: isPlatformAdmin,
|
||||||
canSeeOrgAdmin: isPlatformAdmin,
|
canSeeOrgAdmin: isPlatformAdmin,
|
||||||
|
canSeeGpuService:
|
||||||
|
isPlatformAdmin || hasKubernetesCluster !== false || hasResourceEvents,
|
||||||
canManageCurrentOrg: false,
|
canManageCurrentOrg: false,
|
||||||
canSeeUser,
|
canSeeUser,
|
||||||
canDelete: true,
|
canDelete: true,
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { userSettingsHelperAtom } from '@/atoms/settings';
|
import { userSettingsHelperAtom } from '@/atoms/settings';
|
||||||
import { GPUStackVersionAtom, UpdateCheckAtom } from '@/atoms/user';
|
import { GPUStackVersionAtom, UpdateCheckAtom, userAtom } from '@/atoms/user';
|
||||||
import { setAtomStorage } from '@/atoms/utils';
|
import { setAtomStorage } from '@/atoms/utils';
|
||||||
import { DEFAULT_ENTER_PAGE, GPUSTACK_API_BASE_URL } from '@/config/settings';
|
import { DEFAULT_ENTER_PAGE, GPUSTACK_API_BASE_URL } from '@/config/settings';
|
||||||
import { COLOR_PRIMARY } from '@/config/theme/constants';
|
import { COLOR_PRIMARY } from '@/config/theme/constants';
|
||||||
|
import { getGPUStackPlugin } from '@/plugins';
|
||||||
import { enterprisePluginReady } from '@/plugins/enterprise-ready';
|
import { enterprisePluginReady } from '@/plugins/enterprise-ready';
|
||||||
import { GPUStackPluginManager } from '@/plugins/manager';
|
import { GPUStackPluginManager } from '@/plugins/manager';
|
||||||
import { requestConfig } from '@/request-config';
|
import { requestConfig } from '@/request-config';
|
||||||
@@ -13,6 +14,11 @@ import {
|
|||||||
} from '@/services/profile/apis';
|
} from '@/services/profile/apis';
|
||||||
import { fetchSystemConfig } from '@/services/system/query-system-config';
|
import { fetchSystemConfig } from '@/services/system/query-system-config';
|
||||||
import { isOnline } from '@/utils';
|
import { isOnline } from '@/utils';
|
||||||
|
import {
|
||||||
|
markInitialStateProbed,
|
||||||
|
probeAccessFlags
|
||||||
|
} from '@/utils/access-probes';
|
||||||
|
import { installTenantFetch } from '@/utils/install-fetch';
|
||||||
import {
|
import {
|
||||||
IS_FIRST_LOGIN,
|
IS_FIRST_LOGIN,
|
||||||
readState,
|
readState,
|
||||||
@@ -22,6 +28,8 @@ import '@gpustack/core-ui/style.css';
|
|||||||
import { RequestConfig, history, request as umiRequest } from '@umijs/max';
|
import { RequestConfig, history, request as umiRequest } from '@umijs/max';
|
||||||
import { message } from 'antd';
|
import { message } from 'antd';
|
||||||
|
|
||||||
|
installTenantFetch();
|
||||||
|
|
||||||
// only for the first login and access from http://localhost
|
// only for the first login and access from http://localhost
|
||||||
|
|
||||||
const checkDefaultPage = async (userInfo: any) => {
|
const checkDefaultPage = async (userInfo: any) => {
|
||||||
@@ -39,6 +47,8 @@ export async function getInitialState(): Promise<{
|
|||||||
fetchUserInfo: () => Promise<Global.UserInfo>;
|
fetchUserInfo: () => Promise<Global.UserInfo>;
|
||||||
currentUser?: Global.UserInfo;
|
currentUser?: Global.UserInfo;
|
||||||
pluginData?: Record<string, any>;
|
pluginData?: Record<string, any>;
|
||||||
|
hasKubernetesCluster?: boolean;
|
||||||
|
hasResourceEvents?: boolean;
|
||||||
}> {
|
}> {
|
||||||
const { location } = history;
|
const { location } = history;
|
||||||
|
|
||||||
@@ -83,6 +93,36 @@ export async function getInitialState(): Promise<{
|
|||||||
getUpdateCheck();
|
getUpdateCheck();
|
||||||
fetchSystemConfig();
|
fetchSystemConfig();
|
||||||
}
|
}
|
||||||
|
// Only commit a substantive user object. A truthy-but-empty
|
||||||
|
// `data` (e.g. server responded 200 with an empty body) would
|
||||||
|
// otherwise look like "logged in" to every `currentUser`
|
||||||
|
// reader and the access seam — break out instead and let the
|
||||||
|
// caller treat the request as failed.
|
||||||
|
if (data && typeof data === 'object' && Object.keys(data).length > 0) {
|
||||||
|
// Commit the identity to atom storage (and so to localStorage)
|
||||||
|
// before returning. The access function — memoized on
|
||||||
|
// `initialState` and run once per commit — reads identity from
|
||||||
|
// localStorage; without this preemptive write the predicate
|
||||||
|
// sees the prior session's identity on its first evaluation
|
||||||
|
// after login, and stays stale until the next identity change
|
||||||
|
// (which usually doesn't come without a manual refresh).
|
||||||
|
try {
|
||||||
|
setAtomStorage(userAtom, data);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('userAtom commit error:', err);
|
||||||
|
}
|
||||||
|
// Fire `onUserFetched` so plugins maintaining identity-scoped
|
||||||
|
// caches can seed them under the new identity before any
|
||||||
|
// caller commits this user to `initialState`. Errors here are
|
||||||
|
// swallowed and logged — fetchUserInfo must still return.
|
||||||
|
try {
|
||||||
|
await getGPUStackPlugin()?.login?.onUserFetched?.(data, {
|
||||||
|
request: umiRequest
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error('onUserFetched plugin hook error:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
return data;
|
return data;
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
const data = error?.response?.data;
|
const data = error?.response?.data;
|
||||||
@@ -122,12 +162,24 @@ export async function getInitialState(): Promise<{
|
|||||||
getAppVersionInfo();
|
getAppVersionInfo();
|
||||||
|
|
||||||
if (![DEFAULT_ENTER_PAGE.login].includes(location.pathname)) {
|
if (![DEFAULT_ENTER_PAGE.login].includes(location.pathname)) {
|
||||||
const userInfo = await fetchUserInfo();
|
const [userInfo, accessFlags] = await Promise.all([
|
||||||
|
fetchUserInfo(),
|
||||||
|
probeAccessFlags()
|
||||||
|
]);
|
||||||
|
// Record that the probes ran for an authenticated user this page load
|
||||||
|
// (the refresh path) so the layout doesn't re-probe. A failed
|
||||||
|
// fetch (empty user — e.g. unauthenticated deep link that bounces to
|
||||||
|
// login) is NOT marked: the user will log in via SPA afterwards and
|
||||||
|
// the layout becomes responsible for probing.
|
||||||
|
if (userInfo?.username) {
|
||||||
|
markInitialStateProbed();
|
||||||
|
}
|
||||||
checkDefaultPage(userInfo);
|
checkDefaultPage(userInfo);
|
||||||
return {
|
return {
|
||||||
fetchUserInfo,
|
fetchUserInfo,
|
||||||
currentUser: userInfo,
|
currentUser: userInfo,
|
||||||
pluginData
|
pluginData,
|
||||||
|
...accessFlags
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 7.0 KiB After Width: | Height: | Size: 7.0 KiB |
|
After Width: | Height: | Size: 7.2 KiB |
|
After Width: | Height: | Size: 56 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 6.8 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 8.9 KiB |
|
After Width: | Height: | Size: 4.6 KiB |
|
After Width: | Height: | Size: 2.1 MiB |
|
After Width: | Height: | Size: 5.4 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 3.6 KiB |
|
After Width: | Height: | Size: 4.8 KiB |
|
After Width: | Height: | Size: 5.9 KiB |
|
After Width: | Height: | Size: 196 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 7.2 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 9.3 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 9.2 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 68 KiB |
|
After Width: | Height: | Size: 7.5 KiB |
|
After Width: | Height: | Size: 640 B |
|
After Width: | Height: | Size: 4.1 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 8.2 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 4.8 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 5.1 KiB |
|
After Width: | Height: | Size: 801 B |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 8.4 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 8.1 KiB |
|
After Width: | Height: | Size: 2.4 KiB |
@@ -0,0 +1 @@
|
|||||||
|
<svg height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>AlibabaCloud</title><path d="M14.752 4.64h5.274C22.242 4.64 24 6.475 24 8.691V15.8a3.947 3.947 0 01-3.974 3.975h-5.274l1.299-1.835 3.822-1.222c.688-.23 1.146-.918 1.146-1.605v-5.81c0-.687-.458-1.375-1.146-1.605L16.05 6.475l-1.3-1.835zM2.98 15.111c0 .688.46 1.376 1.147 1.606l3.822 1.146 1.3 1.835H3.974A3.947 3.947 0 010 15.723V8.69c0-2.216 1.758-4.05 3.975-4.05h5.273L7.95 6.474 4.127 7.697c-.688.23-1.146.918-1.146 1.606v5.808z" fill="#FF6A00"></path><path d="M16.051 11.213H8.025v1.835h8.026v-1.835z" fill="#FF6A00"></path></svg>
|
||||||
|
After Width: | Height: | Size: 656 B |
@@ -0,0 +1 @@
|
|||||||
|
<svg height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>BaiLian</title><path d="M6.336 8.919v6.162l5.335-3.083L6.337 8.92z" fill="#1C54E3"></path><path d="M21.394 5.288s-.006-.006-.01-.006L17.01 2.754 6.336 8.92l5.335 3.082 9.701-5.6.016-.01a.635.635 0 00.006-1.1v-.003z" fill="#AA9AFF"></path><path d="M21.71 12.465a.62.62 0 00-.316.085s-.006 0-.009.003l-4.375 2.528 5.05 2.915h.006a2.06 2.06 0 00.28-1.04v-3.855a.637.637 0 00-.636-.636z" fill="#00EAD1"></path><path d="M22.06 17.996l-5.05-2.915L6.34 21.242l4.27 2.465s.016.006.022.012a2.102 2.102 0 002.093 0c.006-.003.016-.006.022-.012l8.538-4.93c.003 0 .006-.003.01-.006.321-.183.589-.45.775-.772h-.006l-.004-.003z" fill="#00CEC9"></path><path d="M11.672 11.998l-5.336 3.083-1.444.832-3.605 2.083H1.28c.173.303.416.555.709.738l.078.044.016.01.02.012 4.232 2.442 10.671-6.161-5.335-3.082z" fill="#00EAD1"></path><path d="M12.74.29c-.1-.06-.208-.107-.315-.148-.02-.006-.038-.016-.057-.022a2.121 2.121 0 00-.7-.12c-.233 0-.457.038-.668.11l-.031.01a2.196 2.196 0 00-.372.17L2.068 5.222s-.003 0-.006.003c-.324.183-.592.451-.781.773h.006l5.049 2.918L17.01 2.758 12.74.29z" fill="#7347FF"></path><path d="M1.287 6.001H1.28A2.06 2.06 0 001 7.041v9.915c0 .378.1.735.28 1.043h.007l5.049-2.918V8.919l-5.05-2.918z" fill="#0423DA"></path></svg>
|
||||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -3,6 +3,7 @@
|
|||||||
.ant-layout-sider-children {
|
.ant-layout-sider-children {
|
||||||
border-inline: none;
|
border-inline: none;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
|
padding-inline-end: 0;
|
||||||
padding-block-end: 8px;
|
padding-block-end: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -63,6 +63,13 @@ export const fromClusterCreationAtom = atom(false);
|
|||||||
export const clusterSessionAtom = atom<{
|
export const clusterSessionAtom = atom<{
|
||||||
firstAddWorker: boolean;
|
firstAddWorker: boolean;
|
||||||
firstAddCluster: boolean;
|
firstAddCluster: boolean;
|
||||||
|
presetClusterType?: 'model' | 'gpu';
|
||||||
|
// Provider to preselect when the create flow opens — set by the
|
||||||
|
// empty-state CTA on feature pages that need a specific provider
|
||||||
|
// (e.g. GPU Service can only schedule on Kubernetes, so its
|
||||||
|
// "Add Cluster" button skips provider catalog and lands on the
|
||||||
|
// K8s configure step). Consumed once by ClusterCreate on mount.
|
||||||
|
providerHint?: string;
|
||||||
} | null>(null);
|
} | null>(null);
|
||||||
|
|
||||||
export const clusterDetailAtom = atom<ClusterListItem | null>(null);
|
export const clusterDetailAtom = atom<ClusterListItem | null>(null);
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
import { ClusterListItem } from '@/pages/cluster-management/config/types';
|
|
||||||
import { atom } from 'jotai';
|
|
||||||
|
|
||||||
export const currentClusterAtom = atom<
|
|
||||||
(Partial<ClusterListItem> & { label?: string; value?: number }) | null
|
|
||||||
>(null);
|
|
||||||
|
|
||||||
export const addSSHKeyPageAtom = atom<{ create: boolean }>({
|
|
||||||
create: false
|
|
||||||
});
|
|
||||||
@@ -55,3 +55,10 @@ export const userSettingsHelperAtom = atom(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
export const hideModalTemporarilyAtom = atom<boolean>(false);
|
export const hideModalTemporarilyAtom = atom<boolean>(false);
|
||||||
|
|
||||||
|
export const collapsedMenuGroupsAtom = atomWithStorage<string[]>(
|
||||||
|
'collapsedMenuGroups',
|
||||||
|
[],
|
||||||
|
undefined,
|
||||||
|
{ getOnInit: true }
|
||||||
|
);
|
||||||
|
|||||||
@@ -3,6 +3,15 @@ import { atomWithStorage } from 'jotai/utils';
|
|||||||
|
|
||||||
export const userAtom = atomWithStorage<any>('userInfo', null);
|
export const userAtom = atomWithStorage<any>('userInfo', null);
|
||||||
|
|
||||||
|
// Backs the `currentOrganizationId` localStorage key. Stays null in
|
||||||
|
// builds with no Org context (single-tenant), and is shared with any
|
||||||
|
// extension that persists the same key so both sides stay in sync
|
||||||
|
// without one side having to import from the other.
|
||||||
|
export const currentOrganizationIdAtom = atomWithStorage<number | null>(
|
||||||
|
'currentOrganizationId',
|
||||||
|
null
|
||||||
|
);
|
||||||
|
|
||||||
export const GPUStackVersionAtom = atom<{
|
export const GPUStackVersionAtom = atom<{
|
||||||
version: string;
|
version: string;
|
||||||
git_commit: string;
|
git_commit: string;
|
||||||
@@ -23,10 +32,7 @@ export const UpdateCheckAtom = atom<{
|
|||||||
latest_version: ''
|
latest_version: ''
|
||||||
});
|
});
|
||||||
|
|
||||||
export const initialPasswordAtom = atomWithStorage<string>(
|
export const initialPasswordAtom = atom<string>('');
|
||||||
'initialPassword',
|
|
||||||
''
|
|
||||||
);
|
|
||||||
|
|
||||||
// Namespace the server creates for an Org's resources on each Kubernetes
|
// Namespace the server creates for an Org's resources on each Kubernetes
|
||||||
// cluster. The format must match the backend's ``get_namespace_name``
|
// cluster. The format must match the backend's ``get_namespace_name``
|
||||||
@@ -81,23 +87,57 @@ const getStoredCurrentOrgId = (): number | null => {
|
|||||||
// cluster-owner fallback.
|
// cluster-owner fallback.
|
||||||
const ORG_CACHE_KEYS = ['organizationList', 'allOrganizations'] as const;
|
const ORG_CACHE_KEYS = ['organizationList', 'allOrganizations'] as const;
|
||||||
|
|
||||||
const lookupOrgNamespace = (id: number | null): string | null => {
|
export interface CachedOrg {
|
||||||
|
id: number;
|
||||||
|
name?: string;
|
||||||
|
// The platform Org (single global tenant). Its models are NOT
|
||||||
|
// namespaced in ``/v1/models`` — they appear under their bare name.
|
||||||
|
is_platform?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve the cached Org record for an owner/principal id by scanning both
|
||||||
|
// org caches. ``organizationList`` (the caller's member orgs) is checked
|
||||||
|
// alongside the admin-only ``allOrganizations`` so member sessions resolve
|
||||||
|
// too. Id types vary between localStorage payloads (some writers stringify,
|
||||||
|
// others persist as a JSON number), so compare as strings — strict equality
|
||||||
|
// would silently miss those cases.
|
||||||
|
export const getOrgById = (
|
||||||
|
id: number | string | null | undefined
|
||||||
|
): CachedOrg | null => {
|
||||||
if (id == null) return null;
|
if (id == null) return null;
|
||||||
// Normalise both sides to strings — the stored id type varies between
|
|
||||||
// localStorage payloads (some writers stringify, others persist as a
|
|
||||||
// JSON number); strict equality would silently miss those cases.
|
|
||||||
const target = String(id);
|
const target = String(id);
|
||||||
for (const key of ORG_CACHE_KEYS) {
|
for (const key of ORG_CACHE_KEYS) {
|
||||||
try {
|
try {
|
||||||
const raw = localStorage.getItem(key);
|
const raw = localStorage.getItem(key);
|
||||||
if (!raw) continue;
|
if (!raw) continue;
|
||||||
const list = JSON.parse(raw) as Array<{ id: number; name?: string }>;
|
const list = JSON.parse(raw) as CachedOrg[];
|
||||||
if (!Array.isArray(list)) continue;
|
if (!Array.isArray(list)) continue;
|
||||||
const match = list.find((item) => String(item?.id) === target);
|
const match = list.find((item) => String(item?.id) === target);
|
||||||
if (match?.name) return `gpustack-${match.name}`;
|
if (match) return match;
|
||||||
} catch {
|
} catch {
|
||||||
// ignore malformed cache; continue checking other keys
|
// ignore malformed cache; continue checking other keys
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Bare Org *name* (e.g. ``org1``) for an owner/principal id, or null.
|
||||||
|
export const getOrgNameById = (
|
||||||
|
id: number | string | null | undefined
|
||||||
|
): string | null => {
|
||||||
|
return getOrgById(id)?.name ?? null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const lookupOrgNamespace = (id: number | null): string | null => {
|
||||||
|
const name = getOrgNameById(id);
|
||||||
|
return name ? `gpustack-${name}` : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
// The Org the caller is currently acting under, or null in the admin-"All"
|
||||||
|
// context. The org-switcher reloads the page on switch, so the list pages
|
||||||
|
// always show this org's resources — which is how ``/v1/models`` namespaces
|
||||||
|
// their model ids (``{org}/{name}``). Callers reconstructing that id use this
|
||||||
|
// as the fallback owner when a row carries no explicit ``owner_principal_id``.
|
||||||
|
export const getCurrentOrg = (): CachedOrg | null => {
|
||||||
|
return getOrgById(getStoredCurrentOrgId());
|
||||||
|
};
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
// Wrapper around core-ui's FullMarkdown that co-locates the KaTeX stylesheet.
|
||||||
|
//
|
||||||
|
// core-ui deliberately does NOT bundle katex.min.css (importing it there
|
||||||
|
// base64-inlines ~1.4MB of fonts into the shared, render-blocking index.css).
|
||||||
|
// Importing it here keeps the KaTeX CSS in the route chunk that actually
|
||||||
|
// renders math, so it loads lazily and never blocks first paint.
|
||||||
|
//
|
||||||
|
// Always import FullMarkdown from this module, not from '@gpustack/core-ui/markdown'.
|
||||||
|
import { FullMarkdown } from '@gpustack/core-ui/markdown';
|
||||||
|
import 'katex/dist/katex.min.css';
|
||||||
|
|
||||||
|
export default FullMarkdown;
|
||||||
@@ -92,6 +92,12 @@ declare namespace Global {
|
|||||||
interface InitialStateType {
|
interface InitialStateType {
|
||||||
fetchUserInfo: () => Promise<UserInfo>;
|
fetchUserInfo: () => Promise<UserInfo>;
|
||||||
currentUser?: UserInfo;
|
currentUser?: UserInfo;
|
||||||
|
// Captured at app boot so access predicates can gate GPU Service —
|
||||||
|
// the feature is Kubernetes-only, and Org members without a K8s
|
||||||
|
// cluster they can schedule on shouldn't see the menu. Refreshed
|
||||||
|
// by full page reload (e.g. OrgSwitcher) which re-runs
|
||||||
|
// getInitialState.
|
||||||
|
hasKubernetesCluster?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
type SearchParams = Pagination & { search?: string; [key: string]: any };
|
type SearchParams = Pagination & { search?: string; [key: string]: any };
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ export default {
|
|||||||
rowSelectedBg: 'transparent',
|
rowSelectedBg: 'transparent',
|
||||||
headerSortActiveBg: 'transparent',
|
headerSortActiveBg: 'transparent',
|
||||||
headerSortHoverBg: 'transparent',
|
headerSortHoverBg: 'transparent',
|
||||||
|
bodySortBg: 'transparent',
|
||||||
headerBg: 'none'
|
headerBg: 'none'
|
||||||
},
|
},
|
||||||
Button: {
|
Button: {
|
||||||
@@ -54,7 +55,8 @@ export default {
|
|||||||
itemHoverColor: 'rgba(0,0,0,1)',
|
itemHoverColor: 'rgba(0,0,0,1)',
|
||||||
itemColor: 'rgba(0,0,0,1)',
|
itemColor: 'rgba(0,0,0,1)',
|
||||||
itemHoverBg: 'rgb(24 25 27)',
|
itemHoverBg: 'rgb(24 25 27)',
|
||||||
itemActiveBg: 'rgb(24 25 27)'
|
itemActiveBg: 'rgb(24 25 27)',
|
||||||
|
menuItemSelectedBg: '#292929'
|
||||||
},
|
},
|
||||||
Progress: {
|
Progress: {
|
||||||
lineBorderRadius: 4
|
lineBorderRadius: 4
|
||||||
@@ -108,6 +110,7 @@ export default {
|
|||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
motion: true,
|
motion: true,
|
||||||
colorFill: '#0A0A0A',
|
colorFill: '#0A0A0A',
|
||||||
colorBgBase: '#0A0A0A'
|
colorBgBase: '#0A0A0A',
|
||||||
|
menuItemSelectedBg: '#292929'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ export default {
|
|||||||
rowSelectedBg: 'transparent',
|
rowSelectedBg: 'transparent',
|
||||||
headerSortActiveBg: 'transparent',
|
headerSortActiveBg: 'transparent',
|
||||||
headerSortHoverBg: 'transparent',
|
headerSortHoverBg: 'transparent',
|
||||||
|
bodySortBg: 'transparent',
|
||||||
headerSplitColor: '#e8e8e8',
|
headerSplitColor: '#e8e8e8',
|
||||||
headerBg: 'none'
|
headerBg: 'none'
|
||||||
},
|
},
|
||||||
@@ -56,7 +57,8 @@ export default {
|
|||||||
itemHoverColor: 'rgba(0,0,0,1)',
|
itemHoverColor: 'rgba(0,0,0,1)',
|
||||||
itemColor: 'rgba(0,0,0,1)',
|
itemColor: 'rgba(0,0,0,1)',
|
||||||
itemHoverBg: 'rgba(0,0,0,0.04)',
|
itemHoverBg: 'rgba(0,0,0,0.04)',
|
||||||
itemActiveBg: 'rgba(0,0,0,0.04)'
|
itemActiveBg: 'rgba(0,0,0,0.04)',
|
||||||
|
menuItemSelectedBg: '#e8eaed'
|
||||||
},
|
},
|
||||||
Progress: {
|
Progress: {
|
||||||
lineBorderRadius: 3
|
lineBorderRadius: 3
|
||||||
|
|||||||
@@ -171,8 +171,6 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.ant-table .ant-table-container table {
|
.ant-table .ant-table-container table {
|
||||||
// border-spacing: 0 20px;
|
|
||||||
|
|
||||||
.ant-table-thead th.ant-table-column-sort {
|
.ant-table-thead th.ant-table-column-sort {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
|
|
||||||
@@ -241,8 +239,8 @@ body {
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
||||||
.ant-pro-sider-footer {
|
.ant-pro-sider-footer {
|
||||||
padding-block: 2px 0;
|
padding-block: 4px 0;
|
||||||
padding-left: 6px;
|
padding-left: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ant-pro-sider-actions-list-item {
|
.ant-pro-sider-actions-list-item {
|
||||||
@@ -259,49 +257,29 @@ body {
|
|||||||
border-block-end: none;
|
border-block-end: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ant-pro-sider-logo {
|
||||||
|
.collapse-btn {
|
||||||
|
color: var(--ant-color-text-tertiary);
|
||||||
|
:hover {
|
||||||
|
color: var(--ant-color-text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.ant-pro-sider-logo-collapsed {
|
.ant-pro-sider-logo-collapsed {
|
||||||
padding-left: 12px;
|
padding-left: 6px;
|
||||||
cursor: e-resize;
|
|
||||||
|
|
||||||
.collapse-wrap {
|
|
||||||
display: none;
|
|
||||||
position: absolute;
|
|
||||||
top: 12px;
|
|
||||||
|
|
||||||
&::after {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
height: 48px;
|
|
||||||
width: 64px;
|
|
||||||
top: -16px;
|
|
||||||
left: -16px;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
.ant-table .ant-table-tbody {
|
||||||
.collapse-wrap {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.ant-pro-sider .ant-layout-sider-children {
|
|
||||||
// border-right: 1px solid var(--ant-color-split);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.ant-table-content table {
|
|
||||||
.ant-table-tbody {
|
|
||||||
.ant-table-row {
|
.ant-table-row {
|
||||||
border-radius: var(--table-td-radius);
|
border-radius: var(--table-td-radius);
|
||||||
|
|
||||||
> td {
|
> td {
|
||||||
background-color: unset;
|
|
||||||
border-bottom: 1px solid var(--ant-color-split);
|
border-bottom: 1px solid var(--ant-color-split);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// ============== new theme style end =================
|
// ============== new theme style end =================
|
||||||
}
|
}
|
||||||
@@ -347,7 +325,6 @@ body {
|
|||||||
.ant-pro-layout-container {
|
.ant-pro-layout-container {
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
// background-color: var(--ant-color-bg-container);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.ant-pro-sider {
|
.ant-pro-sider {
|
||||||
@@ -661,10 +638,15 @@ body {
|
|||||||
inset: 53px auto auto 0 !important;
|
inset: 53px auto auto 0 !important;
|
||||||
border-radius: 0 0 var(--border-radius-base) var(--border-radius-base);
|
border-radius: 0 0 var(--border-radius-base) var(--border-radius-base);
|
||||||
border-top: none;
|
border-top: none;
|
||||||
max-height: 240px;
|
.ant-cascader-menus {
|
||||||
min-height: 100px;
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
&.gpu-selector {
|
&.gpu-selector {
|
||||||
|
.ant-cascader-menu {
|
||||||
|
max-height: 400px;
|
||||||
|
height: unset;
|
||||||
|
}
|
||||||
.ant-cascader-menu:last-child {
|
.ant-cascader-menu:last-child {
|
||||||
flex: auto;
|
flex: auto;
|
||||||
|
|
||||||
@@ -812,6 +794,8 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.ant-pro-sider-logo + div {
|
.ant-pro-sider-logo + div {
|
||||||
|
display: flex;
|
||||||
|
overflow: hidden;
|
||||||
&::-webkit-scrollbar {
|
&::-webkit-scrollbar {
|
||||||
width: 0;
|
width: 0;
|
||||||
}
|
}
|
||||||
@@ -867,3 +851,6 @@ body {
|
|||||||
padding-right: 8px;
|
padding-right: 8px;
|
||||||
padding-block: 8px;
|
padding-block: 8px;
|
||||||
}
|
}
|
||||||
|
.ant-select-multiple .ant-select-content {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
export default function useActions<T>(actions: Global.ActionItem<T>[], ctx: T) {
|
|
||||||
return actions
|
|
||||||
.filter((action) => {
|
|
||||||
return action.visible ? action.visible(ctx) : true;
|
|
||||||
})
|
|
||||||
.map((action) => ({
|
|
||||||
...action,
|
|
||||||
disabled: action.disabled?.(ctx)
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
import { useIntl } from '@umijs/max';
|
|
||||||
import { message } from 'antd';
|
|
||||||
|
|
||||||
type MessageType = 'input' | 'select';
|
|
||||||
|
|
||||||
const useAppUtils = () => {
|
|
||||||
const intl = useIntl();
|
|
||||||
const [messageApi, contextHolder] = message.useMessage();
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param type Array<'input' | 'select'>
|
|
||||||
* @param name
|
|
||||||
* @param locale boolean
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
const getRuleMessage = (
|
|
||||||
type: MessageType | MessageType[],
|
|
||||||
name: string,
|
|
||||||
locale = true
|
|
||||||
) => {
|
|
||||||
const nameStr = locale ? intl.formatMessage({ id: name }) : name;
|
|
||||||
// transform type to array
|
|
||||||
const typeList = Array.isArray(type) ? type : [type];
|
|
||||||
|
|
||||||
if (typeList.includes('select') && typeList.includes('input')) {
|
|
||||||
return intl.formatMessage(
|
|
||||||
{ id: 'common.form.rule.selectInput' },
|
|
||||||
{ name: nameStr }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeList.includes('input')) {
|
|
||||||
return intl.formatMessage(
|
|
||||||
{ id: 'common.form.rule.input' },
|
|
||||||
{ name: nameStr }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return intl.formatMessage(
|
|
||||||
{ id: 'common.form.rule.select' },
|
|
||||||
{ name: nameStr }
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const showSuccess = (msg?: string) => {
|
|
||||||
messageApi.success(
|
|
||||||
msg || intl.formatMessage({ id: 'common.message.success' })
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
getRuleMessage,
|
|
||||||
showSuccess
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default useAppUtils;
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
// broadcast channel hook
|
|
||||||
|
|
||||||
import { useEffect, useRef } from 'react';
|
|
||||||
|
|
||||||
export const useBroadcast = () => {
|
|
||||||
const broadcastChannel = useRef<BroadcastChannel | null>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
broadcastChannel.current = new BroadcastChannel('broadcast_channel');
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
broadcastChannel.current?.close();
|
|
||||||
console.log('broadcast channel closed');
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return { broadcastChannel };
|
|
||||||
};
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
import _ from 'lodash';
|
|
||||||
import { useRef } from 'react';
|
|
||||||
|
|
||||||
export default function useContainerScroll(
|
|
||||||
container: any,
|
|
||||||
options?: { toBottom?: boolean }
|
|
||||||
) {
|
|
||||||
const isWheeled = useRef(false);
|
|
||||||
const scroller = useRef(container);
|
|
||||||
const optionsRef = useRef(options);
|
|
||||||
const toBottomFlag = useRef(options?.toBottom);
|
|
||||||
const timerRef = useRef<any>(null);
|
|
||||||
|
|
||||||
const debunceResetWheeled = _.debounce(() => {
|
|
||||||
isWheeled.current = false;
|
|
||||||
}, 5000);
|
|
||||||
|
|
||||||
const handleContentWheel = (e: any) => {
|
|
||||||
isWheeled.current = true;
|
|
||||||
debunceResetWheeled.cancel?.();
|
|
||||||
debunceResetWheeled();
|
|
||||||
};
|
|
||||||
|
|
||||||
const scrollerRun = () => {
|
|
||||||
const scrollerContainer = scroller.current?.current || {};
|
|
||||||
const { scrollHeight, clientHeight, scrollTop } = scrollerContainer;
|
|
||||||
if (
|
|
||||||
optionsRef.current?.toBottom &&
|
|
||||||
toBottomFlag.current &&
|
|
||||||
scrollHeight > clientHeight + scrollTop
|
|
||||||
) {
|
|
||||||
scroller.current.current.scrollTop = scrollHeight;
|
|
||||||
// toBottomFlag.current = false;
|
|
||||||
isWheeled.current = false;
|
|
||||||
} else if (
|
|
||||||
!isWheeled.current &&
|
|
||||||
scrollHeight > clientHeight + scrollTop &&
|
|
||||||
scroller.current?.current
|
|
||||||
) {
|
|
||||||
scroller.current.current.scrollTop += 10;
|
|
||||||
window.requestAnimationFrame(scrollerRun);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateScrollerPosition = () => {
|
|
||||||
if (!isWheeled.current) {
|
|
||||||
window.requestAnimationFrame(scrollerRun);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
handleContentWheel,
|
|
||||||
updateScrollerPosition,
|
|
||||||
scroller
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
import { useCallback, useState } from 'react';
|
|
||||||
|
|
||||||
const useCopyToClipboard = () => {
|
|
||||||
const [copied, setCopied] = useState(false);
|
|
||||||
|
|
||||||
const copyToClipboard = useCallback(async (text: string) => {
|
|
||||||
try {
|
|
||||||
if (navigator.clipboard) {
|
|
||||||
await navigator.clipboard.writeText(text);
|
|
||||||
setCopied(true);
|
|
||||||
setTimeout(() => {
|
|
||||||
setCopied(false);
|
|
||||||
}, 3000);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
setCopied(false);
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return { copied, copyToClipboard };
|
|
||||||
};
|
|
||||||
|
|
||||||
export default useCopyToClipboard;
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
import { HandlerOptions } from '@/hooks/use-chunk-fetch';
|
|
||||||
import { useDownloadStream } from '@gpustack/core-ui';
|
|
||||||
import { useIntl } from '@umijs/max';
|
|
||||||
import { Progress, notification } from 'antd';
|
|
||||||
import dayjs from 'dayjs';
|
|
||||||
|
|
||||||
const renderMessage = (title: string) => {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
width: 280,
|
|
||||||
textOverflow: 'ellipsis',
|
|
||||||
whiteSpace: 'nowrap',
|
|
||||||
overflow: 'hidden'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{title}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const createFileName = (name: string) => {
|
|
||||||
const timestamp = dayjs().format('YYYY-MM-DD_HH-mm-ss');
|
|
||||||
const fileName = `${name}_${timestamp}.txt`;
|
|
||||||
return fileName;
|
|
||||||
};
|
|
||||||
|
|
||||||
const useDownloadLogs = () => {
|
|
||||||
const { downloadStream } = useDownloadStream();
|
|
||||||
const intl = useIntl();
|
|
||||||
const [api, contextHolder] = notification.useNotification({
|
|
||||||
stack: { threshold: 1 }
|
|
||||||
});
|
|
||||||
|
|
||||||
const downloadNotification = (
|
|
||||||
data: HandlerOptions & {
|
|
||||||
filename: string;
|
|
||||||
duration?: number;
|
|
||||||
chunkRequestRef: any;
|
|
||||||
}
|
|
||||||
) => {
|
|
||||||
api.open({
|
|
||||||
duration: data.duration,
|
|
||||||
message: renderMessage(data.filename),
|
|
||||||
key: data.filename,
|
|
||||||
closeIcon: (
|
|
||||||
<span>{intl.formatMessage({ id: 'common.button.cancel' })}</span>
|
|
||||||
),
|
|
||||||
description: <Progress percent={data.percent} size="small"></Progress>,
|
|
||||||
onClose() {
|
|
||||||
data.chunkRequestRef?.current?.abort();
|
|
||||||
notification.destroy?.(data.filename);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDownloadLog = async (params: { url: string; name: string }) => {
|
|
||||||
downloadStream({
|
|
||||||
url: params.url,
|
|
||||||
filename: createFileName(params.name),
|
|
||||||
downloadNotification
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
onDownloadLog: handleDownloadLog,
|
|
||||||
contextHolder
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default useDownloadLogs;
|
|
||||||
@@ -1,136 +0,0 @@
|
|||||||
import useSetChunkFetch, { HandlerOptions } from '@/hooks/use-chunk-fetch';
|
|
||||||
import { message } from 'antd';
|
|
||||||
import { useEffect, useRef } from 'react';
|
|
||||||
|
|
||||||
export default function useDownloadStream() {
|
|
||||||
const chunkRequestRef = useRef<any>(null);
|
|
||||||
const logParseWorker = useRef<any>(null);
|
|
||||||
const clearScreen = useRef(false);
|
|
||||||
const filename = useRef('log');
|
|
||||||
const downloadNotificationRef = useRef<any>(null);
|
|
||||||
|
|
||||||
const { setChunkFetch } = useSetChunkFetch();
|
|
||||||
|
|
||||||
const downloadFile = (content: string) => {
|
|
||||||
const blob = new Blob([content], { type: 'text/plain' });
|
|
||||||
const url = URL.createObjectURL(blob);
|
|
||||||
|
|
||||||
const a = document.createElement('a');
|
|
||||||
a.href = url;
|
|
||||||
a.download = filename.current;
|
|
||||||
document.body.appendChild(a);
|
|
||||||
a.click();
|
|
||||||
document.body.removeChild(a);
|
|
||||||
|
|
||||||
URL.revokeObjectURL(url);
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateContent = (data: string, options?: HandlerOptions) => {
|
|
||||||
const { isComplete, percent } = options || {};
|
|
||||||
|
|
||||||
logParseWorker.current?.postMessage({
|
|
||||||
inputStr: data,
|
|
||||||
reset: clearScreen.current,
|
|
||||||
isComplete: isComplete,
|
|
||||||
percent: percent,
|
|
||||||
chunked: false
|
|
||||||
});
|
|
||||||
clearScreen.current = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleError = (error: any) => {
|
|
||||||
const errorMsg = error?.message || error;
|
|
||||||
const msg =
|
|
||||||
typeof errorMsg === 'string' ? errorMsg : JSON.stringify(errorMsg);
|
|
||||||
message.error(msg);
|
|
||||||
downloadNotificationRef.current?.({
|
|
||||||
duration: 1,
|
|
||||||
percent: 0,
|
|
||||||
filename: filename.current
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const downloadStream = async (props: {
|
|
||||||
data?: any;
|
|
||||||
url: string;
|
|
||||||
params?: any;
|
|
||||||
signal?: AbortSignal;
|
|
||||||
method?: string;
|
|
||||||
headers?: any;
|
|
||||||
filename?: string;
|
|
||||||
downloadNotification?: (data: any) => void;
|
|
||||||
}) => {
|
|
||||||
try {
|
|
||||||
clearScreen.current = true;
|
|
||||||
filename.current = props.filename || 'log';
|
|
||||||
downloadNotificationRef.current = props.downloadNotification;
|
|
||||||
const { params, url } = props;
|
|
||||||
|
|
||||||
chunkRequestRef.current?.current?.abort?.();
|
|
||||||
|
|
||||||
chunkRequestRef.current = setChunkFetch({
|
|
||||||
url,
|
|
||||||
params,
|
|
||||||
watch: false,
|
|
||||||
contentType: 'text',
|
|
||||||
errorHandler: handleError,
|
|
||||||
handler: updateContent
|
|
||||||
});
|
|
||||||
downloadNotificationRef.current?.({
|
|
||||||
filename: filename.current,
|
|
||||||
duration: null,
|
|
||||||
chunkRequestRef: chunkRequestRef.current
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
//
|
|
||||||
downloadNotificationRef.current?.({
|
|
||||||
duration: 1,
|
|
||||||
percent: 0,
|
|
||||||
filename: filename.current
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
logParseWorker.current?.terminate?.();
|
|
||||||
|
|
||||||
logParseWorker.current = new Worker(
|
|
||||||
// @ts-ignore
|
|
||||||
new URL('@/components/logs-viewer/parse-worker.ts', import.meta.url),
|
|
||||||
{
|
|
||||||
type: 'module'
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
logParseWorker.current.onmessage = (event: any) => {
|
|
||||||
const { result, isComplete, percent } = event.data;
|
|
||||||
|
|
||||||
const isAborted = chunkRequestRef.current?.current?.signal?.aborted;
|
|
||||||
if (!isComplete && !isAborted) {
|
|
||||||
downloadNotificationRef.current?.({
|
|
||||||
percent: percent,
|
|
||||||
duration: null,
|
|
||||||
filename: filename.current,
|
|
||||||
chunkRequestRef: chunkRequestRef.current
|
|
||||||
});
|
|
||||||
} else if (isComplete && !isAborted) {
|
|
||||||
downloadNotificationRef.current?.({
|
|
||||||
duration: 1,
|
|
||||||
percent: 100,
|
|
||||||
filename: filename.current
|
|
||||||
});
|
|
||||||
downloadFile(result);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
if (logParseWorker.current) {
|
|
||||||
logParseWorker.current.terminate();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return {
|
|
||||||
downloadStream
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
import { useIntl } from '@umijs/max';
|
|
||||||
import { driver, type Config } from 'driver.js';
|
|
||||||
import { useEffect, useRef } from 'react';
|
|
||||||
|
|
||||||
export const useDriver = (config?: Config & { id: string }) => {
|
|
||||||
const intl = useIntl();
|
|
||||||
const driverRef = useRef<any>(null);
|
|
||||||
|
|
||||||
const handleDoNotShowAgain = () => {};
|
|
||||||
|
|
||||||
const init = () => {
|
|
||||||
driverRef.current = driver({
|
|
||||||
overlayOpacity: 0.2,
|
|
||||||
animate: false,
|
|
||||||
...config
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const start = () => {
|
|
||||||
if (!driverRef.current) {
|
|
||||||
init();
|
|
||||||
}
|
|
||||||
driverRef.current.drive();
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
return () => {
|
|
||||||
driverRef.current?.destroy();
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return { start, initDriver: init, driver: driverRef.current };
|
|
||||||
};
|
|
||||||
|
|
||||||
export default useDriver;
|
|
||||||
@@ -1,106 +0,0 @@
|
|||||||
import HotKeys from '@/config/hotkeys';
|
|
||||||
import { useIntl } from '@umijs/max';
|
|
||||||
import { createStyles } from 'antd-style';
|
|
||||||
import { throttle } from 'lodash';
|
|
||||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
||||||
import { useHotkeys } from 'react-hotkeys-hook';
|
|
||||||
|
|
||||||
const useStyles = createStyles(({ css, token }) => ({
|
|
||||||
hintOverlay: css`
|
|
||||||
position: fixed;
|
|
||||||
top: 50%;
|
|
||||||
left: 50%;
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
background: var(--color-esc-hint-bg);
|
|
||||||
color: ${token.colorTextLightSolid};
|
|
||||||
padding: 16px 24px;
|
|
||||||
border-radius: 4px;
|
|
||||||
z-index: 2000;
|
|
||||||
font-size: 14px;
|
|
||||||
pointer-events: none;
|
|
||||||
animation: fadeInOut 2s ease-in-out;
|
|
||||||
@keyframes fadeInOut {
|
|
||||||
0% {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
10% {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
90% {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
}));
|
|
||||||
|
|
||||||
export function useEscHint(options?: {
|
|
||||||
enabled?: boolean;
|
|
||||||
message?: string;
|
|
||||||
throttleDelay?: number;
|
|
||||||
}) {
|
|
||||||
const { enabled = true, message, throttleDelay = 3000 } = options || {};
|
|
||||||
const intl = useIntl();
|
|
||||||
const { styles } = useStyles();
|
|
||||||
const [visible, setVisible] = useState(false);
|
|
||||||
const timeoutRef = useRef<any>(null);
|
|
||||||
const isHintActiveRef = useRef(false);
|
|
||||||
|
|
||||||
const showHintThrottled = useMemo(
|
|
||||||
() =>
|
|
||||||
throttle(
|
|
||||||
() => {
|
|
||||||
if (isHintActiveRef.current) return;
|
|
||||||
|
|
||||||
isHintActiveRef.current = true;
|
|
||||||
|
|
||||||
setVisible(true);
|
|
||||||
|
|
||||||
if (timeoutRef.current) {
|
|
||||||
clearTimeout(timeoutRef.current);
|
|
||||||
}
|
|
||||||
|
|
||||||
timeoutRef.current = setTimeout(() => {
|
|
||||||
setVisible(false);
|
|
||||||
isHintActiveRef.current = false;
|
|
||||||
}, 2000);
|
|
||||||
},
|
|
||||||
throttleDelay,
|
|
||||||
{
|
|
||||||
leading: true,
|
|
||||||
trailing: false
|
|
||||||
}
|
|
||||||
),
|
|
||||||
[throttleDelay]
|
|
||||||
);
|
|
||||||
|
|
||||||
useHotkeys(
|
|
||||||
HotKeys.ESC,
|
|
||||||
() => {
|
|
||||||
if (!enabled) return;
|
|
||||||
showHintThrottled();
|
|
||||||
},
|
|
||||||
{
|
|
||||||
enabled: enabled
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
return () => {
|
|
||||||
if (timeoutRef.current) {
|
|
||||||
clearTimeout(timeoutRef.current);
|
|
||||||
}
|
|
||||||
showHintThrottled.cancel();
|
|
||||||
};
|
|
||||||
}, [showHintThrottled]);
|
|
||||||
|
|
||||||
const EscHint = visible ? (
|
|
||||||
<div className={styles.hintOverlay}>
|
|
||||||
{message || intl.formatMessage({ id: 'common.tips.escape.disable' })}
|
|
||||||
</div>
|
|
||||||
) : null;
|
|
||||||
|
|
||||||
return { EscHint };
|
|
||||||
}
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
import qs from 'query-string';
|
|
||||||
import { useEffect, useRef } from 'react';
|
|
||||||
|
|
||||||
export const createEventSourceURL = (url: string) => {
|
|
||||||
const { host, protocol } = window.location;
|
|
||||||
return `${protocol}://${host}${url}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
/*
|
|
||||||
0: connecting
|
|
||||||
1: connect successfully
|
|
||||||
2: closed
|
|
||||||
*/
|
|
||||||
|
|
||||||
export default function useEventSource() {
|
|
||||||
const eventSourceRef = useRef<any>(null);
|
|
||||||
|
|
||||||
const createEventSourceConnection = (query: {
|
|
||||||
url: string;
|
|
||||||
params: any;
|
|
||||||
onmessage?: (data: any) => void;
|
|
||||||
}) => {
|
|
||||||
eventSourceRef.current?.close?.();
|
|
||||||
|
|
||||||
const { url, params, onmessage = () => {} } = query;
|
|
||||||
|
|
||||||
const sseurl = createEventSourceURL(url);
|
|
||||||
|
|
||||||
eventSourceRef.current = new EventSource(
|
|
||||||
`${url}?${qs.stringify({
|
|
||||||
...params
|
|
||||||
})}`,
|
|
||||||
{
|
|
||||||
withCredentials: true
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
eventSourceRef.current.onmessage = (res: any) => {
|
|
||||||
try {
|
|
||||||
console.log('event source message: ', { res, resData: res });
|
|
||||||
const data = JSON.parse(res.data);
|
|
||||||
onmessage(data);
|
|
||||||
} catch (error) {
|
|
||||||
// error
|
|
||||||
console.log('event source error: ', error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
eventSourceRef.current.onclose = () => {
|
|
||||||
console.log('event source closed...');
|
|
||||||
};
|
|
||||||
|
|
||||||
eventSourceRef.current.onopen = () => {
|
|
||||||
console.log('event source connected...');
|
|
||||||
};
|
|
||||||
|
|
||||||
eventSourceRef.current.onerror = (error: any) => {
|
|
||||||
console.log('event source error: ', error);
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
return () => {
|
|
||||||
eventSourceRef.current?.close?.();
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return {
|
|
||||||
eventSourceRef: eventSourceRef,
|
|
||||||
createEventSourceConnection
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,280 +0,0 @@
|
|||||||
import { useMemoizedFn } from 'ahooks';
|
|
||||||
import { throttle } from 'lodash';
|
|
||||||
import {
|
|
||||||
UseOverlayScrollbarsParams,
|
|
||||||
useOverlayScrollbars
|
|
||||||
} from 'overlayscrollbars-react';
|
|
||||||
import React, { useEffect } from 'react';
|
|
||||||
import useUserSettings from './use-user-settings';
|
|
||||||
|
|
||||||
type OverflowBehavior =
|
|
||||||
| 'hidden'
|
|
||||||
| 'scroll'
|
|
||||||
| 'visible'
|
|
||||||
| 'visible-hidden'
|
|
||||||
| 'visible-scroll';
|
|
||||||
export interface OverlayScrollerOptions {
|
|
||||||
oppositeTheme?: boolean;
|
|
||||||
overflow?: {
|
|
||||||
x?: OverflowBehavior;
|
|
||||||
y?: OverflowBehavior;
|
|
||||||
};
|
|
||||||
scrollbars?: {
|
|
||||||
theme?: 'os-theme-light' | 'os-theme-dark';
|
|
||||||
autoHide?: 'never' | 'scroll' | 'leave' | 'move';
|
|
||||||
autoHideDelay?: number;
|
|
||||||
clickScroll?: boolean | 'instant';
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export const overlaySollerOptions: UseOverlayScrollbarsParams = {
|
|
||||||
options: {
|
|
||||||
update: {
|
|
||||||
debounce: 0
|
|
||||||
},
|
|
||||||
overflow: {
|
|
||||||
x: 'hidden'
|
|
||||||
},
|
|
||||||
scrollbars: {
|
|
||||||
theme: 'os-theme-light',
|
|
||||||
autoHide: 'scroll',
|
|
||||||
autoHideDelay: 600,
|
|
||||||
clickScroll: 'instant'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
defer: true
|
|
||||||
};
|
|
||||||
|
|
||||||
const RESETSCROLLDELAY = 5000;
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param options.theme: if set theme, it will fix the theme
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export default function useOverlayScroller(data?: {
|
|
||||||
options?: OverlayScrollerOptions;
|
|
||||||
events?: any;
|
|
||||||
defer?: boolean;
|
|
||||||
}) {
|
|
||||||
const { userSettings } = useUserSettings();
|
|
||||||
const { options, events, defer = true } = data || {};
|
|
||||||
const { scrollbars, overflow, oppositeTheme } = options || {};
|
|
||||||
const scrollEventElement = React.useRef<any>(null);
|
|
||||||
const instanceRef = React.useRef<any>(null);
|
|
||||||
const initialized = React.useRef(false);
|
|
||||||
const scrollElementRef = React.useRef<any>(null);
|
|
||||||
const stopUpdatePosition = React.useRef(false);
|
|
||||||
const timerRef = React.useRef<any>(null);
|
|
||||||
const [initialize, instance] = useOverlayScrollbars({
|
|
||||||
options: {
|
|
||||||
update: {
|
|
||||||
debounce: 0
|
|
||||||
},
|
|
||||||
overflow: {
|
|
||||||
x: 'hidden',
|
|
||||||
...overflow
|
|
||||||
},
|
|
||||||
scrollbars: {
|
|
||||||
autoHide: 'scroll',
|
|
||||||
autoHideDelay: 600,
|
|
||||||
clickScroll: 'instant',
|
|
||||||
...scrollbars,
|
|
||||||
theme:
|
|
||||||
scrollbars?.theme ||
|
|
||||||
(userSettings.theme === 'light' || !userSettings.theme
|
|
||||||
? 'os-theme-dark'
|
|
||||||
: 'os-theme-light')
|
|
||||||
}
|
|
||||||
},
|
|
||||||
events: {
|
|
||||||
...events
|
|
||||||
},
|
|
||||||
defer: defer
|
|
||||||
});
|
|
||||||
|
|
||||||
instanceRef.current = instance?.();
|
|
||||||
scrollEventElement.current =
|
|
||||||
instanceRef.current?.elements()?.scrollEventElement;
|
|
||||||
|
|
||||||
const handleOnScroll = () => {
|
|
||||||
const scrollTop = scrollEventElement.current?.scrollTop;
|
|
||||||
const scrollHeight = scrollEventElement.current?.scrollHeight;
|
|
||||||
const clientHeight = scrollEventElement.current?.clientHeight;
|
|
||||||
|
|
||||||
const isBottom = scrollTop + clientHeight + 20 >= scrollHeight;
|
|
||||||
|
|
||||||
if (isBottom) {
|
|
||||||
stopUpdatePosition.current = false;
|
|
||||||
} else {
|
|
||||||
stopUpdatePosition.current = true;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const throttledScroll = useMemoizedFn(
|
|
||||||
throttle(() => {
|
|
||||||
scrollEventElement.current?.scrollTo?.({
|
|
||||||
top: scrollEventElement.current?.scrollHeight,
|
|
||||||
behavior: 'smooth'
|
|
||||||
});
|
|
||||||
instanceRef.current?.update?.();
|
|
||||||
}, 100)
|
|
||||||
);
|
|
||||||
|
|
||||||
const scrollauto = useMemoizedFn(() => {
|
|
||||||
scrollEventElement.current?.scrollTo?.({
|
|
||||||
top: scrollEventElement.current.scrollHeight,
|
|
||||||
behavior: 'auto'
|
|
||||||
});
|
|
||||||
instanceRef.current?.update?.();
|
|
||||||
});
|
|
||||||
|
|
||||||
// scroll to bottom
|
|
||||||
const throttledUpdateScrollerPosition = useMemoizedFn((delay?: number) => {
|
|
||||||
if (stopUpdatePosition.current) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (delay === 0) {
|
|
||||||
scrollauto();
|
|
||||||
} else {
|
|
||||||
throttledScroll();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// scroll to top
|
|
||||||
const updateScrollerPositionToTop = useMemoizedFn(() => {
|
|
||||||
scrollEventElement.current?.scrollTo?.({
|
|
||||||
top: 0,
|
|
||||||
behavior: 'auto'
|
|
||||||
});
|
|
||||||
instanceRef.current?.update?.();
|
|
||||||
});
|
|
||||||
|
|
||||||
const generateInstance = () => {
|
|
||||||
instanceRef.current = instance?.();
|
|
||||||
scrollEventElement.current =
|
|
||||||
instanceRef.current?.elements()?.scrollEventElement;
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleWheelCallback = useMemoizedFn((e: any) => {
|
|
||||||
handleOnScroll();
|
|
||||||
if (timerRef.current) {
|
|
||||||
clearTimeout(timerRef.current);
|
|
||||||
}
|
|
||||||
timerRef.current = setTimeout(() => {
|
|
||||||
stopUpdatePosition.current = false;
|
|
||||||
}, RESETSCROLLDELAY);
|
|
||||||
});
|
|
||||||
|
|
||||||
// add wheel event
|
|
||||||
const handleWheelEvent = () => {
|
|
||||||
scrollElementRef.current?.addEventListener?.('wheel', handleWheelCallback, {
|
|
||||||
passive: true
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// remove wheel event
|
|
||||||
const removeWheelEvent = () => {
|
|
||||||
scrollElementRef.current?.removeEventListener?.(
|
|
||||||
'wheel',
|
|
||||||
handleWheelCallback,
|
|
||||||
{ passive: true }
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const createInstance = useMemoizedFn((el: any) => {
|
|
||||||
if (instanceRef.current) {
|
|
||||||
return instanceRef.current;
|
|
||||||
}
|
|
||||||
if (el) {
|
|
||||||
initialize(el);
|
|
||||||
scrollElementRef.current = el;
|
|
||||||
initialized.current = true;
|
|
||||||
instanceRef.current = instance?.();
|
|
||||||
scrollEventElement.current =
|
|
||||||
instanceRef.current?.elements()?.scrollEventElement;
|
|
||||||
handleWheelEvent();
|
|
||||||
}
|
|
||||||
return instanceRef.current;
|
|
||||||
});
|
|
||||||
|
|
||||||
const destroyInstance = () => {
|
|
||||||
instanceRef.current?.destroy?.();
|
|
||||||
removeWheelEvent();
|
|
||||||
instanceRef.current = null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const scrollToTarget = (target: any, offset = 100) => {
|
|
||||||
if (!target) return;
|
|
||||||
if (!instanceRef.current || !scrollEventElement.current) {
|
|
||||||
instanceRef.current = instance?.();
|
|
||||||
scrollEventElement.current =
|
|
||||||
instanceRef.current?.elements()?.scrollEventElement;
|
|
||||||
}
|
|
||||||
|
|
||||||
const viewport = instanceRef.current?.elements().viewport;
|
|
||||||
const containerRect = viewport.getBoundingClientRect();
|
|
||||||
const targetRect = target.getBoundingClientRect();
|
|
||||||
|
|
||||||
const scrollerState = instanceRef.current?.state();
|
|
||||||
|
|
||||||
const currentScroll = scrollerState.current?.overflowAmount?.y;
|
|
||||||
|
|
||||||
// const currentScroll = instanceRef.current?.scroll().position.y;
|
|
||||||
const targetPos = targetRect.top - containerRect.top + currentScroll;
|
|
||||||
|
|
||||||
scrollEventElement.current.scroll({
|
|
||||||
y: targetPos - offset,
|
|
||||||
behavior: 'smooth'
|
|
||||||
});
|
|
||||||
instanceRef.current?.update?.();
|
|
||||||
};
|
|
||||||
|
|
||||||
const getScrollElementScrollableHeight = () => {
|
|
||||||
if (!instanceRef.current || !scrollEventElement.current) {
|
|
||||||
instanceRef.current = instance?.();
|
|
||||||
scrollEventElement.current =
|
|
||||||
instanceRef.current?.elements()?.scrollEventElement;
|
|
||||||
}
|
|
||||||
|
|
||||||
const scrollOffsetElement = instanceRef.current?.elements().viewport;
|
|
||||||
|
|
||||||
return {
|
|
||||||
scrollTop: scrollOffsetElement?.scrollTop,
|
|
||||||
scrollHeight:
|
|
||||||
scrollOffsetElement?.scrollHeight - scrollOffsetElement?.clientHeight
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const getScrollElement = () => {
|
|
||||||
if (!instanceRef.current || !scrollEventElement.current) {
|
|
||||||
instanceRef.current = instance?.();
|
|
||||||
scrollEventElement.current =
|
|
||||||
instanceRef.current?.elements()?.scrollEventElement;
|
|
||||||
}
|
|
||||||
return scrollEventElement;
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
return () => {
|
|
||||||
instanceRef.current?.destroy?.();
|
|
||||||
removeWheelEvent();
|
|
||||||
};
|
|
||||||
}, [instance]);
|
|
||||||
|
|
||||||
return {
|
|
||||||
initialize: createInstance,
|
|
||||||
instance: instanceRef,
|
|
||||||
scrollEventElement: scrollEventElement,
|
|
||||||
initialized: initialized.current,
|
|
||||||
getScrollElementScrollableHeight,
|
|
||||||
getScrollElement,
|
|
||||||
generateInstance,
|
|
||||||
destroyInstance: destroyInstance,
|
|
||||||
updateScrollerPosition: throttledUpdateScrollerPosition,
|
|
||||||
updateScrollerPositionToTop: updateScrollerPositionToTop,
|
|
||||||
scrollToBottom: scrollauto,
|
|
||||||
scrollToTop: updateScrollerPositionToTop,
|
|
||||||
scrollToTarget
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -18,6 +18,7 @@ export function useQueryDataList<
|
|||||||
Response = Array<ListItem>
|
Response = Array<ListItem>
|
||||||
>(option: {
|
>(option: {
|
||||||
key: string;
|
key: string;
|
||||||
|
manual?: boolean;
|
||||||
responseType?: 'array' | 'object';
|
responseType?: 'array' | 'object';
|
||||||
fetchList: (
|
fetchList: (
|
||||||
params: Params,
|
params: Params,
|
||||||
@@ -38,6 +39,7 @@ export function useQueryDataList<
|
|||||||
fetchList,
|
fetchList,
|
||||||
getLabel,
|
getLabel,
|
||||||
getValue,
|
getValue,
|
||||||
|
manual = true,
|
||||||
responseType = 'array',
|
responseType = 'array',
|
||||||
errorMsg
|
errorMsg
|
||||||
} = option;
|
} = option;
|
||||||
@@ -70,7 +72,7 @@ export function useQueryDataList<
|
|||||||
return responseType === 'array' ? res.items || [] : res;
|
return responseType === 'array' ? res.items || [] : res;
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
manual: true,
|
manual: manual,
|
||||||
debounceWait: option.debounceWait || 300,
|
debounceWait: option.debounceWait || 300,
|
||||||
onSuccess: () => {},
|
onSuccess: () => {},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
@@ -105,16 +107,18 @@ export function useQueryDataList<
|
|||||||
export function useQueryData<Detail, Params = any>(option: {
|
export function useQueryData<Detail, Params = any>(option: {
|
||||||
key: string;
|
key: string;
|
||||||
delay?: number;
|
delay?: number;
|
||||||
|
manual?: boolean;
|
||||||
fetchDetail: (params: Params, options?: any) => Promise<Detail>;
|
fetchDetail: (params: Params, options?: any) => Promise<Detail>;
|
||||||
getData?: (response: Detail, params?: any) => any;
|
getData?: (response: Detail, params?: any) => any;
|
||||||
errorMsg?: string;
|
errorMsg?: string;
|
||||||
}): {
|
}): {
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
detailData: Detail;
|
detailData: Detail;
|
||||||
|
manual?: boolean;
|
||||||
cancelRequest: () => void;
|
cancelRequest: () => void;
|
||||||
fetchData: (params: Params, extra?: any) => Promise<Detail>;
|
fetchData: (params: Params, extra?: any) => Promise<Detail>;
|
||||||
} {
|
} {
|
||||||
const { key, fetchDetail, getData, errorMsg, delay } = option;
|
const { key, fetchDetail, getData, errorMsg, delay, manual = true } = option;
|
||||||
const axiosTokenRef = useRef<CancelTokenSource | null>(null);
|
const axiosTokenRef = useRef<CancelTokenSource | null>(null);
|
||||||
const [detailData, setDetailData] = useState<Detail>({} as Detail);
|
const [detailData, setDetailData] = useState<Detail>({} as Detail);
|
||||||
|
|
||||||
@@ -142,7 +146,7 @@ export function useQueryData<Detail, Params = any>(option: {
|
|||||||
return res;
|
return res;
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
manual: true,
|
manual: manual,
|
||||||
onSuccess: () => {},
|
onSuccess: () => {},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
message.error(
|
message.error(
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import { useMemoizedFn } from 'ahooks';
|
||||||
|
import { useRef, useState } from 'react';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Centralizes the anti-double-submit + anti-deadlock logic shared by the
|
||||||
|
* "add/edit" modals.
|
||||||
|
*
|
||||||
|
* The lock has to straddle antd's async field validation, whose result comes
|
||||||
|
* back through two separate callbacks (`onFinish` / `onFinishFailed`). So the
|
||||||
|
* three wiring points below are intentional and map 1:1 to those anchors:
|
||||||
|
*
|
||||||
|
* - `guard` wrap the submit trigger (button / ModalFooter onOk). Blocks
|
||||||
|
* re-entry while a submit is already in flight, then fires submit.
|
||||||
|
* - `run` wrap the `onFinish` handler. Holds the lock + loading until the
|
||||||
|
* `onOk` request settles (success or error).
|
||||||
|
* - `release` pass as the form's `onFinishFailed`. Releases the lock when
|
||||||
|
* validation fails, otherwise the button would dead-lock.
|
||||||
|
*/
|
||||||
|
export default function useSubmitLock() {
|
||||||
|
const [loading, setLoading] = useState<boolean>(false);
|
||||||
|
const lockRef = useRef<boolean>(false);
|
||||||
|
|
||||||
|
const release = useMemoizedFn(() => {
|
||||||
|
setLoading(false);
|
||||||
|
lockRef.current = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
const guard = useMemoizedFn((submit: () => void) => {
|
||||||
|
if (lockRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
lockRef.current = true;
|
||||||
|
submit();
|
||||||
|
});
|
||||||
|
|
||||||
|
const run = useMemoizedFn(async (task: () => void | Promise<void>) => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
await task();
|
||||||
|
} finally {
|
||||||
|
release();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { loading, guard, run, release };
|
||||||
|
}
|
||||||
@@ -37,6 +37,7 @@ export default function useTableFetch<T>(
|
|||||||
key?: (typeof PaginationKey)[keyof typeof PaginationKey];
|
key?: (typeof PaginationKey)[keyof typeof PaginationKey];
|
||||||
fetchAPI: (params: any, options?: any) => Promise<Global.PageResponse<T>>;
|
fetchAPI: (params: any, options?: any) => Promise<Global.PageResponse<T>>;
|
||||||
deleteAPI?: (id: number, params?: any) => Promise<any>;
|
deleteAPI?: (id: number, params?: any) => Promise<any>;
|
||||||
|
afterDelete?: (id?: number | number[]) => void;
|
||||||
contentForDelete?: string;
|
contentForDelete?: string;
|
||||||
defaultData?: any[];
|
defaultData?: any[];
|
||||||
events?: EventsType[];
|
events?: EventsType[];
|
||||||
@@ -48,6 +49,7 @@ export default function useTableFetch<T>(
|
|||||||
const {
|
const {
|
||||||
fetchAPI,
|
fetchAPI,
|
||||||
deleteAPI,
|
deleteAPI,
|
||||||
|
afterDelete,
|
||||||
contentForDelete,
|
contentForDelete,
|
||||||
API,
|
API,
|
||||||
polling = false,
|
polling = false,
|
||||||
@@ -363,7 +365,7 @@ export default function useTableFetch<T>(
|
|||||||
|
|
||||||
// remove the deleted id from selected ids in row selection
|
// remove the deleted id from selected ids in row selection
|
||||||
rowSelection.removeSelectedKeys([row.id]);
|
rowSelection.removeSelectedKeys([row.id]);
|
||||||
|
afterDelete?.(row.id);
|
||||||
// ======== to avoid fetch data twice, because of debounceFetchData has been run =======
|
// ======== to avoid fetch data twice, because of debounceFetchData has been run =======
|
||||||
if (!updateManually) {
|
if (!updateManually) {
|
||||||
fetchData();
|
fetchData();
|
||||||
@@ -390,6 +392,7 @@ export default function useTableFetch<T>(
|
|||||||
successIds.push(id);
|
successIds.push(id);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
afterDelete?.(successIds);
|
||||||
rowSelection.removeSelectedKeys(successIds);
|
rowSelection.removeSelectedKeys(successIds);
|
||||||
fetchData();
|
fetchData();
|
||||||
return res;
|
return res;
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ type EventsType = 'CREATE' | 'UPDATE' | 'DELETE' | 'INSERT';
|
|||||||
export function useUpdateChunkedList(options: {
|
export function useUpdateChunkedList(options: {
|
||||||
events?: EventsType[];
|
events?: EventsType[];
|
||||||
dataList?: any[];
|
dataList?: any[];
|
||||||
triggerAt?: React.MutableRefObject<number>;
|
|
||||||
limit?: number;
|
limit?: number;
|
||||||
onCreate?: (args: any) => void;
|
onCreate?: (args: any) => void;
|
||||||
onUpdate?: (args: any) => void;
|
onUpdate?: (args: any) => void;
|
||||||
@@ -24,9 +23,9 @@ export function useUpdateChunkedList(options: {
|
|||||||
filterFun?: (args: any) => boolean;
|
filterFun?: (args: any) => boolean;
|
||||||
mapFun?: (args: any) => any;
|
mapFun?: (args: any) => any;
|
||||||
computedID?: (d: object) => string;
|
computedID?: (d: object) => string;
|
||||||
|
isNewItem?: (item: any) => boolean;
|
||||||
}) {
|
}) {
|
||||||
const { events = ['CREATE', 'DELETE', 'UPDATE', 'INSERT'], triggerAt } =
|
const { events = ['CREATE', 'DELETE', 'UPDATE', 'INSERT'] } = options;
|
||||||
options;
|
|
||||||
const deletedIdsRef = useRef<Set<number | string>>(new Set());
|
const deletedIdsRef = useRef<Set<number | string>>(new Set());
|
||||||
const cacheDataListRef = useRef<any[]>(options.dataList || []);
|
const cacheDataListRef = useRef<any[]>(options.dataList || []);
|
||||||
const timerRef = useRef<any>(null);
|
const timerRef = useRef<any>(null);
|
||||||
@@ -71,17 +70,14 @@ export function useUpdateChunkedList(options: {
|
|||||||
(sItem: any) => sItem.id === item.id
|
(sItem: any) => sItem.id === item.id
|
||||||
);
|
);
|
||||||
const updateItem = { ...item };
|
const updateItem = { ...item };
|
||||||
if (updateIndex === -1 && !triggerAt?.current) {
|
if (updateIndex === -1) {
|
||||||
acc.push(updateItem);
|
acc.push(updateItem);
|
||||||
} else if (!triggerAt?.current) {
|
} else {
|
||||||
cacheDataListRef.current[updateIndex] = updateItem;
|
cacheDataListRef.current[updateIndex] = updateItem;
|
||||||
}
|
}
|
||||||
|
// only push items created after the watch started
|
||||||
|
|
||||||
// TODO: only push items created after triggerAt
|
if (options.isNewItem?.(item)) {
|
||||||
if (
|
|
||||||
triggerAt?.current &&
|
|
||||||
Date.parse(item.created_at) >= triggerAt.current
|
|
||||||
) {
|
|
||||||
latestCreateList.push(updateItem);
|
latestCreateList.push(updateItem);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,6 +89,10 @@ export function useUpdateChunkedList(options: {
|
|||||||
...cacheDataListRef.current
|
...cacheDataListRef.current
|
||||||
].slice(0, limit);
|
].slice(0, limit);
|
||||||
|
|
||||||
|
options.setDataList?.([...cacheDataListRef.current], {
|
||||||
|
createdIds: newDataList.map((item) => item.id)
|
||||||
|
});
|
||||||
|
|
||||||
options.onCreate?.(latestCreateList);
|
options.onCreate?.(latestCreateList);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,11 +103,9 @@ export function useUpdateChunkedList(options: {
|
|||||||
cacheDataListRef.current = cacheDataListRef.current?.filter(
|
cacheDataListRef.current = cacheDataListRef.current?.filter(
|
||||||
(item: any) => {
|
(item: any) => {
|
||||||
// collect deleted items
|
// collect deleted items
|
||||||
if (triggerAt?.current) {
|
if (ids?.includes(item.id) && !options.isNewItem?.(item)) {
|
||||||
if (ids?.includes(item.id)) {
|
|
||||||
deletedList.push(item);
|
deletedList.push(item);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return !ids?.includes(item.id);
|
return !ids?.includes(item.id);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -133,12 +131,10 @@ export function useUpdateChunkedList(options: {
|
|||||||
updateItem,
|
updateItem,
|
||||||
...cacheDataListRef.current.slice(0, limit - 1)
|
...cacheDataListRef.current.slice(0, limit - 1)
|
||||||
];
|
];
|
||||||
if (options.onUpdate && triggerAt?.current) {
|
if (options.onUpdate && options.isNewItem?.(item)) {
|
||||||
if (Date.parse(item.created_at) >= triggerAt.current) {
|
|
||||||
options.onUpdate?.([updateItem]);
|
options.onUpdate?.([updateItem]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -63,8 +63,7 @@ export default function useWatchList<T = Record<string, any>>(API: string) {
|
|||||||
listRequestTokenRef.current?.cancel?.();
|
listRequestTokenRef.current?.cancel?.();
|
||||||
listRequestTokenRef.current = createAxiosToken();
|
listRequestTokenRef.current = createAxiosToken();
|
||||||
const params = {
|
const params = {
|
||||||
page: 1,
|
page: -1
|
||||||
perPage: 100
|
|
||||||
};
|
};
|
||||||
const res: any = await queryAllDataList(params, {
|
const res: any = await queryAllDataList(params, {
|
||||||
token: listRequestTokenRef.current.token
|
token: listRequestTokenRef.current.token
|
||||||
@@ -86,6 +85,7 @@ export default function useWatchList<T = Record<string, any>>(API: string) {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
watchDataList,
|
watchDataList,
|
||||||
|
setWatchDataList,
|
||||||
deleteItemFromCache: handleDeleteItemFromCache
|
deleteItemFromCache: handleDeleteItemFromCache
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import { useAtom } from 'jotai';
|
|||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import { DEFAULT_ENTER_PAGE } from '../config/settings';
|
import { DEFAULT_ENTER_PAGE } from '../config/settings';
|
||||||
|
import GithubStar from './github-star';
|
||||||
|
|
||||||
const NewLabel = styled.span`
|
const NewLabel = styled.span`
|
||||||
position: relative;
|
position: relative;
|
||||||
@@ -210,14 +211,14 @@ export const ExtraContent = (props: { isDarkTheme?: boolean }) => {
|
|||||||
key: 'settings',
|
key: 'settings',
|
||||||
label: (
|
label: (
|
||||||
<span className="flex flex-center">
|
<span className="flex flex-center">
|
||||||
<IconFont type="icon-settings-02" />
|
<IconFont type="icon-preferences" />
|
||||||
<span className="m-l-8" style={{ marginLeft: 8 }}>
|
<span className="m-l-8" style={{ marginLeft: 8 }}>
|
||||||
{intl?.formatMessage?.({ id: 'common.button.settings' })}
|
{intl?.formatMessage?.({ id: 'common.preferences' })}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
),
|
),
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
history.push('/profile');
|
history.push('/preferences');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -260,6 +261,7 @@ export const ExtraContent = (props: { isDarkTheme?: boolean }) => {
|
|||||||
<Wrapper>
|
<Wrapper>
|
||||||
{contextHolder}
|
{contextHolder}
|
||||||
<PluginExtraField name="OrgSwitcher" isDarkTheme={isDarkTheme} />
|
<PluginExtraField name="OrgSwitcher" isDarkTheme={isDarkTheme} />
|
||||||
|
{process.env.ENABLE_ENTERPRISE !== 'true' && <GithubStar />}
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
|
|||||||
@@ -0,0 +1,140 @@
|
|||||||
|
import externalLinks from '@/constants/external-links';
|
||||||
|
import { GithubFilled } from '@ant-design/icons';
|
||||||
|
import { useIntl } from '@umijs/max';
|
||||||
|
import { Tooltip } from 'antd';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
const REPO = 'gpustack/gpustack';
|
||||||
|
const CACHE_KEY = 'gpustack:github-stars';
|
||||||
|
const CACHE_TTL = 24 * 60 * 60 * 1000;
|
||||||
|
const FETCH_TIMEOUT = 4000;
|
||||||
|
|
||||||
|
const StarLink = styled.a`
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: stretch;
|
||||||
|
height: 24px;
|
||||||
|
border-radius: var(--ant-border-radius);
|
||||||
|
border: 1px solid var(--ant-color-border-secondary);
|
||||||
|
background-color: var(--ant-color-bg-container);
|
||||||
|
color: var(--ant-color-text-secondary);
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
transition:
|
||||||
|
border-color 0.2s,
|
||||||
|
color 0.2s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: var(--ant-color-border);
|
||||||
|
color: var(--ant-color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.seg {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 0 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.seg + .seg {
|
||||||
|
border-left: 1px solid var(--ant-color-border-secondary);
|
||||||
|
background-color: var(--ant-color-fill-quaternary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.anticon {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.count {
|
||||||
|
font-weight: 500;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
min-width: 1.5em;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const formatCount = (n: number): string => {
|
||||||
|
if (n >= 1000) {
|
||||||
|
const k = n / 1000;
|
||||||
|
return k >= 10 ? `${Math.round(k)}k` : `${k.toFixed(1)}k`;
|
||||||
|
}
|
||||||
|
return String(n);
|
||||||
|
};
|
||||||
|
|
||||||
|
type CacheEntry = { value: number; time: number };
|
||||||
|
|
||||||
|
const readCache = (): CacheEntry | null => {
|
||||||
|
try {
|
||||||
|
const raw = localStorage.getItem(CACHE_KEY);
|
||||||
|
if (!raw) return null;
|
||||||
|
const parsed = JSON.parse(raw);
|
||||||
|
if (typeof parsed?.value !== 'number' || typeof parsed?.time !== 'number') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return parsed;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const writeCache = (value: number) => {
|
||||||
|
try {
|
||||||
|
localStorage.setItem(
|
||||||
|
CACHE_KEY,
|
||||||
|
JSON.stringify({ value, time: Date.now() })
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
// ignore quota errors
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const GithubStar = () => {
|
||||||
|
const intl = useIntl();
|
||||||
|
const [count, setCount] = useState<number | null>(
|
||||||
|
() => readCache()?.value ?? null
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const cached = readCache();
|
||||||
|
const fresh = cached && Date.now() - cached.time < CACHE_TTL;
|
||||||
|
if (fresh) return;
|
||||||
|
|
||||||
|
const controller = new AbortController();
|
||||||
|
const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT);
|
||||||
|
|
||||||
|
fetch(`https://api.github.com/repos/${REPO}`, { signal: controller.signal })
|
||||||
|
.then((r) => (r.ok ? r.json() : null))
|
||||||
|
.then((data) => {
|
||||||
|
if (!data || typeof data.stargazers_count !== 'number') return;
|
||||||
|
setCount(data.stargazers_count);
|
||||||
|
writeCache(data.stargazers_count);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
// offline, blocked, rate-limited — stay hidden if no cache
|
||||||
|
})
|
||||||
|
.finally(() => clearTimeout(timer));
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearTimeout(timer);
|
||||||
|
controller.abort();
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip title={intl.formatMessage({ id: 'common.github.star.tooltip' })}>
|
||||||
|
<StarLink href={externalLinks.github} target="_blank" rel="noreferrer">
|
||||||
|
<span className="seg">
|
||||||
|
<GithubFilled />
|
||||||
|
</span>
|
||||||
|
<span className="seg">
|
||||||
|
<span className="count">
|
||||||
|
{count != null ? formatCount(count) : 'Star'}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</StarLink>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default GithubStar;
|
||||||