Compare commits

...
49 Commits
Author SHA1 Message Date
jialinandjialin 3a1565690d fix(worker): incorrect worker version 2026-07-06 11:58:51 +08:00
jialinandjialin 1bb9fd73db chore(template): theme logo 2026-07-06 11:58:51 +08:00
jialinandjialin 39024b1d46 chore(hooks): remove unused hooks 2026-07-06 11:58:51 +08:00
hibigandjialin 4ecaa0a326 chore: bump @gpustack/core-ui to v1.0.35 2026-07-02 13:23:14 +08:00
jialinandjialin e1af27e11c fix(dashboard): remove date from group_by 2026-07-02 13:18:04 +08:00
jialinandjialin f4ebd8d7d1 chore: remove old code 2026-07-02 13:18:04 +08:00
jialinandjialin bd58a911d5 chore: remove driver.js 2026-07-02 13:18:04 +08:00
jialinandjialin 5321973ebe perf(charts): import chart components from @gpustack/core-ui/charts
Switch all echarts-based chart imports to the dedicated core-ui charts
entry so echarts stays out of the synchronous entry bundle and loads
only on chart routes. Requires @gpustack/core-ui with the ./charts export.
2026-07-01 20:54:26 +08:00
jialinandjialin 0a1f875590 perf(markdown): load katex css lazily via local FullMarkdown wrapper
core-ui no longer bundles katex.min.css (it would base64-inline ~1.4MB
of fonts into the render-blocking global css). Wrap FullMarkdown locally
to co-locate the katex stylesheet so it loads only in the routes that
render math.
2026-07-01 20:54:26 +08:00
hibigandjialin ea2c9870a7 chore: bump @gpustack/core-ui to v1.0.34 2026-07-01 20:48:55 +08:00
jialinandjialin 23f0e4398a fix(pageContainer): remove leftContent, rightContent, props 2026-07-01 19:11:47 +08:00
jialinandjialin b342174fc2 feat(cluster-detail): add cluster switcher; consume core-ui header-slot
page-box now imports the header-slot bridge from core-ui and re-exports it, plus a dev warning when a header slot has multiple owners. cluster-detail breadcrumb becomes a BaseSelect to switch clusters, and the tab tables are keyed by id so they refetch on switch.
2026-07-01 19:11:47 +08:00
jialinandjialin d07d3b2c99 refactor(layout): adopt header-slot page container across pages 2026-07-01 19:11:47 +08:00
hibigandjialin 9d0a5d01b0 chore: bump @gpustack/core-ui to v1.0.33 2026-07-01 19:08:52 +08:00
gitlawrandjialin 1e97d29fca feat(login): surface SSO callback failures via error query param
The CAS / OIDC / SAML callbacks now redirect to `/login?error=<code>`
on failure instead of letting the browser land on a raw JSON error
page, so the actionable copy reaches the user. Two codes are
recognised:

* `source_conflict` — incoming SSO username collides with an existing
  account from a different source. Message points the user at an
  administrator to link or convert.
* `auth_failed` — anything else (bad ticket, expired state, IdP
  unreachable, malformed response). Generic message: try again or
  contact the administrator.

On mount the login form picks up the `?error=` query param, maps it
through a small `messageIdByCode` table to an i18n key, and routes
the result through the existing auth-error toast. Unknown codes are
silently ignored so a future server release adding a code doesn't
render a bare key. The query param is cleared via
`history.replaceState` so a refresh doesn't re-fire the toast.

Strings added to all five locales.
2026-07-01 15:39:10 +08:00
gitlawrandjialin 350f398cde feat(users): add authentication source dropdown to the user form
The add / edit user drawer now exposes a Source select (Local / OIDC /
SAML / CAS) so an admin can flip an existing account between Local
password and an external IdP without touching the database. Mirrors
the matching `PUT /v1/users/{id}` change on the backend.

Password field follows the selected source:

* Hidden when source != Local — those users authenticate via the IdP
  and a local password row would be a /login bypass.
* Required when CREATE-with-Local, or when EDIT is switching an SSO
  user back to Local (the backend rejects SSO -> Local without a
  fresh password to avoid locking the user out of /login).
* Optional when editing an already-Local user, matching today's
  behaviour.

A switch in EDIT mode surfaces a tip explaining the side effect
(password cleared / new password required) so the consequence isn't
hidden. The Source select is disabled on self-edit — same guard the
role column already uses — so an admin can't lock themselves out by
flipping their own row to an external source.

Strings are added to all five locales; the IdP protocol acronyms
(OIDC / SAML / CAS) render verbatim and don't need translation keys.
2026-07-01 15:39:10 +08:00
jialinandjialin 8a15172316 fix(provider): table sorting does not work 2026-07-01 11:42:30 +08:00
jialinandjialin 185e3475e7 feat: add model icon in my models 2026-07-01 11:42:30 +08:00
jialinandjialin 64720ccbfa chore: press enter key to submit while focus on password 2026-07-01 11:42:30 +08:00
jialinandjialin db17192e55 fix(usage): carry full filter set in breakdown tables and fix double fetch
- breakdown sub-tables now send all active filters (route/user/api_key), matching the trend chart
- summary tab filters the token trend by user and unions user options from both meta APIs (deduped by id)
- stabilize the filters reference so meta load no longer retriggers a second fetch on mount
2026-06-30 17:02:17 +08:00
gitlawr a1dd1ec861 ci: trigger backend pack on UI merge
After build-publish uploads the UI tarball to COS, dispatch a
repository_dispatch event (ui-built) to gpustack/gpustack so the
backend repack picks up the new UI.

Skipped for tag pushes — release tags drive the backend pack via
its own tag trigger. Requires a DISPATCH_PAT secret with
actions:write on gpustack/gpustack.
2026-06-30 15:57:33 +08:00
jialinandjialin a516e6ce72 feat(dashboard): split top user token usage into prompt/completion 2026-06-30 14:38:35 +08:00
micheliaandjialin 2e375c6603 fix(usage): label per-instance rows by shape, not an undefined title
The Instances-tab Instance Type column passed isCpu ? cpuOnlyLabel : undefined.
For a GPU row with vram but a missing/zero gpu_count, undefined let
renderInstanceType fall back to "CPU Only" (acceleratable=false). Pass
instanceTypeSeriesLabel(row) directly — consistent with the Instance Types
column and robust to that edge. (PR #1245 review)
2026-06-30 14:11:39 +08:00
micheliaandjialin 7d94c77c15 fix(usage): sum same-label buckets in the trend series
buildTrendSeries overwrote a (label, bucket) value on collision. Should two
backend groups ever map to the same display label, their values must add up
rather than letting the last write win. Accumulate instead.
2026-06-30 14:11:39 +08:00
micheliaandjialin 457d2f2f72 feat(usage): render instance types by actual shape (CPU spec / GPU x cards)
Instance Types are grouped by actual shape now, so each row/series is one
concrete type. Label them accordingly via a shared instanceTypeSeriesLabel,
used by both the table column and the trend legend so they read identically:
GPU shows "<product> x <cards>", CPU shows "CPU Only · N vCPU · M GB" (the
instance's real size, not the bare "CPU Only" + hover tooltip). The Instances
tab also uses the per-instance cpu/mem totals, so a 3c6g instance of a 1c2g
flavor reads 3c6g instead of 1c2g. flatten carries cpu_milli/memory_mib/
gpu_count from the breakdown dimensions.
2026-06-30 14:11:39 +08:00
gitlawrandjialin 5e7d83e5dd fix(api-keys): let platform admin create a non-tenant-pinned API key
The CreateOrgScopeField slot only renders a "Global" choice when the
host form passes ``allowGlobal: true``. Without it, an admin in the
"All Orgs" view sees only Personal + every org in the dropdown — and
the form's submit injects an ``X-Organization-Id`` header for every
choice, pinning the new key to a tenant. The matching backend change
(api_keys.owner_principal_id nullable) only kicks in when no header
is sent.

Pass ``allowGlobal: true`` (default-selects Global for admin) plus
``globalLabelId: 'scope.global'`` so the dropdown option reads as
plain "Global" — matching the existing tag / column labels for the
same concept elsewhere in the UI, instead of the longer
inference-backend "shared with all organizations" copy.

Relax the ListItem type so ``owner_principal_id`` may be ``null`` —
the OrganizationCell / OwnerScopeTag renderers already render the
"Global" placeholder for null/undefined; the type was the only thing
still claiming the column was always present.
2026-06-30 11:54:00 +08:00
jialinandjialin b1cffe047c docs: expand CLAUDE.md conventions (naming, types, config, core-ui)
- Clarify styles rule: avoid styled-components in new code; existing
  usage is legacy debt, not migrated wholesale
- Add naming conventions (module structure, file naming)
- Add config & types section (FormData/ListItem, config/index.ts)
- Expand Common components with frequently-reused core-ui inventory
2026-06-29 19:44:28 +08:00
jialinandjialin 6509f2a4ff docs: consolidate component conventions into CLAUDE.md and skill
- Add "Dynamic add-item form fields" guidance to CLAUDE.md (pick
  component by field schema: LabelSelector / ListInput / MetadataList)
- Fold StatusTag status-display recipe into create-crud-page skill
- Remove DESIGN.md (content migrated to the above)
2026-06-29 19:44:28 +08:00
jialinandjialin f719c11606 refactor(perferences): use center wrapper layout 2026-06-29 19:44:28 +08:00
jialinandjialin b8d7873b77 chore: dedupe agent instruction files into CLAUDE.md 2026-06-29 12:10:18 +08:00
jialinandjialin a9248a98a5 chore: add create-crud-page and form-patterns skills, refresh CLAUDE.md 2026-06-29 12:10:18 +08:00
micheliaandjialin 8f85e9a082 fix(usage): use frontend gpu_type token for instance-type legend
flattenItem receives the unmapped frontend dimension; the instance-type
grouping is 'gpu_type' (mapped to backend 'instance_type' via GROUP_BY_MAP),
so the previous 'instance_type' check never matched and the trend legend kept
showing the raw flavor slug. Check 'gpu_type'. (PR #1240 review)
2026-06-27 22:27:42 +08:00
micheliaandjialin 1a6d1654b2 fix(usage): label instance_type trend series by product name
The grouped-trend legend used the raw group key, which for instance_type is
the flavor slug (e.g. gpustack--generic-ln-x64-40c-160g--nvidia-a100-80g-4d).
Prefer the enriched product name so the chart legend matches the GPU Instances
list (#5700); falls back to the slug for legacy rows without dimension data.
2026-06-27 22:27:42 +08:00
micheliaandjialin abe705c034 fix(usage): fetch full trend/export series via page=-1
Trend charts and breakdown exports need the complete date series. The old
perPage=10000 workaround (and the missing page param on the token trend,
which fell back to perPage=20) dropped low-traffic — often most recent —
buckets, leaving gaps in the charts (#5690).

Send page=-1 (the backend's no-pagination sentinel) for the token trend,
the storage/instances trend charts and table exports, the resource export
dialog, and the summary-tab date trends.
2026-06-27 22:27:42 +08:00
jialinandjialin 8b34f68824 feat(cluster): resource topology entrance 2026-06-26 22:14:37 +08:00
jialinandjialin e37828a2cd fix: resource topology ux 2026-06-26 22:14:37 +08:00
jialinandjialin 4709661f93 fix(tts): reset form fields to initialvalues after model changed 2026-06-25 14:34:15 +08:00
jialinandjialin 1d7543f19c chore: adjust colors generator 2026-06-25 14:34:15 +08:00
jialinandjialin 1cf146ca32 fix(usage): dynamic bar color value 2026-06-24 20:54:16 +08:00
jialinandjialin b05c510776 fix: add warning tips when deleting ssh key 2026-06-24 20:54:16 +08:00
jialinandjialin e79d5f8962 fix(usage): selection maxTagCount 2026-06-24 20:54:16 +08:00
hibigandjialin 5503fdc21e chore: bump @gpustack/core-ui to v1.0.32 2026-06-24 19:00:22 +08:00
gitlawrandjialin 36a1038d12 feat(login): render SSO button data-driven from /auth/config
The backend now advertises the active external auth provider on
``/auth/config`` as a single ``external_auth: {type, login_url} | null``
field (replacing the per-provider ``is_oidc`` / ``is_saml`` booleans).
This is the API needed to add CAS without per-provider UI conditionals.

Wire the login UI accordingly:

- ``useSSOAuth`` exposes a single ``loginWithExternalAuth()`` action and
  ``options.external_auth`` carrying the provider info; the OIDC- and
  SAML-specific exports are gone.
- ``LoginForm`` renders one SSO button whenever ``external_auth`` is
  set, navigating to ``login_url``. New providers (CAS, future LDAP /
  Azure AD / …) need zero UI changes — only a backend route.
- ``LocalUserForm`` and ``LoginKit`` type definitions drop the
  per-provider booleans.
2026-06-24 18:59:19 +08:00
jialinandjialin ee42a9d0ed refactor(components): migrate shared components to core-ui and remove unused ones 2026-06-24 14:02:41 +08:00
jialinandjialin 28976dea4b fix(cardpage): duplicate data appears after clicking refresh 2026-06-24 14:02:41 +08:00
jialinandjialin 22e5cc3aa5 fix(usage): display usage tabs base on access permissions 2026-06-24 11:26:40 +08:00
jialinandjialin 3052c0b23e fix(app): run probe resources at two stages 2026-06-24 11:26:40 +08:00
jialinandjialin 1c9cabf7e7 fix(usage): instance type check conditions 2026-06-22 15:25:18 +08:00
jialinandjialin 03842d1741 fix: gpu selector height 2026-06-22 15:25:18 +08:00
205 changed files with 2673 additions and 3803 deletions
+143
View File
@@ -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.
+103
View File
@@ -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`.
+18
View File
@@ -1,6 +1,7 @@
name: CI
on:
workflow_dispatch: {}
push:
branches:
- 'main'
@@ -117,3 +118,20 @@ jobs:
remote_path: releases/${{ steps.version.outputs.version }}.tar.gz
accelerate: true
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 }}"
}
+1 -1
View File
@@ -13,6 +13,6 @@
.swc
.DS_Store
.idea
.claude
.claude/settings.local.json
/dist.zip
.cache
-265
View File
@@ -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 });
};
```
+5
View File
@@ -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
+140
View File
@@ -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`.
-46
View File
@@ -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.
+1 -2
View File
@@ -17,7 +17,7 @@
"@ant-design/pro-components": "3.1.0-0",
"@antv/g6": "^5.0.51",
"@braintree/sanitize-url": "^7.1.1",
"@gpustack/core-ui": "^1.0.27",
"@gpustack/core-ui": "^1.0.35",
"@huggingface/gguf": "^0.1.7",
"@huggingface/hub": "^0.15.1",
"@huggingface/tasks": "^0.11.6",
@@ -39,7 +39,6 @@
"culori": "^4.0.2",
"dayjs": "^1.11.11",
"dompurify": "^3.2.6",
"driver.js": "^1.3.1",
"echarts": "^5.5.1",
"file-saver": "^2.0.5",
"has-ansi": "^5.0.1",
+117 -125
View File
@@ -24,8 +24,8 @@ importers:
specifier: ^7.1.1
version: 7.1.2
'@gpustack/core-ui':
specifier: ^1.0.27
version: 1.0.27(czdvzceysqw7iv6pct2ucnb23e)
specifier: ^1.0.35
version: 1.0.35(czdvzceysqw7iv6pct2ucnb23e)
'@huggingface/gguf':
specifier: ^0.1.7
version: 0.1.18
@@ -49,7 +49,7 @@ importers:
version: 4.17.24
'@umijs/max':
specifier: ^4.6.15
version: 4.6.51(@babel/core@7.29.0)(@types/node@25.6.2)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(lightningcss@1.22.1)(prettier@3.8.3)(rc-field-form@2.7.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.29.0)(react@18.3.1))(terser@5.48.0)(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14))
version: 4.6.51(@babel/core@7.23.6)(@types/node@25.6.2)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(lightningcss@1.22.1)(prettier@3.8.3)(rc-field-form@2.7.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.23.6)(react@18.3.1))(terser@5.48.0)(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14))
'@xterm/addon-fit':
specifier: ^0.10.0
version: 0.10.0(@xterm/xterm@5.5.0)
@@ -89,9 +89,6 @@ importers:
dompurify:
specifier: ^3.2.6
version: 3.4.2
driver.js:
specifier: ^1.3.1
version: 1.4.0
echarts:
specifier: ^5.5.1
version: 5.6.0
@@ -109,7 +106,7 @@ importers:
version: 3.3.0
jotai:
specifier: ^2.8.4
version: 2.20.0(@babel/core@7.29.0)(@babel/template@7.28.6)(@types/react@18.3.28)(react@18.3.1)
version: 2.20.0(@babel/core@7.23.6)(@babel/template@7.28.6)(@types/react@18.3.28)(react@18.3.1)
js-yaml:
specifier: ^4.1.0
version: 4.1.1
@@ -205,7 +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)
umi-presets-pro:
specifier: ^2.0.3
version: 2.0.3(@babel/core@7.29.0)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(antd@6.3.7(date-fns@2.30.0)(moment@2.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(chokidar@3.6.0)(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(encoding@0.1.13)(rc-field-form@2.7.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3)(umi@4.6.51(@babel/core@7.29.0)(@types/node@25.6.2)(@types/react@18.3.28)(eslint@9.39.4(jiti@2.7.0))(lightningcss@1.22.1)(prettier@3.8.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.29.0)(react@18.3.1))(stylelint@14.8.2)(terser@5.48.0)(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14)))
version: 2.0.3(@babel/core@7.23.6)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(antd@6.3.7(date-fns@2.30.0)(moment@2.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(chokidar@3.6.0)(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(encoding@0.1.13)(rc-field-form@2.7.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3)(umi@4.6.51(@babel/core@7.23.6)(@types/node@25.6.2)(@types/react@18.3.28)(eslint@9.39.4(jiti@2.7.0))(lightningcss@1.22.1)(prettier@3.8.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.23.6)(react@18.3.1))(stylelint@14.8.2)(terser@5.48.0)(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14)))
wavesurfer.js:
specifier: ^7.8.8
version: 7.12.6
@@ -233,10 +230,10 @@ importers:
version: 1.0.1
'@umijs/plugins':
specifier: ^4.4.11
version: 4.6.51(@babel/core@7.29.0)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(antd@6.3.7(date-fns@2.30.0)(moment@2.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(rc-field-form@2.7.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
version: 4.6.51(@babel/core@7.23.6)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(antd@6.3.7(date-fns@2.30.0)(moment@2.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(rc-field-form@2.7.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
babel-plugin-named-asset-import:
specifier: ^0.3.8
version: 0.3.8(@babel/core@7.29.0)
version: 0.3.8(@babel/core@7.23.6)
case-sensitive-paths-webpack-plugin:
specifier: ^2.4.0
version: 2.4.0
@@ -1484,24 +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}
deprecated: the package is rather renamed to @formatjs/ecma-abstract with some changes in functionality (primarily selectUnit is removed and we don't plan to make any further changes to this package
'@gpustack/core-ui@1.0.27':
resolution: {integrity: sha512-m3ue0EHFKULla0mpnpxZwn9LVaGKS+HnuzQYSBECQa4vaP8MEEQsR7oFBtG8bhWUWYA9VzP6k7fjozDkP6TymA==, tarball: https://registry.npmjs.org/@gpustack/core-ui/-/core-ui-1.0.27.tgz}
'@gpustack/core-ui@1.0.35':
resolution: {integrity: sha512-MaEmCM3FikeKZdUgDSyUtIfXtNMbNOLeFiTynl6DcKEraUOpk6tgQXPDginBY3uXuisG4hzoIQbpHHkzaYiQew==, tarball: https://registry.npmjs.org/@gpustack/core-ui/-/core-ui-1.0.35.tgz}
peerDependencies:
'@ant-design/icons': '>=6.0.0'
'@ant-design/icons': ^6.1.0
'@ant-design/pro-components': 3.1.0-0
'@monaco-editor/react': ^4.6.0
ahooks: '>=3.0.0'
antd: '>=6.0.0'
antd-style: '>=3.0.0'
axios: '>=1.8.0'
echarts: '>=5.0.0'
ahooks: ^3.8.5
antd: ^6.3.3
antd-style: ^3.6.2
axios: ^1.8.2
echarts: ^5.5.1
file-saver: ^2.0.5
monaco-editor: ^0.30.1
monaco-yaml: ^4.0.0
overlayscrollbars-react: ^0.5.6
react: '>=18.0.0'
react-dom: '>=18.0.0'
styled-components: '>=6.0.0'
react: ^18.2.0
react-dom: ^18.2.0
styled-components: ^6.1.15
'@hono/node-server@1.19.14':
resolution: {integrity: sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==, tarball: https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.14.tgz}
@@ -4320,9 +4317,6 @@ packages:
dot-case@3.0.4:
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:
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
react: 18.3.1
'@alita/plugins@3.5.5(@babel/core@7.29.0)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(antd@6.3.7(date-fns@2.30.0)(moment@2.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(rc-field-form@2.7.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
'@alita/plugins@3.5.5(@babel/core@7.23.6)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(antd@6.3.7(date-fns@2.30.0)(moment@2.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(rc-field-form@2.7.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@alita/babel-transform-jsx-class': 0.0.2
'@alita/inspx': 0.0.2(react@18.3.1)
'@alita/request': 3.1.2
'@alita/types': 3.1.2
'@umijs/bundler-utils': 4.4.11
'@umijs/plugins': 4.4.11(@babel/core@7.29.0)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(antd@6.3.7(date-fns@2.30.0)(moment@2.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(rc-field-form@2.7.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@umijs/plugins': 4.4.11(@babel/core@7.23.6)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(antd@6.3.7(date-fns@2.30.0)(moment@2.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(rc-field-form@2.7.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@umijs/utils': 4.4.11
ahooks: 3.9.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
antd-mobile-alita: 2.3.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@@ -10134,90 +10128,90 @@ snapshots:
dependencies:
'@babel/types': 7.29.0
'@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.29.0)':
'@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.23.6)':
dependencies:
'@babel/core': 7.29.0
'@babel/core': 7.23.6
'@babel/helper-plugin-utils': 7.28.6
'@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.29.0)':
'@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.23.6)':
dependencies:
'@babel/core': 7.29.0
'@babel/core': 7.23.6
'@babel/helper-plugin-utils': 7.28.6
'@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.29.0)':
'@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.23.6)':
dependencies:
'@babel/core': 7.29.0
'@babel/core': 7.23.6
'@babel/helper-plugin-utils': 7.28.6
'@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.29.0)':
'@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.23.6)':
dependencies:
'@babel/core': 7.29.0
'@babel/core': 7.23.6
'@babel/helper-plugin-utils': 7.28.6
'@babel/plugin-syntax-import-attributes@7.28.6(@babel/core@7.29.0)':
'@babel/plugin-syntax-import-attributes@7.28.6(@babel/core@7.23.6)':
dependencies:
'@babel/core': 7.29.0
'@babel/core': 7.23.6
'@babel/helper-plugin-utils': 7.28.6
'@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.29.0)':
'@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.23.6)':
dependencies:
'@babel/core': 7.29.0
'@babel/core': 7.23.6
'@babel/helper-plugin-utils': 7.28.6
'@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.29.0)':
'@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.23.6)':
dependencies:
'@babel/core': 7.29.0
'@babel/core': 7.23.6
'@babel/helper-plugin-utils': 7.28.6
'@babel/plugin-syntax-jsx@7.28.6(@babel/core@7.29.0)':
'@babel/plugin-syntax-jsx@7.28.6(@babel/core@7.23.6)':
dependencies:
'@babel/core': 7.29.0
'@babel/core': 7.23.6
'@babel/helper-plugin-utils': 7.28.6
'@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.29.0)':
'@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.23.6)':
dependencies:
'@babel/core': 7.29.0
'@babel/core': 7.23.6
'@babel/helper-plugin-utils': 7.28.6
'@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.29.0)':
'@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.23.6)':
dependencies:
'@babel/core': 7.29.0
'@babel/core': 7.23.6
'@babel/helper-plugin-utils': 7.28.6
'@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.29.0)':
'@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.23.6)':
dependencies:
'@babel/core': 7.29.0
'@babel/core': 7.23.6
'@babel/helper-plugin-utils': 7.28.6
'@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.29.0)':
'@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.23.6)':
dependencies:
'@babel/core': 7.29.0
'@babel/core': 7.23.6
'@babel/helper-plugin-utils': 7.28.6
'@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.29.0)':
'@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.23.6)':
dependencies:
'@babel/core': 7.29.0
'@babel/core': 7.23.6
'@babel/helper-plugin-utils': 7.28.6
'@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.29.0)':
'@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.23.6)':
dependencies:
'@babel/core': 7.29.0
'@babel/core': 7.23.6
'@babel/helper-plugin-utils': 7.28.6
'@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.29.0)':
'@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.23.6)':
dependencies:
'@babel/core': 7.29.0
'@babel/core': 7.23.6
'@babel/helper-plugin-utils': 7.28.6
'@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.29.0)':
'@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.23.6)':
dependencies:
'@babel/core': 7.29.0
'@babel/core': 7.23.6
'@babel/helper-plugin-utils': 7.28.6
'@babel/plugin-transform-modules-commonjs@7.23.3(@babel/core@7.29.0)':
'@babel/plugin-transform-modules-commonjs@7.23.3(@babel/core@7.23.6)':
dependencies:
'@babel/core': 7.29.0
'@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0)
'@babel/core': 7.23.6
'@babel/helper-module-transforms': 7.28.6(@babel/core@7.23.6)
'@babel/helper-plugin-utils': 7.28.6
'@babel/helper-simple-access': 7.27.1
transitivePeerDependencies:
@@ -10808,7 +10802,7 @@ snapshots:
'@formatjs/intl-utils@2.3.0': {}
'@gpustack/core-ui@1.0.27(czdvzceysqw7iv6pct2ucnb23e)':
'@gpustack/core-ui@1.0.35(czdvzceysqw7iv6pct2ucnb23e)':
dependencies:
'@ant-design/icons': 6.2.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@ant-design/pro-components': 3.1.0-0(antd@6.3.7(date-fns@2.30.0)(moment@2.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@@ -12281,11 +12275,11 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@umijs/bundler-utoopack@4.6.51(styled-jsx@5.1.7(@babel/core@7.29.0)(react@18.3.1))(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14))':
'@umijs/bundler-utoopack@4.6.51(styled-jsx@5.1.7(@babel/core@7.23.6)(react@18.3.1))(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14))':
dependencies:
'@umijs/bundler-utils': 4.6.51
'@umijs/bundler-webpack': 4.6.51(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14))
'@utoo/pack': 1.4.3(less-loader@11.1.0(less@4.1.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14)))(less@4.1.3)(postcss@8.5.14)(resolve-url-loader@5.0.0)(sass-loader@13.2.0(sass@1.54.0)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14)))(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.29.0)(react@18.3.1))
'@utoo/pack': 1.4.3(less-loader@11.1.0(less@4.1.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14)))(less@4.1.3)(postcss@8.5.14)(resolve-url-loader@5.0.0)(sass-loader@13.2.0(sass@1.54.0)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14)))(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.23.6)(react@18.3.1))
compression: 1.8.1
connect-history-api-fallback: 2.0.0
cors: 2.8.6
@@ -12553,14 +12547,14 @@ snapshots:
- supports-color
- typescript
'@umijs/max@4.6.51(@babel/core@7.29.0)(@types/node@25.6.2)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(lightningcss@1.22.1)(prettier@3.8.3)(rc-field-form@2.7.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.29.0)(react@18.3.1))(terser@5.48.0)(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14))':
'@umijs/max@4.6.51(@babel/core@7.23.6)(@types/node@25.6.2)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(lightningcss@1.22.1)(prettier@3.8.3)(rc-field-form@2.7.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.23.6)(react@18.3.1))(terser@5.48.0)(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14))':
dependencies:
'@umijs/lint': 4.6.51(eslint@8.35.0)(stylelint@14.8.2)(typescript@5.9.3)
'@umijs/plugins': 4.6.51(@babel/core@7.29.0)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(antd@4.24.16(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(rc-field-form@2.7.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@umijs/plugins': 4.6.51(@babel/core@7.23.6)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(antd@4.24.16(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(rc-field-form@2.7.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
antd: 4.24.16(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
eslint: 8.35.0
stylelint: 14.8.2
umi: 4.6.51(@babel/core@7.29.0)(@types/node@25.6.2)(@types/react@18.3.28)(eslint@8.35.0)(lightningcss@1.22.1)(prettier@3.8.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.29.0)(react@18.3.1))(stylelint@14.8.2)(terser@5.48.0)(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14))
umi: 4.6.51(@babel/core@7.23.6)(@types/node@25.6.2)(@types/react@18.3.28)(eslint@8.35.0)(lightningcss@1.22.1)(prettier@3.8.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.23.6)(react@18.3.1))(stylelint@14.8.2)(terser@5.48.0)(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14))
transitivePeerDependencies:
- '@babel/core'
- '@rspack/core'
@@ -12642,7 +12636,7 @@ snapshots:
dependencies:
tsx: 3.12.2
'@umijs/plugins@4.4.11(@babel/core@7.29.0)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(antd@6.3.7(date-fns@2.30.0)(moment@2.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(rc-field-form@2.7.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
'@umijs/plugins@4.4.11(@babel/core@7.23.6)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(antd@6.3.7(date-fns@2.30.0)(moment@2.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(rc-field-form@2.7.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@ahooksjs/use-request': 2.8.15(react@18.3.1)
'@ant-design/antd-theme-variable': 1.0.0
@@ -12657,7 +12651,7 @@ snapshots:
antd-dayjs-webpack-plugin: 1.0.6(dayjs@1.11.20)
axios: 0.27.2
babel-plugin-import: 1.13.8
babel-plugin-styled-components: 2.1.4(@babel/core@7.29.0)(styled-components@6.1.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))
babel-plugin-styled-components: 2.1.4(@babel/core@7.23.6)(styled-components@6.1.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))
dayjs: 1.11.20
dva-core: 2.0.4(redux@4.2.1)
dva-immer: 1.0.2(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))
@@ -12687,7 +12681,7 @@ snapshots:
- react-native
- supports-color
'@umijs/plugins@4.6.51(@babel/core@7.29.0)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(antd@4.24.16(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(rc-field-form@2.7.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
'@umijs/plugins@4.6.51(@babel/core@7.23.6)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(antd@4.24.16(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(rc-field-form@2.7.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@ahooksjs/use-request': 2.8.15(react@18.3.1)
'@ant-design/antd-theme-variable': 1.0.0
@@ -12702,7 +12696,7 @@ snapshots:
antd-dayjs-webpack-plugin: 1.0.6(dayjs@1.11.20)
axios: 0.27.2
babel-plugin-import: 1.13.8
babel-plugin-styled-components: 2.1.4(@babel/core@7.29.0)(styled-components@6.1.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))
babel-plugin-styled-components: 2.1.4(@babel/core@7.23.6)(styled-components@6.1.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))
dayjs: 1.11.20
dva-core: 2.0.4(redux@4.2.1)
dva-immer: 1.0.2(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))
@@ -12732,7 +12726,7 @@ snapshots:
- react-native
- supports-color
'@umijs/plugins@4.6.51(@babel/core@7.29.0)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(antd@6.3.7(date-fns@2.30.0)(moment@2.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(rc-field-form@2.7.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
'@umijs/plugins@4.6.51(@babel/core@7.23.6)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(antd@6.3.7(date-fns@2.30.0)(moment@2.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(rc-field-form@2.7.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@ahooksjs/use-request': 2.8.15(react@18.3.1)
'@ant-design/antd-theme-variable': 1.0.0
@@ -12747,7 +12741,7 @@ snapshots:
antd-dayjs-webpack-plugin: 1.0.6(dayjs@1.11.20)
axios: 0.27.2
babel-plugin-import: 1.13.8
babel-plugin-styled-components: 2.1.4(@babel/core@7.29.0)(styled-components@6.1.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))
babel-plugin-styled-components: 2.1.4(@babel/core@7.23.6)(styled-components@6.1.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))
dayjs: 1.11.20
dva-core: 2.0.4(redux@4.2.1)
dva-immer: 1.0.2(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))
@@ -12777,7 +12771,7 @@ snapshots:
- react-native
- supports-color
'@umijs/preset-umi@4.6.51(@types/node@25.6.2)(@types/react@18.3.28)(lightningcss@1.22.1)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.29.0)(react@18.3.1))(terser@5.48.0)(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14))':
'@umijs/preset-umi@4.6.51(@types/node@25.6.2)(@types/react@18.3.28)(lightningcss@1.22.1)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.23.6)(react@18.3.1))(terser@5.48.0)(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14))':
dependencies:
'@iconify/utils': 2.1.1
'@stagewise/toolbar': 0.6.2
@@ -12787,7 +12781,7 @@ snapshots:
'@umijs/bundler-esbuild': 4.6.51
'@umijs/bundler-mako': 0.11.10(postcss@8.5.14)(sass@1.54.0)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14))
'@umijs/bundler-utils': 4.6.51
'@umijs/bundler-utoopack': 4.6.51(styled-jsx@5.1.7(@babel/core@7.29.0)(react@18.3.1))(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14))
'@umijs/bundler-utoopack': 4.6.51(styled-jsx@5.1.7(@babel/core@7.23.6)(react@18.3.1))(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14))
'@umijs/bundler-vite': 4.6.51(@types/node@25.6.2)(lightningcss@1.22.1)(postcss@8.5.14)(rollup@3.30.0)(sass@1.54.0)(terser@5.48.0)
'@umijs/bundler-webpack': 4.6.51(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14))
'@umijs/core': 4.6.51
@@ -12870,13 +12864,13 @@ snapshots:
react-helmet-async: 1.3.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
react-router-dom: 6.3.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@umijs/request-record@1.1.4(umi@4.6.51(@babel/core@7.29.0)(@types/node@25.6.2)(@types/react@18.3.28)(eslint@9.39.4(jiti@2.7.0))(lightningcss@1.22.1)(prettier@3.8.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.29.0)(react@18.3.1))(stylelint@14.8.2)(terser@5.48.0)(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14)))':
'@umijs/request-record@1.1.4(umi@4.6.51(@babel/core@7.23.6)(@types/node@25.6.2)(@types/react@18.3.28)(eslint@9.39.4(jiti@2.7.0))(lightningcss@1.22.1)(prettier@3.8.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.23.6)(react@18.3.1))(stylelint@14.8.2)(terser@5.48.0)(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14)))':
dependencies:
chokidar: 3.6.0
express: 4.22.1
lodash: 4.18.1
prettier: 2.8.8
umi: 4.6.51(@babel/core@7.29.0)(@types/node@25.6.2)(@types/react@18.3.28)(eslint@9.39.4(jiti@2.7.0))(lightningcss@1.22.1)(prettier@3.8.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.29.0)(react@18.3.1))(stylelint@14.8.2)(terser@5.48.0)(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14))
umi: 4.6.51(@babel/core@7.23.6)(@types/node@25.6.2)(@types/react@18.3.28)(eslint@9.39.4(jiti@2.7.0))(lightningcss@1.22.1)(prettier@3.8.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.23.6)(react@18.3.1))(stylelint@14.8.2)(terser@5.48.0)(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14))
transitivePeerDependencies:
- supports-color
@@ -12892,13 +12886,13 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@umijs/test@4.6.51(@babel/core@7.29.0)':
'@umijs/test@4.6.51(@babel/core@7.23.6)':
dependencies:
'@babel/plugin-transform-modules-commonjs': 7.23.3(@babel/core@7.29.0)
'@babel/plugin-transform-modules-commonjs': 7.23.3(@babel/core@7.23.6)
'@jest/types': 27.5.1
'@umijs/bundler-utils': 4.6.51
'@umijs/utils': 4.6.51
babel-jest: 29.7.0(@babel/core@7.29.0)
babel-jest: 29.7.0(@babel/core@7.23.6)
esbuild: 0.21.4
identity-obj-proxy: 3.0.0
isomorphic-unfetch: 4.0.2
@@ -13018,7 +13012,7 @@ snapshots:
'@utoo/pack-win32-x64-msvc@1.4.3':
optional: true
'@utoo/pack@1.4.3(less-loader@11.1.0(less@4.1.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14)))(less@4.1.3)(postcss@8.5.14)(resolve-url-loader@5.0.0)(sass-loader@13.2.0(sass@1.54.0)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14)))(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.29.0)(react@18.3.1))':
'@utoo/pack@1.4.3(less-loader@11.1.0(less@4.1.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14)))(less@4.1.3)(postcss@8.5.14)(resolve-url-loader@5.0.0)(sass-loader@13.2.0(sass@1.54.0)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14)))(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.23.6)(react@18.3.1))':
dependencies:
'@babel/code-frame': 7.22.5
'@hono/node-server': 1.19.14(hono@4.12.18)
@@ -13039,7 +13033,7 @@ snapshots:
sass-loader: 13.2.0(sass@1.54.0)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14))
semver: 7.8.0
send: 0.17.1
styled-jsx: 5.1.7(@babel/core@7.29.0)(react@18.3.1)
styled-jsx: 5.1.7(@babel/core@7.23.6)(react@18.3.1)
ws: 8.20.0
optionalDependencies:
'@utoo/pack-darwin-arm64': 1.4.3
@@ -13555,13 +13549,13 @@ snapshots:
transitivePeerDependencies:
- debug
babel-jest@29.7.0(@babel/core@7.29.0):
babel-jest@29.7.0(@babel/core@7.23.6):
dependencies:
'@babel/core': 7.29.0
'@babel/core': 7.23.6
'@jest/transform': 29.7.0
'@types/babel__core': 7.20.5
babel-plugin-istanbul: 6.1.1
babel-preset-jest: 29.6.3(@babel/core@7.29.0)
babel-preset-jest: 29.6.3(@babel/core@7.23.6)
chalk: 4.1.2
graceful-fs: 4.2.11
slash: 3.0.0
@@ -13601,9 +13595,9 @@ snapshots:
cosmiconfig: 7.1.0
resolve: 1.22.12
babel-plugin-named-asset-import@0.3.8(@babel/core@7.29.0):
babel-plugin-named-asset-import@0.3.8(@babel/core@7.23.6):
dependencies:
'@babel/core': 7.29.0
'@babel/core': 7.23.6
babel-plugin-react-compiler@0.0.0-experimental-c23de8d-20240515:
dependencies:
@@ -13615,11 +13609,11 @@ snapshots:
zod: 3.25.76
zod-validation-error: 2.1.0(zod@3.25.76)
babel-plugin-styled-components@2.1.4(@babel/core@7.29.0)(styled-components@6.1.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)):
babel-plugin-styled-components@2.1.4(@babel/core@7.23.6)(styled-components@6.1.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)):
dependencies:
'@babel/helper-annotate-as-pure': 7.27.3
'@babel/helper-module-imports': 7.28.6
'@babel/plugin-syntax-jsx': 7.28.6(@babel/core@7.29.0)
'@babel/plugin-syntax-jsx': 7.28.6(@babel/core@7.23.6)
lodash: 4.18.1
picomatch: 2.3.2
styled-components: 6.1.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@@ -13627,30 +13621,30 @@ snapshots:
- '@babel/core'
- supports-color
babel-preset-current-node-syntax@1.2.0(@babel/core@7.29.0):
babel-preset-current-node-syntax@1.2.0(@babel/core@7.23.6):
dependencies:
'@babel/core': 7.29.0
'@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.29.0)
'@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.29.0)
'@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.29.0)
'@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.29.0)
'@babel/plugin-syntax-import-attributes': 7.28.6(@babel/core@7.29.0)
'@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.29.0)
'@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.29.0)
'@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.29.0)
'@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.29.0)
'@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.29.0)
'@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.29.0)
'@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.29.0)
'@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.29.0)
'@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.29.0)
'@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.29.0)
'@babel/core': 7.23.6
'@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.23.6)
'@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.23.6)
'@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.23.6)
'@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.23.6)
'@babel/plugin-syntax-import-attributes': 7.28.6(@babel/core@7.23.6)
'@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.23.6)
'@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.23.6)
'@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.23.6)
'@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.23.6)
'@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.23.6)
'@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.23.6)
'@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.23.6)
'@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.23.6)
'@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.23.6)
'@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.23.6)
babel-preset-jest@29.6.3(@babel/core@7.29.0):
babel-preset-jest@29.6.3(@babel/core@7.23.6):
dependencies:
'@babel/core': 7.29.0
'@babel/core': 7.23.6
babel-plugin-jest-hoist: 29.6.3
babel-preset-current-node-syntax: 1.2.0(@babel/core@7.29.0)
babel-preset-current-node-syntax: 1.2.0(@babel/core@7.23.6)
babel-runtime-jsx-plus@0.1.5: {}
@@ -14554,8 +14548,6 @@ snapshots:
no-case: 3.0.4
tslib: 2.8.1
driver.js@1.4.0: {}
duck@0.1.12:
dependencies:
underscore: 1.13.8
@@ -16410,9 +16402,9 @@ snapshots:
jiti@2.7.0: {}
jotai@2.20.0(@babel/core@7.29.0)(@babel/template@7.28.6)(@types/react@18.3.28)(react@18.3.1):
jotai@2.20.0(@babel/core@7.23.6)(@babel/template@7.28.6)(@types/react@18.3.28)(react@18.3.1):
optionalDependencies:
'@babel/core': 7.29.0
'@babel/core': 7.23.6
'@babel/template': 7.28.6
'@types/react': 18.3.28
react: 18.3.1
@@ -19805,12 +19797,12 @@ snapshots:
css-to-react-native: 3.2.0
react-dom: 18.3.1(react@18.3.1)
styled-jsx@5.1.7(@babel/core@7.29.0)(react@18.3.1):
styled-jsx@5.1.7(@babel/core@7.23.6)(react@18.3.1):
dependencies:
client-only: 0.0.1
react: 18.3.1
optionalDependencies:
'@babel/core': 7.29.0
'@babel/core': 7.23.6
stylelint-config-recommended@7.0.0(stylelint@14.8.2):
dependencies:
@@ -20164,12 +20156,12 @@ snapshots:
ua-parser-js@0.7.41: {}
umi-presets-pro@2.0.3(@babel/core@7.29.0)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(antd@6.3.7(date-fns@2.30.0)(moment@2.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(chokidar@3.6.0)(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(encoding@0.1.13)(rc-field-form@2.7.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3)(umi@4.6.51(@babel/core@7.29.0)(@types/node@25.6.2)(@types/react@18.3.28)(eslint@9.39.4(jiti@2.7.0))(lightningcss@1.22.1)(prettier@3.8.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.29.0)(react@18.3.1))(stylelint@14.8.2)(terser@5.48.0)(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14))):
umi-presets-pro@2.0.3(@babel/core@7.23.6)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(antd@6.3.7(date-fns@2.30.0)(moment@2.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(chokidar@3.6.0)(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(encoding@0.1.13)(rc-field-form@2.7.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3)(umi@4.6.51(@babel/core@7.23.6)(@types/node@25.6.2)(@types/react@18.3.28)(eslint@9.39.4(jiti@2.7.0))(lightningcss@1.22.1)(prettier@3.8.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.23.6)(react@18.3.1))(stylelint@14.8.2)(terser@5.48.0)(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14))):
dependencies:
'@alita/plugins': 3.5.5(@babel/core@7.29.0)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(antd@6.3.7(date-fns@2.30.0)(moment@2.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(rc-field-form@2.7.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@alita/plugins': 3.5.5(@babel/core@7.23.6)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(antd@6.3.7(date-fns@2.30.0)(moment@2.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(rc-field-form@2.7.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@umijs/max-plugin-openapi': 2.0.3(chokidar@3.6.0)(encoding@0.1.13)(typescript@5.9.3)
'@umijs/plugins': 4.6.51(@babel/core@7.29.0)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(antd@6.3.7(date-fns@2.30.0)(moment@2.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(rc-field-form@2.7.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@umijs/request-record': 1.1.4(umi@4.6.51(@babel/core@7.29.0)(@types/node@25.6.2)(@types/react@18.3.28)(eslint@9.39.4(jiti@2.7.0))(lightningcss@1.22.1)(prettier@3.8.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.29.0)(react@18.3.1))(stylelint@14.8.2)(terser@5.48.0)(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14)))
'@umijs/plugins': 4.6.51(@babel/core@7.23.6)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(antd@6.3.7(date-fns@2.30.0)(moment@2.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(rc-field-form@2.7.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@umijs/request-record': 1.1.4(umi@4.6.51(@babel/core@7.23.6)(@types/node@25.6.2)(@types/react@18.3.28)(eslint@9.39.4(jiti@2.7.0))(lightningcss@1.22.1)(prettier@3.8.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.23.6)(react@18.3.1))(stylelint@14.8.2)(terser@5.48.0)(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14)))
swagger-ui-dist: 4.19.1
transitivePeerDependencies:
- '@babel/core'
@@ -20193,17 +20185,17 @@ snapshots:
isomorphic-fetch: 2.2.1
qs: 6.15.1
umi@4.6.51(@babel/core@7.29.0)(@types/node@25.6.2)(@types/react@18.3.28)(eslint@8.35.0)(lightningcss@1.22.1)(prettier@3.8.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.29.0)(react@18.3.1))(stylelint@14.8.2)(terser@5.48.0)(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14)):
umi@4.6.51(@babel/core@7.23.6)(@types/node@25.6.2)(@types/react@18.3.28)(eslint@8.35.0)(lightningcss@1.22.1)(prettier@3.8.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.23.6)(react@18.3.1))(stylelint@14.8.2)(terser@5.48.0)(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14)):
dependencies:
'@babel/runtime': 7.23.6
'@umijs/bundler-utils': 4.6.51
'@umijs/bundler-webpack': 4.6.51(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14))
'@umijs/core': 4.6.51
'@umijs/lint': 4.6.51(eslint@8.35.0)(stylelint@14.8.2)(typescript@5.9.3)
'@umijs/preset-umi': 4.6.51(@types/node@25.6.2)(@types/react@18.3.28)(lightningcss@1.22.1)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.29.0)(react@18.3.1))(terser@5.48.0)(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14))
'@umijs/preset-umi': 4.6.51(@types/node@25.6.2)(@types/react@18.3.28)(lightningcss@1.22.1)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.23.6)(react@18.3.1))(terser@5.48.0)(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14))
'@umijs/renderer-react': 4.6.51(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@umijs/server': 4.6.51
'@umijs/test': 4.6.51(@babel/core@7.29.0)
'@umijs/test': 4.6.51(@babel/core@7.23.6)
'@umijs/utils': 4.6.51
prettier-plugin-organize-imports: 3.2.4(prettier@3.8.3)(typescript@5.9.3)
prettier-plugin-packagejson: 2.4.3(prettier@3.8.3)
@@ -20247,17 +20239,17 @@ snapshots:
- webpack-hot-middleware
- webpack-plugin-serve
umi@4.6.51(@babel/core@7.29.0)(@types/node@25.6.2)(@types/react@18.3.28)(eslint@9.39.4(jiti@2.7.0))(lightningcss@1.22.1)(prettier@3.8.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.29.0)(react@18.3.1))(stylelint@14.8.2)(terser@5.48.0)(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14)):
umi@4.6.51(@babel/core@7.23.6)(@types/node@25.6.2)(@types/react@18.3.28)(eslint@9.39.4(jiti@2.7.0))(lightningcss@1.22.1)(prettier@3.8.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.23.6)(react@18.3.1))(stylelint@14.8.2)(terser@5.48.0)(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14)):
dependencies:
'@babel/runtime': 7.23.6
'@umijs/bundler-utils': 4.6.51
'@umijs/bundler-webpack': 4.6.51(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14))
'@umijs/core': 4.6.51
'@umijs/lint': 4.6.51(eslint@9.39.4(jiti@2.7.0))(stylelint@14.8.2)(typescript@5.9.3)
'@umijs/preset-umi': 4.6.51(@types/node@25.6.2)(@types/react@18.3.28)(lightningcss@1.22.1)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.29.0)(react@18.3.1))(terser@5.48.0)(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14))
'@umijs/preset-umi': 4.6.51(@types/node@25.6.2)(@types/react@18.3.28)(lightningcss@1.22.1)(rollup@3.30.0)(sass@1.54.0)(styled-jsx@5.1.7(@babel/core@7.23.6)(react@18.3.1))(terser@5.48.0)(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.106.2(lightningcss@1.22.1)(postcss@8.5.14))
'@umijs/renderer-react': 4.6.51(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@umijs/server': 4.6.51
'@umijs/test': 4.6.51(@babel/core@7.29.0)
'@umijs/test': 4.6.51(@babel/core@7.23.6)
'@umijs/utils': 4.6.51
prettier-plugin-organize-imports: 3.2.4(prettier@3.8.3)(typescript@5.9.3)
prettier-plugin-packagejson: 2.4.3(prettier@3.8.3)
+15 -83
View File
@@ -3,9 +3,6 @@ import { GPUStackVersionAtom, UpdateCheckAtom, userAtom } from '@/atoms/user';
import { setAtomStorage } from '@/atoms/utils';
import { DEFAULT_ENTER_PAGE, GPUSTACK_API_BASE_URL } from '@/config/settings';
import { COLOR_PRIMARY } from '@/config/theme/constants';
import { queryClusterList } from '@/pages/cluster-management/apis';
import { ProviderValueMap } from '@/pages/cluster-management/config';
import { queryResourceEvents } from '@/pages/usage/apis/resource';
import { getGPUStackPlugin } from '@/plugins';
import { enterprisePluginReady } from '@/plugins/enterprise-ready';
import { GPUStackPluginManager } from '@/plugins/manager';
@@ -17,6 +14,10 @@ import {
} from '@/services/profile/apis';
import { fetchSystemConfig } from '@/services/system/query-system-config';
import { isOnline } from '@/utils';
import {
markInitialStateProbed,
probeAccessFlags
} from '@/utils/access-probes';
import { installTenantFetch } from '@/utils/install-fetch';
import {
IS_FIRST_LOGIN,
@@ -41,80 +42,6 @@ const checkDefaultPage = async (userInfo: any) => {
}
};
// Probes the caller's cluster list once so access predicates can gate
// GPU Service (Kubernetes-only). Cheap (one list request) and never
// blocks login — any failure just falls back to `undefined`, which
// the predicate treats as "unknown / don't restrict beyond role".
// The result is also mirrored into sessionStorage so access extensions
// that run without the initialState argument can read it (e.g. to
// override the admin shortcut in scopes where the menu shouldn't
// show even for admins).
const HAS_K8S_CLUSTER_KEY = 'hasKubernetesCluster';
const probeHasKubernetesCluster = async (): Promise<boolean | undefined> => {
try {
const res = await queryClusterList(
{ page: -1 },
{
skipErrorHandler: true
}
);
const value = (res?.items ?? []).some(
(c) => c?.provider === ProviderValueMap.Kubernetes
);
try {
window.sessionStorage.setItem(HAS_K8S_CLUSTER_KEY, JSON.stringify(value));
} catch {
// sessionStorage may be unavailable (Safari private mode); the
// access predicate already handles a missing value as "unknown".
}
return value;
} catch (error) {
console.error('probeHasKubernetesCluster error', error);
try {
window.sessionStorage.removeItem(HAS_K8S_CLUSTER_KEY);
} catch {
// ignore
}
return undefined;
}
};
// Probes whether the caller has ANY resource-usage events (GPU/CPU instance or
// storage lifecycle). Used alongside the cluster probe so a user who has run
// GPU instances still sees GPU Service / the full Usage page even if they
// currently have no Kubernetes cluster. Mirrored into sessionStorage for the
// access extensions; any failure → undefined ("unknown — don't restrict").
const HAS_RESOURCE_EVENTS_KEY = 'hasResourceEvents';
const probeHasResourceEvents = async (): Promise<boolean | undefined> => {
try {
// No date range = "ever"; scope is clamped to the caller server-side.
const res = await queryResourceEvents(
{ perPage: 1 },
{
skipErrorHandler: true
}
);
const value = (res?.pagination?.total ?? 0) > 0;
try {
window.sessionStorage.setItem(
HAS_RESOURCE_EVENTS_KEY,
JSON.stringify(value)
);
} catch {
// sessionStorage may be unavailable; predicate treats missing as unknown.
}
return value;
} catch (error) {
console.error('probeHasResourceEvents error', error);
try {
window.sessionStorage.removeItem(HAS_RESOURCE_EVENTS_KEY);
} catch {
// ignore
}
return undefined;
}
};
// runtime configuration
export async function getInitialState(): Promise<{
fetchUserInfo: () => Promise<Global.UserInfo>;
@@ -235,19 +162,24 @@ export async function getInitialState(): Promise<{
getAppVersionInfo();
if (![DEFAULT_ENTER_PAGE.login].includes(location.pathname)) {
const [userInfo, hasKubernetesCluster, hasResourceEvents] =
await Promise.all([
const [userInfo, accessFlags] = await Promise.all([
fetchUserInfo(),
probeHasKubernetesCluster(),
probeHasResourceEvents()
probeAccessFlags()
]);
// Record that the probes ran for an authenticated user this page load
// (the refresh path) so the layout doesn't re-probe. A failed
// fetch (empty user — e.g. unauthenticated deep link that bounces to
// login) is NOT marked: the user will log in via SPA afterwards and
// the layout becomes responsible for probing.
if (userInfo?.username) {
markInitialStateProbed();
}
checkDefaultPage(userInfo);
return {
fetchUserInfo,
currentUser: userInfo,
pluginData,
hasKubernetesCluster,
hasResourceEvents
...accessFlags
};
}
return {

Before

Width:  |  Height:  |  Size: 7.0 KiB

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 196 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 640 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 801 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

+12
View File
@@ -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;
+1
View File
@@ -28,6 +28,7 @@ export default {
rowSelectedBg: 'transparent',
headerSortActiveBg: 'transparent',
headerSortHoverBg: 'transparent',
bodySortBg: 'transparent',
headerBg: 'none'
},
Button: {
+1
View File
@@ -31,6 +31,7 @@ export default {
rowSelectedBg: 'transparent',
headerSortActiveBg: 'transparent',
headerSortHoverBg: 'transparent',
bodySortBg: 'transparent',
headerSplitColor: '#e8e8e8',
headerBg: 'none'
},
+10 -2
View File
@@ -638,10 +638,15 @@ body {
inset: 53px auto auto 0 !important;
border-radius: 0 0 var(--border-radius-base) var(--border-radius-base);
border-top: none;
max-height: 240px;
min-height: 100px;
.ant-cascader-menus {
align-items: stretch;
}
&.gpu-selector {
.ant-cascader-menu {
max-height: 400px;
height: unset;
}
.ant-cascader-menu:last-child {
flex: auto;
@@ -846,3 +851,6 @@ body {
padding-right: 8px;
padding-block: 8px;
}
.ant-select-multiple .ant-select-content {
height: 100%;
}
-10
View File
@@ -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)
}));
}
-58
View File
@@ -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;
-18
View File
@@ -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 };
};
-56
View File
@@ -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
};
}
+114 -36
View File
@@ -1,54 +1,132 @@
import useMemoizedFn from 'ahooks/lib/useMemoizedFn';
import { converter, modeHsl, modeRgb, useMode } from 'culori/fn';
import { clampChroma, formatHex, modeOklch, modeRgb, useMode } from 'culori/fn';
import useUserSettings from './use-user-settings';
const toHsl = converter('hsl');
const DEFAULT_BRAND_HUE = 211; // brand color
const COOL_HUE_START = 180;
const COOL_HUE_END = 280;
// Linear-style minimal/neutral palette: a TIGHT blue -> violet band in OKLCH
// (perceptually uniform), kept moderate-chroma so fills read as clean and gentle
// rather than candy-colored. We deliberately stay narrow and DON'T fan out to
// green/magenta — in this aesthetic series are separated by LIGHTNESS, not by
// spreading across the hue wheel. Every series (including the first) is generated
// from this one ramp, so the whole palette stays in a single cohesive color
// family — no special high-saturation brand color that clashes with the rest.
const COOL_HUE_START = 250; // blue
const COOL_HUE_END = 315; // violet (stops short of magenta/pink, stays neutral-cool)
const COOL_HUE_RANGE = COOL_HUE_END - COOL_HUE_START;
const GOLDEN_RATIO_CONJUGATE = 0.618033988749895;
function colorStringToHue(input?: string): number | undefined {
if (!input) return undefined;
const color = toHsl(input);
if (!color || typeof color.h !== 'number') return undefined;
return color.h;
}
export default function useCoolColors() {
// const brandHue = useMemo(
// () => colorStringToHue(userSettings.colorPrimary) ?? DEFAULT_BRAND_HUE,
// [userSettings.colorPrimary]
// );
/**
* Vivid, distinct cool accents — for places that need a handful of "primary"
* colors, one per card/section (e.g. the summary trend cards), NOT a stacked
* multi-series palette. Every color is anchor-quality (bright + saturated) and
* spread evenly across the blue→violet band, so the set reads as several equally
* strong primaries rather than one bold + several washed-out fills.
*/
export function useCoolAccents() {
useMode(modeRgb);
useMode(modeHsl);
useMode(modeOklch);
const brandHue = DEFAULT_BRAND_HUE;
const { isDarkTheme } = useUserSettings();
return useMemoizedFn((count: number): string[] => {
if (count <= 0) return [];
const colors: string[] = [];
const l = isDarkTheme ? 0.62 : 0.64;
const c = isDarkTheme ? 0.16 : 0.2;
const out: string[] = [];
for (let i = 0; i < count; i++) {
let hue: number;
if (i === 0) {
hue = brandHue - 5;
} 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;
}
const t = count <= 1 ? 0 : i / (count - 1);
const hue = COOL_HUE_START + t * COOL_HUE_RANGE;
out.push(
formatHex(clampChroma({ mode: 'oklch', l, c, h: hue }, 'oklch'))
);
}
return out;
});
}
const s = i === 0 ? 100 : 75 + (i % 3) * 5;
const l = i === 0 ? 50 : 55 + (i % 2) * 5;
export default function useCoolColors() {
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;
-23
View File
@@ -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;
-71
View File
@@ -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;
-136
View File
@@ -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
};
}
-35
View File
@@ -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;
-106
View File
@@ -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 };
}
-72
View File
@@ -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
};
}
-280
View File
@@ -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
};
}
+46 -25
View File
@@ -12,6 +12,7 @@ import useUserSettings from '@/hooks/use-user-settings';
import useUserSettingsStorage from '@/hooks/use-user-settings-storage';
import useAddResource from '@/pages/dashboard/hooks/use-add-resource';
import { logout } from '@/pages/login/apis';
import { didInitialStateProbe, probeAccessFlags } from '@/utils/access-probes';
import {
readColumnSettings,
readState,
@@ -41,7 +42,7 @@ import {
import { Button, ConfigProvider, Modal, theme } from 'antd';
import { useAtom } from 'jotai';
import 'overlayscrollbars/overlayscrollbars.css';
import { useMemo, useRef } from 'react';
import { useEffect, useMemo, useRef } from 'react';
import { PageContainerInner } from '../pages/_components/page-box';
import Exception from './Exception';
import './Layout.css';
@@ -51,21 +52,6 @@ import { ExtraContent } from './extraRender';
import { patchRoutes } from './runtime';
import SiderMenu from './sider-menu';
// Pages that use the page container in the page
const NO_CONTAINER_PAGES = [
'chat',
'rerank',
'embedding',
'speech',
'image',
'text2images',
'clusterDetail',
'clusterCreate',
'benchmarkDetail',
'deployment',
'video'
];
const CHECK_RESOURCE_PATH = [
'/resources/workers',
'/resources/clusters/list',
@@ -155,6 +141,50 @@ export default (props: any) => {
const { initialState, loading, setInitialState } = initialInfo;
const access = useAccess();
const probedForUserRef = useRef<string | null>(null);
// Backfill the access probes (cluster / resource-events) once we're in
// the authenticated shell. `getInitialState` runs only once at app
// boot and can't probe on the login page (no session => 401), so after
// a first login the flags arrive here as `undefined` — which the
// access predicate treats as "don't restrict", flashing GPU Service /
// the full Usage page on until a manual refresh. This layout mounts
// only post-auth (login is `layout:false`) and on every entry, so it's
// the reliable place to resolve them.
//
// The effect is keyed on the user IDENTITY, not on the flag values —
// gating on the flags is what made this fragile (on a refresh
// `getInitialState` commits `currentUser` and the flags in the same
// update, so a flag-gated effect sees them already-known and never
// fires). Whether to actually hit the network is decided by the
// module-scoped `didInitialStateProbe()` marker, which is true only
// when `getInitialState` already probed for an authenticated user this
// page load (the refresh path) — so we skip the duplicate request there
// but still probe on the SPA-login path. Keying on identity also
// re-probes correctly if the signed-in user changes.
const currentUser = initialState?.currentUser;
const username = currentUser?.username;
useEffect(() => {
if (!username || !setInitialState) {
return;
}
if (probedForUserRef.current === username) {
return;
}
probedForUserRef.current = username;
// The refresh path already resolved the flags inside `getInitialState`
// for this user — don't issue a duplicate probe.
if (didInitialStateProbe()) {
return;
}
probeAccessFlags().then((accessFlags) => {
setInitialState((prev: any) => ({
...prev,
...accessFlags
}));
});
}, [username, setInitialState]);
const userConfig = {
title: '',
@@ -224,11 +254,6 @@ export default (props: any) => {
[location.pathname]
);
const isNoContainerPage = useMemo(() => {
// @ts-ignore
return NO_CONTAINER_PAGES.includes(matchedRoute?.name as string);
}, [matchedRoute]);
const collapsed = useMemo(() => {
return userSettings.collapsed || false;
}, [userSettings.collapsed]);
@@ -424,15 +449,11 @@ export default (props: any) => {
unAccessible={runtimeConfig?.unAccessible}
noAccessible={runtimeConfig?.noAccessible}
>
{isNoContainerPage ? (
<Outlet />
) : (
<PageContainerInner>
<div>
<Outlet />
</div>
</PageContainerInner>
)}
</Exception>
</div>
{NoResourceModal}
+11
View File
@@ -198,6 +198,8 @@ export default {
'common.table.user': 'User',
'common.settings.instructions': 'Instructions',
'common.settings.language': 'Language',
'common.settings.language.tips':
'Set the display language for the interface.',
'common.delete.confirm':
'Are you sure you want to delete the selected {type}?',
'common.delete.single.confirm':
@@ -251,6 +253,11 @@ export default {
'common.appearance.tips': 'Default follows system preference.',
'common.button.forgotpassword': 'Forgot password?',
'common.appearance.theme': 'Theme',
'common.appearance.description':
'Customize how the interface looks on your device.',
'common.security': 'Security',
'common.security.description':
'Manage the password used to sign in to your account.',
'common.page.wentwrong': 'Something went wrong.',
'common.page.refresh.tips':
'The page may need to be updated. Try refreshing it!',
@@ -260,6 +267,10 @@ export default {
'common.select.count': '{count} selected',
'common.login.auth': 'Authenticating...',
'common.login.auth.failed': 'Authentication failed',
'common.login.error.source_conflict':
'An account with this username already exists from a different authentication source. Please contact an administrator to link or convert it.',
'common.login.error.auth_failed':
'Authentication with the identity provider failed. Please try again or contact your administrator.',
'common.login.password': 'Log in with Password',
'common.login.username.holder': 'Please enter username',
'common.login.password.holder': 'Please enter password',
+2
View File
@@ -81,6 +81,8 @@ export default {
'gpuservice.publicKey': 'SSH Public Key',
'gpuservice.publicKey.add': 'Add SSH Public Key',
'gpuservice.publicKey.edit': 'Edit SSH Public Key',
'gpuservice.publicKey.delete.tips':
'Deleting an SSH Public Key will not revoke access for existing attached Instances. To remove access, edit those Instances separately.',
'gpuservice.publicKey.filter.name': 'Search by name',
'gpuservice.publicKey.label': 'SSH Public Key',
'gpuservice.instance.ssh.enable': 'Enable SSH Access',
+7
View File
@@ -12,6 +12,11 @@ export default {
'users.form.active.description': 'Enable or disable this user account',
'users.form.fullname': 'Full Name',
'users.form.source': 'Source',
'users.form.source.local': 'Local',
'users.form.source.tip.switchToLocal':
'Switching to Local requires a new password. The user will sign in via the standard login form.',
'users.form.source.tip.switchToExternal':
"Switching to an external source clears the user's local password. They will sign in via the configured identity provider.",
'users.table.user': 'users',
'users.form.admin': 'Admin',
'users.form.user': 'User',
@@ -28,6 +33,8 @@ export default {
'users.password.modify.title': 'Modify Password',
'users.password.modify.description':
"For your account's security, please change your initial password.",
'users.password.modify.tips':
'Regularly updating your password helps keep your account secure.',
'users.password.confirm': 'Confirm New Password',
'users.password.confirm.empty': 'Please confirm the new password.',
'users.password.confirm.error': 'The two passwords entered do not match.',
+10
View File
@@ -199,6 +199,7 @@ export default {
'common.table.user': 'ユーザー',
'common.settings.instructions': '手順',
'common.settings.language': '言語',
'common.settings.language.tips': 'インターフェースの表示言語を設定します。',
'common.delete.confirm': '選択した {type} を削除してもよろしいですか?',
'common.delete.single.confirm':
'<span style="font-size: 13px;font-weight: 700">{name}</span> を削除してもよろしいですか?',
@@ -251,6 +252,11 @@ export default {
'common.appearance.tips': 'Default follows system preference.',
'common.button.forgotpassword': 'Forgot password?',
'common.appearance.theme': 'Theme',
'common.appearance.description':
'デバイス上でのインターフェースの表示をカスタマイズします。',
'common.security': 'セキュリティ',
'common.security.description':
'アカウントへのログインに使用するパスワードを管理します。',
'common.page.wentwrong': 'Something went wrong.',
'common.page.refresh.tips':
'The page may need to be updated. Try refreshing it!',
@@ -260,6 +266,10 @@ export default {
'common.select.count': '{count} selected',
'common.login.auth': 'Authenticating...',
'common.login.auth.failed': 'Authentication failed',
'common.login.error.source_conflict':
'このユーザー名のアカウントは別の認証ソースで既に存在します。管理者にリンクまたは変換を依頼してください。',
'common.login.error.auth_failed':
'ID プロバイダーでの認証に失敗しました。再試行するか、管理者にお問い合わせください。',
'common.login.password': 'Log in with Password',
'common.login.username.holder': 'Please enter username',
'common.login.password.holder': 'Please enter password',
+2
View File
@@ -80,6 +80,8 @@ export default {
'gpuservice.publicKey': 'SSH 公開鍵',
'gpuservice.publicKey.add': 'SSH 公開鍵を追加',
'gpuservice.publicKey.edit': 'SSH 公開鍵を編集',
'gpuservice.publicKey.delete.tips':
'SSH 公開鍵を削除しても、既にアタッチされているインスタンスのアクセス権は取り消されません。アクセス権を削除するには、対象のインスタンスを個別に編集してください。',
'gpuservice.publicKey.filter.name': '名前で検索',
'gpuservice.publicKey.label': 'SSH 公開鍵',
'gpuservice.instance.ssh.enable': 'SSH アクセスを有効化',
+7
View File
@@ -13,6 +13,11 @@ export default {
'このユーザーアカウントを有効または無効にする',
'users.form.fullname': 'フルネーム',
'users.form.source': 'ソース',
'users.form.source.local': 'ローカル',
'users.form.source.tip.switchToLocal':
'ローカルに切り替えるには新しいパスワードが必要です。以後、ユーザーは標準のログインフォームからサインインします。',
'users.form.source.tip.switchToExternal':
'外部ソースに切り替えるとユーザーのローカルパスワードが削除され、設定済みの ID プロバイダーからサインインするようになります。',
'users.table.user': 'ユーザー',
'users.form.admin': '管理者',
'users.form.user': '一般ユーザー',
@@ -29,6 +34,8 @@ export default {
'users.password.modify.title': 'パスワードを変更',
'users.password.modify.description':
'アカウントのセキュリティのため、初期パスワードを変更してください。',
'users.password.modify.tips':
'パスワードを定期的に更新すると、アカウントの安全を保てます。',
'users.password.confirm': '新しいパスワードを確認',
'users.password.confirm.empty': '新しいパスワードを確認してください。',
'users.password.confirm.error': '入力された2つのパスワードが一致しません。',
+10
View File
@@ -197,6 +197,7 @@ export default {
'common.table.user': 'Пользователь',
'common.settings.instructions': 'Инструкции',
'common.settings.language': 'Язык',
'common.settings.language.tips': 'Задайте язык отображения интерфейса.',
'common.delete.confirm': 'Вы уверены, что хотите удалить выбранный {type}?',
'common.delete.single.confirm':
'Вы уверены, что хотите удалить <span style="font-size: 13px;font-weight: 700">{name}</span>?',
@@ -250,6 +251,11 @@ export default {
'common.appearance.tips': 'По умолчанию соответствует системным настройкам.',
'common.button.forgotpassword': 'Забыли пароль?',
'common.appearance.theme': 'Тема',
'common.appearance.description':
'Настройте внешний вид интерфейса на вашем устройстве.',
'common.security': 'Безопасность',
'common.security.description':
'Управляйте паролем для входа в учётную запись.',
'common.page.wentwrong': 'Что-то пошло не так.',
'common.page.refresh.tips':
'Страница может нуждаться в обновлении. Попробуйте обновить её!',
@@ -259,6 +265,10 @@ export default {
'common.select.count': '{count} Выбрано',
'common.login.auth': 'Аутентификация...',
'common.login.auth.failed': 'Ошибка аутентификации',
'common.login.error.source_conflict':
'Учётная запись с таким именем уже существует, но с другим источником аутентификации. Обратитесь к администратору для связывания или преобразования.',
'common.login.error.auth_failed':
'Не удалось пройти аутентификацию через провайдера идентификации. Попробуйте ещё раз или обратитесь к администратору.',
'common.login.password': 'Войти с паролем',
'common.login.username.holder': 'Введите имя пользователя',
'common.login.password.holder': 'Введите пароль',
+2
View File
@@ -84,6 +84,8 @@ export default {
'gpuservice.publicKey': 'Открытый ключ SSH',
'gpuservice.publicKey.add': 'Добавить открытый ключ SSH',
'gpuservice.publicKey.edit': 'Изменить открытый ключ SSH',
'gpuservice.publicKey.delete.tips':
'Удаление открытого ключа SSH не отзывает доступ для уже подключённых экземпляров. Чтобы удалить доступ, отредактируйте эти экземпляры отдельно.',
'gpuservice.publicKey.filter.name': 'Поиск по имени',
'gpuservice.publicKey.label': 'Открытый ключ SSH',
'gpuservice.instance.ssh.enable': 'Включить SSH-доступ',
+7
View File
@@ -13,6 +13,11 @@ export default {
'Включить или отключить эту учетную запись пользователя',
'users.form.fullname': 'Полное имя',
'users.form.source': 'Источник',
'users.form.source.local': 'Локальный',
'users.form.source.tip.switchToLocal':
'Переключение на «Локальный» требует ввода нового пароля. После этого пользователь будет входить через стандартную форму входа.',
'users.form.source.tip.switchToExternal':
'Переключение на внешний источник удаляет локальный пароль пользователя. После этого вход будет выполняться через настроенного провайдера идентификации.',
'users.table.user': 'пользователи',
'users.form.admin': 'Администратор',
'users.form.user': 'Пользователь',
@@ -29,6 +34,8 @@ export default {
'users.password.modify.title': 'Смена пароля',
'users.password.modify.description':
'В целях безопасности измените первоначальный пароль.',
'users.password.modify.tips':
'Регулярное обновление пароля помогает защитить вашу учётную запись.',
'users.password.confirm': 'Подтвердите новый пароль',
'users.password.confirm.empty': 'Подтвердите новый пароль',
'users.password.confirm.error': 'Пароли не совпадают',
+10
View File
@@ -199,6 +199,7 @@ export default {
'common.table.user': 'Kullanıcı',
'common.settings.instructions': 'Talimatlar',
'common.settings.language': 'Dil',
'common.settings.language.tips': 'Arayüzün görüntüleme dilini ayarlayın.',
'common.delete.confirm':
'Seçili {type} öğesini silmek istediğinizden emin misiniz?',
'common.delete.single.confirm':
@@ -254,6 +255,11 @@ export default {
'common.appearance.tips': 'Varsayılan olarak sistem tercihini takip eder.',
'common.button.forgotpassword': 'Şifrenizi mi unuttunuz?',
'common.appearance.theme': 'Tema',
'common.appearance.description':
'Arayüzün cihazınızdaki görünümünü özelleştirin.',
'common.security': 'Güvenlik',
'common.security.description':
'Hesabınıza giriş yapmak için kullanılan parolayı yönetin.',
'common.page.wentwrong': 'Bir şeyler ters gitti.',
'common.page.refresh.tips':
'Sayfanın güncellenmesi gerekebilir. Yenilemeyi deneyin!',
@@ -263,6 +269,10 @@ export default {
'common.select.count': '{count} seçildi',
'common.login.auth': 'Kimlik doğrulanıyor...',
'common.login.auth.failed': 'Kimlik doğrulama başarısız',
'common.login.error.source_conflict':
'Bu kullanıcı adıyla farklı bir kimlik doğrulama kaynağından bir hesap zaten mevcut. Bağlamak veya dönüştürmek için lütfen yöneticinize başvurun.',
'common.login.error.auth_failed':
'Kimlik sağlayıcısı ile kimlik doğrulama başarısız oldu. Lütfen tekrar deneyin veya yöneticinize başvurun.',
'common.login.password': 'Şifre ile giriş yap',
'common.login.username.holder': 'Lütfen kullanıcı adını girin',
'common.login.password.holder': 'Lütfen şifreyi girin',
+2
View File
@@ -80,6 +80,8 @@ export default {
'gpuservice.publicKey': 'SSH Açık Anahtarı',
'gpuservice.publicKey.add': 'SSH Açık Anahtarı Ekle',
'gpuservice.publicKey.edit': 'SSH Açık Anahtarını Düzenle',
'gpuservice.publicKey.delete.tips':
'Bir SSH Açık Anahtarını silmek, mevcut bağlı Örneklerin erişimini iptal etmez. Erişimi kaldırmak için ilgili Örnekleri ayrı ayrı düzenleyin.',
'gpuservice.publicKey.filter.name': 'Ada göre ara',
'gpuservice.publicKey.label': 'SSH Açık Anahtarı',
'gpuservice.instance.ssh.enable': 'SSH Erişimini Etkinleştir',
+7
View File
@@ -13,6 +13,11 @@ export default {
'Bu kullanıcı hesabını etkinleştir veya devre dışı bırak',
'users.form.fullname': 'Tam Ad',
'users.form.source': 'Kaynak',
'users.form.source.local': 'Yerel',
'users.form.source.tip.switchToLocal':
'Yerel kaynağa geçmek yeni bir parola gerektirir. Kullanıcı bundan sonra standart oturum açma formunu kullanır.',
'users.form.source.tip.switchToExternal':
'Harici bir kaynağa geçmek kullanıcının yerel parolasını siler. Kullanıcı bundan sonra yapılandırılmış kimlik sağlayıcı üzerinden oturum açar.',
'users.table.user': 'kullanıcılar',
'users.form.admin': 'Yönetici',
'users.form.user': 'Kullanıcı',
@@ -29,6 +34,8 @@ export default {
'users.password.modify.title': 'Şifreyi Değiştir',
'users.password.modify.description':
'Hesabınızın güvenliği için lütfen başlangıç şifrenizi değiştirin.',
'users.password.modify.tips':
'Parolanızı düzenli olarak güncellemek hesabınızın güvenliğini korumaya yardımcı olur.',
'users.password.confirm': 'Yeni Şifreyi Onayla',
'users.password.confirm.empty': 'Lütfen yeni şifreyi tekrar girin.',
'users.password.confirm.error': 'Girilen iki şifre eşleşmiyor.',
+7
View File
@@ -190,6 +190,7 @@ export default {
'common.table.user': '用户',
'common.settings.instructions': '操作指引',
'common.settings.language': '语言',
'common.settings.language.tips': '设置界面的显示语言。',
'common.delete.confirm': '确定删除选中的{type}吗?',
'common.delete.single.confirm':
'确定删除以下内容?\n <span style="font-size: 13px;font-weight: 700">{name}</span>',
@@ -246,6 +247,9 @@ export default {
'common.appearance.tips': '默认跟随系统设置',
'common.button.forgotpassword': '忘记密码?',
'common.appearance.theme': '主题',
'common.appearance.description': '自定义界面在您设备上的视觉表现。',
'common.security': '安全设置',
'common.security.description': '管理用于登录账户的密码。',
'common.page.wentwrong': '哎呀,出了点问题',
'common.page.refresh.tips': '页面似乎需要更新,刷新一下试试吧!',
'common.tips.escape.disable': '请点击「取消」按钮或右上角 X 关闭窗口',
@@ -253,6 +257,9 @@ export default {
'common.select.count': '已选 {count} 项',
'common.login.auth': '认证中...',
'common.login.auth.failed': '认证失败',
'common.login.error.source_conflict':
'已存在同名账号但来源不同。请联系管理员关联或转换该账号。',
'common.login.error.auth_failed': '身份提供商认证失败。请重试或联系管理员。',
'common.login.password': '使用密码登录',
'common.login.username.holder': '请输入用户名',
'common.login.password.holder': '请输入密码',
+2
View File
@@ -75,6 +75,8 @@ export default {
'gpuservice.publicKey': 'SSH 公钥',
'gpuservice.publicKey.add': '添加 SSH 公钥',
'gpuservice.publicKey.edit': '编辑 SSH 公钥',
'gpuservice.publicKey.delete.tips':
'删除 SSH Public Key 不会撤销已挂载 GPU 实例的访问权限。如需移除访问权限,请分别编辑对应的 GPU 实例。',
'gpuservice.publicKey.filter.name': '按名称搜索',
'gpuservice.publicKey.label': 'SSH 公钥',
'gpuservice.instance.ssh.enable': '启用 SSH 访问',
+6
View File
@@ -12,6 +12,11 @@ export default {
'users.form.active.description': '启用或禁用此用户账户',
'users.form.fullname': '全名',
'users.form.source': '来源',
'users.form.source.local': '本地',
'users.form.source.tip.switchToLocal':
'切换到本地需要设置新密码,之后用户将通过标准登录表单登录。',
'users.form.source.tip.switchToExternal':
'切换到外部来源会清除该用户的本地密码,之后用户将通过所配置的身份提供商登录。',
'users.table.user': '用户',
'users.form.admin': '管理员',
'users.form.user': '普通用户',
@@ -27,6 +32,7 @@ export default {
'users.password.length': '长度在6至64个字符之间',
'users.password.modify.title': '修改密码',
'users.password.modify.description': '为了确保您的账户安全,请修改初始密码',
'users.password.modify.tips': '定期更新密码有助于保护账户安全。',
'users.password.confirm': '确认新密码',
'users.password.confirm.empty': '请确认新密码',
'users.password.confirm.error': '两次输入的密码不一致',
+1 -1
View File
@@ -1,5 +1,5 @@
import useCoolColors from '@/hooks/use-cool-colors';
import { Chart } from '@gpustack/core-ui';
import { Chart } from '@gpustack/core-ui/charts';
import { formatLargeNumber } from '@gpustack/core-ui/utils';
import { Empty, Spin, theme } from 'antd';
import _ from 'lodash';
@@ -1,68 +0,0 @@
import { IconFont } from '@gpustack/core-ui';
import { Collapse, CollapseProps } from 'antd';
import React from 'react';
import styled from 'styled-components';
const CollapseInner = styled(Collapse)`
.ant-collapse-header {
display: flex;
align-items: center;
margin-bottom: 10px !important;
padding-inline: 5px !important;
padding-block: 8px !important;
border-radius: var(--border-radius-base) !important;
font-size: 14px !important;
font-weight: 600 !important;
&:hover {
background-color: var(--ant-color-fill-tertiary) !important;
}
}
.ant-collapse-body {
padding-inline: 0 !important;
padding-block: 0 !important;
}
.ant-collapse-header-text {
display: flex;
align-items: center;
height: 24px;
}
`;
const CollapsePanel: React.FC<{
items: CollapseProps['items'];
activeKey: string | string[];
accordion?: boolean;
defaultActiveKey?: string | string[];
onChange?: (key: string | string[]) => void;
styles?: Record<string, React.CSSProperties>;
}> = ({ items, activeKey, accordion, defaultActiveKey, onChange, styles }) => {
return (
<CollapseInner
expandIconPlacement="start"
bordered={false}
ghost
accordion={accordion}
activeKey={activeKey}
defaultActiveKey={defaultActiveKey}
onChange={onChange}
destroyOnHidden={false}
styles={{
...styles,
header: {
backgroundColor: 'var(--ant-collapse-header-bg)'
}
}}
expandIcon={({ isActive }) => (
<IconFont
type="icon-down"
rotate={isActive ? 0 : -90}
style={{ fontSize: '14px' }}
></IconFont>
)}
items={items}
></CollapseInner>
);
};
export default CollapsePanel;
-250
View File
@@ -1,250 +0,0 @@
import {
readColumnSettings,
writeColumnSettings
} from '@/utils/localstore/index';
import { SettingOutlined } from '@ant-design/icons';
import { OverlayScroller } from '@gpustack/core-ui';
import { useIntl } from '@umijs/max';
import { Button, Checkbox, Col, Popover, Row, Tooltip } from 'antd';
import React, { useEffect } from 'react';
import styled from 'styled-components';
const Container = styled.div`
padding: 8px 12px;
padding-right: 4px;
.title {
font-weight: 500;
margin-bottom: 12px;
}
.btn-wrapper {
display: flex;
justify-content: space-between;
align-items: center;
padding-top: 12px;
}
.buttons {
display: flex;
gap: 8px;
justify-content: flex-end;
}
`;
const Title = styled.div`
font-weight: 500;
margin-bottom: 8px;
margin-top: 4px;
`;
const LabelWrapper = styled.span`
color: var(--ant-color-text-secondary);
> span {
display: flex;
align-items: center;
gap: 4px;
color: var(--ant-color-text-secondary);
.sub-title {
display: none;
}
}
`;
const ColumnSettings: React.FC<{
width?: number;
fixedColumns?: string[];
tableName: string;
contentHeight: number;
columns: {
title: React.ReactNode;
dataIndex?: string;
children?: { title: React.ReactNode; dataIndex?: string }[];
}[];
selectedColumns?: string[];
defaultSelectedColumns?: string[];
grouped?: boolean;
onReset?: () => void;
onChange?: (selectedColumns: string[]) => void;
}> = (props) => {
const intl = useIntl();
const {
tableName,
contentHeight,
width = 420,
columns,
selectedColumns,
defaultSelectedColumns,
grouped,
onReset,
onChange,
fixedColumns
} = props;
const [open, setOpen] = React.useState(false);
const [innerSelectedColumns, setInnerSelectedColumns] = React.useState<
string[]
>(defaultSelectedColumns || []);
React.useEffect(() => {
if (open) {
setInnerSelectedColumns(selectedColumns ?? defaultSelectedColumns ?? []);
}
}, [open, selectedColumns, defaultSelectedColumns]);
const handleToggle = () => {
setOpen(!open);
};
const handleDraftChange = (columns: string[]) => {
setInnerSelectedColumns(columns);
};
const handleSelectAll = () => {
if (grouped) {
const allCols: string[] = [];
columns.forEach((group) => {
group.children?.forEach((col) => {
if (col.dataIndex) {
allCols.push(col.dataIndex);
}
});
});
handleDraftChange(allCols);
} else {
const allCols = columns
.map((col) => col.dataIndex)
.filter((dataIndex): dataIndex is string => Boolean(dataIndex));
handleDraftChange(allCols);
}
};
const handleConfirm = () => {
setOpen(false);
writeColumnSettings(tableName, innerSelectedColumns);
onChange?.(innerSelectedColumns);
};
const handleReset = () => {
const resetCols = defaultSelectedColumns ?? [];
writeColumnSettings(tableName, resetCols);
onChange?.(resetCols);
setInnerSelectedColumns(resetCols);
onReset?.();
};
const handleOpenChange = (isOpen: boolean) => {
setOpen(isOpen);
};
useEffect(() => {
const initColumns = async () => {
const stored = await readColumnSettings(tableName);
if (stored && stored.length > 0) {
setInnerSelectedColumns(stored);
onChange?.(stored);
} else {
setInnerSelectedColumns(defaultSelectedColumns || []);
}
};
initColumns();
}, []);
const contentRender = () => {
return (
<Container>
{!grouped && (
<div className="title">
{intl.formatMessage({ id: 'benchmark.table.columnSettings' })}
</div>
)}
<OverlayScroller
maxHeight={contentHeight}
styles={{
wrapper: {
paddingInlineStart: 0
}
}}
>
<Checkbox.Group
value={innerSelectedColumns}
onChange={handleDraftChange}
>
<>
{grouped ? (
columns.map((row, index) => (
<div key={index}>
<Title>{row.title}</Title>
<Row>
{row.children?.map((col) => (
<Col key={col.dataIndex} span={12}>
<Checkbox
disabled={fixedColumns?.includes(
col.dataIndex || ''
)}
value={col.dataIndex}
style={{ marginBottom: 8 }}
>
<LabelWrapper>{col.title}</LabelWrapper>
</Checkbox>
</Col>
))}
</Row>
</div>
))
) : (
<Row>
{columns.map((col) => (
<Col key={col.dataIndex} span={12}>
<Checkbox
disabled={fixedColumns?.includes(col.dataIndex || '')}
value={col.dataIndex}
style={{ marginBottom: 8 }}
>
<LabelWrapper>{col.title}</LabelWrapper>
</Checkbox>
</Col>
))}
</Row>
)}
</>
</Checkbox.Group>
</OverlayScroller>
<div className="btn-wrapper">
<Button size="middle" onClick={handleReset}>
{intl.formatMessage({ id: 'common.button.resetdefault' })}
</Button>
<div className="buttons">
<Button size="middle" type="primary" onClick={handleSelectAll}>
{intl.formatMessage({ id: 'common.checkbox.all' })}
</Button>
<Button size="middle" type="primary" onClick={handleConfirm}>
{intl.formatMessage({ id: 'common.button.save' })}
</Button>
</div>
</div>
</Container>
);
};
return (
<Popover
open={open}
onOpenChange={handleOpenChange}
trigger={'click'}
arrow={false}
placement="bottomRight"
content={contentRender()}
styles={{
root: {
width: width
}
}}
>
<Tooltip
title={intl.formatMessage({ id: 'benchmark.table.columnSettings' })}
>
<Button onClick={handleToggle} icon={<SettingOutlined />}></Button>
</Tooltip>
</Popover>
);
};
export default ColumnSettings;
@@ -1,99 +0,0 @@
import { useOverlayScroller } from '@gpustack/core-ui';
import React, { useCallback } from 'react';
import styled from 'styled-components';
import { WrapperContext } from './use-wrapper-context';
const Wrapper = styled.div`
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
position: relative;
width: 100%;
`;
const ContentWrapper = styled.div`
flex: 1;
position: relative;
overflow-y: auto;
`;
const Footer = styled.div`
padding-block: 0;
background-color: var(--ant-color-bg-elevated);
`;
interface ColumnWrapperProps {
children: React.ReactNode;
footer?: React.ReactNode;
maxHeight?: string | number;
paddingBottom?: number;
styles?: {
wrapper?: React.CSSProperties;
container?: React.CSSProperties;
};
}
const ColumnWrapper: React.FC<ColumnWrapperProps> = ({
children,
footer,
maxHeight,
styles = {}
}) => {
const scroller = React.useRef<any>(null);
const footerRef = React.useRef<HTMLDivElement>(null);
const contentRef = React.useRef<HTMLDivElement>(null);
const {
initialize,
instance,
scrollEventElement,
scrollToBottom,
scrollToTarget,
getScrollElementScrollableHeight
} = useOverlayScroller({
options: {
scrollbars: {
autoHide: 'move'
}
}
});
React.useEffect(() => {
if (scroller.current) {
initialize(scroller.current);
}
}, []);
const setContentPaddingBottom = useCallback((padding: number) => {
contentRef.current!.style.paddingBottom = `${padding}px`;
}, []);
return (
<WrapperContext.Provider
value={{
scroller: scroller,
osInstance: instance,
scrollEventElement,
getScrollElementScrollableHeight,
scrollToBottom,
scrollToTarget,
setSScrollContentPaddingBottom: setContentPaddingBottom
}}
>
<Wrapper style={{ height: maxHeight || '100%', ...styles.wrapper }}>
<ContentWrapper
ref={scroller}
style={{
padding: '16px 24px',
...styles.container
}}
>
<div ref={contentRef}>{children}</div>
</ContentWrapper>
{footer && <Footer ref={footerRef}>{footer}</Footer>}
</Wrapper>
</WrapperContext.Provider>
);
};
export default ColumnWrapper;
@@ -1,28 +0,0 @@
import { createContext, useContext } from 'react';
interface WrapperContextProps {
osInstance?: any;
scroller?: any;
scrollEventElement?: any;
scrollToBottom?: () => void;
scrollToTop?: () => void;
getScrollElementScrollableHeight?: () => {
scrollHeight: number;
scrollTop: number;
};
scrollToTarget?: (target: any, offset?: number) => void;
getScrollElement?: () => HTMLElement | null;
setSScrollContentPaddingBottom?: (padding: number) => void;
}
export const WrapperContext = createContext<WrapperContextProps>(
{} as WrapperContextProps
);
export const useWrapperContext = () => {
const context = useContext(WrapperContext);
if (!context) {
throw new Error('useWrapperContext must be used within a WrapperProvider');
}
return context;
};
-155
View File
@@ -1,155 +0,0 @@
import { CloseOutlined } from '@ant-design/icons';
import { OverlayScroller } from '@gpustack/core-ui';
import { useIntl } from '@umijs/max';
import { Button, Form } from 'antd';
import classNames from 'classnames';
import React, { forwardRef, useImperativeHandle } from 'react';
import filterFormCss from '../styles/filter-form.less';
const FilterForm: React.FC<
React.PropsWithChildren & {
ref?: any;
width?: number;
contentHeight?: number | string;
initialValues?: any;
hasFilters?: boolean;
open?: boolean;
onClear?: () => void;
onClose?: () => void;
onValuesChange?: (ChangeValues: any, allValues: any) => void;
styles?: {
container?: React.CSSProperties;
wrapper?: React.CSSProperties;
};
}
> = forwardRef(
(
{
children,
open,
width = 300,
contentHeight = 400,
initialValues = {},
styles,
onClose,
onClear,
onValuesChange
},
ref
) => {
const intl = useIntl();
const [form] = Form.useForm();
const handleOnReset = () => {
form.resetFields(Object.keys(initialValues));
form.resetFields();
onValuesChange?.({}, form.getFieldsValue());
};
const handleOnClose = () => {
onClose?.();
};
const handleOnValuesChange = (changedValues: any, allValues: any) => {
onValuesChange?.(changedValues, allValues);
};
const handleOnClear = () => {
handleOnReset();
};
const filtersCount = Object.values(form.getFieldsValue()).filter(
(value) => value !== undefined && value !== null && value !== ''
).length;
const renderFooter = () => {
return (
<div className={filterFormCss['btn-wrapper']}>
<Button size="middle" onClick={handleOnReset}>
{intl.formatMessage({ id: 'common.button.reset' })}
</Button>
<div className={filterFormCss.buttons}>
<Button size="middle" type="primary" onClick={handleOnClose}>
{intl.formatMessage({ id: 'common.button.close' })}
</Button>
</div>
</div>
);
};
useImperativeHandle(ref, () => ({
form,
reset: handleOnReset,
getValues: () => form.getFieldsValue(),
setValues: (values: any) => form.setFieldsValue(values)
}));
return (
<div
className={classNames(filterFormCss.wrapper, {
[filterFormCss.show]: open
})}
style={{
width: open ? width : 0,
height: contentHeight,
...styles?.wrapper
}}
>
<div
style={{ width: width, ...styles?.container }}
className={filterFormCss.container}
>
<div className={filterFormCss.title}>
<span
style={{
fontWeight: 500,
color: 'var(--ant-color-text-tertiary)'
}}
>
{intl.formatMessage({ id: 'common.filter.label' })}
</span>
<Button
size="small"
icon={<CloseOutlined />}
onClick={handleOnClose}
type="text"
style={{
color: 'var(--ant-color-text-secondary)'
}}
></Button>
</div>
<OverlayScroller
maxHeight={contentHeight}
styles={{
wrapper: {
paddingInline: 8
}
}}
>
<Form
onValuesChange={handleOnValuesChange}
initialValues={initialValues}
form={form}
layout="vertical"
styles={{
label: {
lineHeight: 1,
height: 'auto',
marginBottom: 8,
fontWeight: 500
},
content: {
minHeight: 0
}
}}
>
{children}
</Form>
</OverlayScroller>
</div>
</div>
);
}
);
export default FilterForm;
-90
View File
@@ -1,90 +0,0 @@
import { ColumnWrapper, GSDrawer, ModalFooter } from '@gpustack/core-ui';
import type { DrawerProps } from 'antd';
import { Tag } from 'antd';
import React from 'react';
const ModalFooterStyle = {
padding: '16px 24px 8px',
display: 'flex',
justifyContent: 'flex-end'
};
type AddModalProps = {
title: React.ReactNode;
open: boolean;
onCancel?: () => void;
children?: React.ReactNode;
onSubmit?: () => void;
width?: number | string;
footer?: React.ReactNode;
subTitle?: React.ReactNode;
push?: DrawerProps['push'];
};
const FormDrawer: React.FC<AddModalProps> = ({
title,
open,
onCancel,
onSubmit,
children,
width = 600,
subTitle,
footer,
push
}) => {
return (
<GSDrawer
title={
<>
{title}
{subTitle && (
<Tag
variant="outlined"
style={{
fontSize: 12,
fontWeight: 400,
marginLeft: 8,
borderRadius: 4,
borderColor: 'var(--ant-color-border-secondary)',
color: 'var(--ant-color-text-secondary)'
}}
>
{subTitle}
</Tag>
)}
</>
}
open={open}
onClose={onCancel}
destroyOnHidden={true}
closeIcon={false}
mask={{
closable: false
}}
keyboard={false}
push={push}
styles={{
wrapper: { width }
}}
footer={false}
>
<ColumnWrapper
styles={{
container: { paddingBlock: 0 }
}}
footer={
footer ?? (
<ModalFooter
onOk={onSubmit}
onCancel={onCancel}
style={ModalFooterStyle}
></ModalFooter>
)
}
>
{children}
</ColumnWrapper>
</GSDrawer>
);
};
export default FormDrawer;
@@ -1,81 +0,0 @@
.mask {
position: fixed;
inset: 0;
z-index: 1004;
height: 100%;
background-color: var(--ant-color-bg-mask);
opacity: 0;
transition: opacity var(--ant-motion-duration-slow)
var(--ant-motion-ease-in-out);
}
.maskOpen {
opacity: 1;
}
.overlay {
position: fixed;
top: 0;
right: 0;
bottom: auto;
left: auto;
z-index: 1005;
display: flex;
flex-direction: column;
height: 100vh;
overflow: hidden;
background: var(--ant-color-bg-elevated);
border-radius: var(--ant-border-radius-lg) 0 0 var(--ant-border-radius-lg);
box-shadow:
-6px 0 16px 0 rgb(0 0 0 / 8%),
-3px 0 6px -4px rgb(0 0 0 / 12%),
-9px 0 28px 8px rgb(0 0 0 / 5%);
transform: translateX(100%);
transition: transform var(--ant-motion-duration-slow)
var(--ant-motion-ease-in-out);
}
.overlayOpen {
transform: translateX(0);
}
.header {
display: flex;
align-items: center;
gap: 12px;
min-height: 56px;
padding: var(--ant-padding) var(--ant-padding-lg);
border-bottom: 1px solid var(--ant-color-split);
color: var(--ant-color-text);
font-size: 14px;
font-weight: 600;
line-height: 24px;
}
.title {
display: flex;
min-width: 0;
align-items: center;
}
.subTitle {
margin-left: 8px;
border-color: var(--ant-color-border-secondary);
border-radius: 4px;
color: var(--ant-color-text-secondary);
font-size: 12px;
font-weight: 400;
}
.content {
padding-block: 16px;
flex: 1;
height: calc(100vh - 57px);
overflow-y: auto;
}
.footer {
display: flex;
justify-content: flex-end;
padding: 16px 24px 8px;
}
-122
View File
@@ -1,122 +0,0 @@
import { ColumnWrapper, IconFont } from '@gpustack/core-ui';
import { Button, Tag } from 'antd';
import classNames from 'classnames';
import React from 'react';
import { createPortal } from 'react-dom';
import styles from './form-overlay-view.module.less';
type FormOverlayViewProps = {
title: React.ReactNode;
open: boolean;
onCancel?: () => void;
children?: React.ReactNode;
onSubmit?: () => void;
footer?: React.ReactNode;
subTitle?: React.ReactNode;
width?: number | string;
className?: string;
style?: React.CSSProperties;
maskClosable?: boolean;
getContainer?: () => HTMLElement | null | undefined;
};
const FormOverlayView: React.FC<FormOverlayViewProps> = ({
title,
open,
onCancel,
onSubmit,
children,
subTitle,
footer,
width = 600,
className,
style,
maskClosable = false,
getContainer
}) => {
const [container, setContainer] = React.useState<HTMLElement | null>(null);
const [mounted, setMounted] = React.useState(false);
const [active, setActive] = React.useState(false);
React.useEffect(() => {
if (open) {
setContainer(getContainer?.() ?? null);
setMounted(true);
return;
}
setActive(false);
}, [getContainer, open]);
React.useEffect(() => {
if (!mounted || !container) {
return;
}
const id = requestAnimationFrame(() => setActive(true));
return () => cancelAnimationFrame(id);
}, [mounted, container]);
const handleTransitionEnd = (e: React.TransitionEvent<HTMLDivElement>) => {
if (e.target !== e.currentTarget || e.propertyName !== 'transform') {
return;
}
if (!open && !active) {
setMounted(false);
setContainer(null);
}
};
if (!mounted || !container) {
return null;
}
return createPortal(
<>
<div
className={classNames(styles.mask, { [styles.maskOpen]: active })}
onClick={maskClosable ? onCancel : undefined}
/>
<div
className={classNames(
styles.overlay,
{ [styles.overlayOpen]: active },
className
)}
style={{ width, ...style }}
role="dialog"
aria-modal="true"
onTransitionEnd={handleTransitionEnd}
>
<div className={styles.header}>
<Button
type="text"
size="small"
style={{ fontWeight: 600, fontSize: 16 }}
icon={<IconFont type="icon-down2" rotate={90} />}
onClick={onCancel}
/>
<div className={styles.title}>
{title}
{subTitle && (
<Tag variant="outlined" className={styles.subTitle}>
{subTitle}
</Tag>
)}
</div>
</div>
<ColumnWrapper
styles={{
container: { paddingBlock: 16 }
}}
footer={footer}
>
{children}
</ColumnWrapper>
</div>
</>,
container
);
};
export default FormOverlayView;
File diff suppressed because one or more lines are too long
-26
View File
@@ -1,26 +0,0 @@
import { update } from 'jdenticon';
import React, { useEffect, useRef } from 'react';
interface IdenticonProps {
value: string;
size: number;
}
const Identicon: React.FC<IdenticonProps> = ({
value = 'test',
size = 28
}: IdenticonProps) => {
const icon = useRef<any>(null);
useEffect(() => {
update(icon.current, value);
}, [value]);
return (
<>
<svg data-jdenticon-value={value} height={size} ref={icon} width={size} />
</>
);
};
export default Identicon;
+80 -13
View File
@@ -4,28 +4,53 @@ import {
RouteContext,
type PageContainerProps
} from '@ant-design/pro-components';
import { useOverlayScroller } from '@gpustack/core-ui';
import {
HeaderSlotContext,
useOverlayScroller,
type HeaderSlotContextValue
} from '@gpustack/core-ui';
import { Divider } from 'antd';
import classNames from 'classnames';
import { useContext, useEffect, useRef } from 'react';
import {
useCallback,
useContext,
useEffect,
useMemo,
useRef,
useState
} from 'react';
import pageBoxCss from './styles/page-box.less';
// The header-slot bridge (HeaderSlotContext, HeaderLeft, HeaderRight,
// usePageContentStyle) lives in @gpustack/core-ui so the host and the
// enterprise plugin share the SAME context instance and can portal into this
// layout-owned header bar. Re-exported here so existing host import sites
// keep working.
export {
HeaderLeft,
HeaderRight,
usePageContentStyle
} from '@gpustack/core-ui';
const paddingInlinePageContainerContent = 24;
export const PageContainerInner: React.FC<
PageContainerProps & {
leftContent?: React.ReactNode;
rightContent?: React.ReactNode;
styles?: {
containerWrapper?: React.CSSProperties;
};
}
> = ({ children, styles, title, leftContent, rightContent, ...rest }) => {
> = ({ children, styles, title, ...rest }) => {
const { initialize: initialize } = useOverlayScroller({
defer: false
});
const pageContext = useContext(RouteContext);
const contentWrapperRef = useRef<HTMLDivElement>(null);
const [leftEl, setLeftEl] = useState<HTMLDivElement | null>(null);
const [rightEl, setRightEl] = useState<HTMLDivElement | null>(null);
const [contentStyleOverride, setContentStyleOverride] = useState<
React.CSSProperties | undefined
>(undefined);
useEffect(() => {
if (contentWrapperRef.current) {
@@ -34,7 +59,45 @@ export const PageContainerInner: React.FC<
}
}, [initialize, contentWrapperRef]);
const setContentStyle = useCallback(
(style: React.CSSProperties | undefined) => {
setContentStyleOverride(style);
return () => setContentStyleOverride(undefined);
},
[]
);
const slotOwnersRef = useRef<{ left: number; right: number }>({
left: 0,
right: 0
});
const registerSlot = useCallback((slot: 'left' | 'right') => {
const owners = slotOwnersRef.current;
owners[slot] += 1;
if (process.env.NODE_ENV !== 'production' && owners[slot] > 1) {
const name = slot === 'left' ? 'HeaderLeft' : 'HeaderRight';
console.warn(
`[PageContainerInner] ${owners[slot]} <${name}> are mounted at once; their content stacks in the same header slot. Only one page/component should own each slot at a time.`
);
}
return () => {
owners[slot] -= 1;
};
}, []);
const slotValue = useMemo<HeaderSlotContextValue>(
() => ({
leftEl,
rightEl,
setContentStyle,
registerSlot
}),
[leftEl, rightEl, setContentStyle, registerSlot]
);
return (
<HeaderSlotContext.Provider value={slotValue}>
<div className={pageBoxCss.containerWrapper}>
<PageContainer
{...rest}
@@ -50,27 +113,31 @@ export const PageContainerInner: React.FC<
>
<div className={pageBoxCss.title}>
<div className={pageBoxCss.left}>
{leftContent || pageContext.title}
<div ref={setLeftEl} className={pageBoxCss.leftSlot} />
<span className={pageBoxCss.defaultTitle}>
{pageContext.title}
</span>
</div>
<div className={pageBoxCss.right}>
{rightContent && (
<div>
{rightContent}
<Divider orientation="vertical" style={{ margin: '0 16px' }} />
</div>
)}
<div ref={setRightEl} className={pageBoxCss.rightSlot} />
<Divider
className={pageBoxCss.divider}
orientation="vertical"
style={{ margin: '0 16px' }}
/>
<ExtraContent />
</div>
</div>
<div
className={classNames(pageBoxCss.contentWrapper)}
style={styles?.containerWrapper}
style={{ ...styles?.containerWrapper, ...contentStyleOverride }}
ref={contentWrapperRef}
>
{children}
</div>
</PageContainer>
</div>
</HeaderSlotContext.Provider>
);
};
@@ -1,68 +0,0 @@
import {
digitReg,
lowercaseReg,
specialCharacterReg,
uppercaseReg
} from '@/config';
import { CheckCircleFilled, CloseCircleFilled } from '@ant-design/icons';
import { useIntl } from '@umijs/max';
import { Space } from 'antd';
const PasswordValidate: React.FC<{ value: string }> = ({ value = '' }) => {
const intl = useIntl();
const renderIcon = ({ valid, text }: { valid: boolean; text: string }) => {
return (
<>
{valid ? (
<CheckCircleFilled style={{ color: 'green' }} />
) : (
<CloseCircleFilled style={{ color: 'red' }} />
)}
<span
className="m-l-5"
style={{ color: 'var(--ant-color-text-description)' }}
>
{text}
</span>
</>
);
};
return (
<Space direction="vertical" style={{ paddingTop: '10px' }}>
<span>
{renderIcon({
valid: uppercaseReg.test(value),
text: intl.formatMessage({ id: 'users.password.uppcase' })
})}
</span>
<span>
{renderIcon({
valid: lowercaseReg.test(value),
text: intl.formatMessage({ id: 'users.password.lowercase' })
})}
</span>
<span>
{renderIcon({
valid: digitReg.test(value),
text: intl.formatMessage({ id: 'users.password.number' })
})}
</span>
<span>
{renderIcon({
valid: value.length >= 6 && value.length <= 12,
text: intl.formatMessage({ id: 'users.password.length' })
})}
</span>
<span>
{renderIcon({
valid: specialCharacterReg.test(value),
text: intl.formatMessage({ id: 'users.password.special' })
})}
</span>
</Space>
);
};
export default PasswordValidate;

Some files were not shown because too many files have changed in this diff Show More