Compare commits

...
Author SHA1 Message Date
hibigandgithub-actions[bot] 8e832e0b0e chore: bump @gpustack/core-ui to v1.0.36 2026-07-14 11:44:44 +00:00
jialinandjialin 8eb871d0f5 fix(user-models): hold empty state until infra queries resolve
Managers start in a loading state and gate the empty state on the
cluster/worker fetch, so the 'no clusters/workers' guidance no longer
flashes before the real infra data lands.
2026-07-14 17:37:53 +08:00
jialinandjialin 6e321d2d99 feat(models): resource-aware empty state on My Models; tag GPU cluster type experimental
My Models empty state now guides managers to create a cluster/worker or
deploy a model (queries gated to canSeeAdmin/canSeeOrgAdmin); plain users
keep the consumer copy. Add an Experimental badge to the GPU-instance
cluster type card.
2026-07-14 17:37:53 +08:00
jialinandjialin 97179365bb feat(cluster): drop detail page; host Cluster Access drawer
Remove the cluster-detail route and the name-link navigation; the
cluster name is now plain text. Mount the plugin-provided
clusterDetail.AccessDrawer so the enterprise row action can open it.
2026-07-14 17:37:53 +08:00
micheliaandjialin 046d466e16 fix(usage): memoize resource breakdown filters (review comment)
The GPU Instances / Storage tabs built the filter object inline (in
baseRequest and again in the extra tab's props), duplicating the
conditional-spread logic and handing the enterprise Organization tab a fresh
object every render — which refired its fetch effect on any state change.
Hoist it into one memoized ``breakdownFilters`` shared by the chart request
and the extra tab, mirroring the Tokens tab's stable-filters shape.
2026-07-14 14:35:23 +08:00
micheliaandjialin 77a2abad32 fix(usage): satisfy Rules of Hooks for plugin extra tabs; date-safe export
- change the breakdown extra-tab contract from a `useVisible()` hook to a plain
  `isVisible(ctx)` function so the host no longer calls hooks inside a loop
  (token breakdown-tabs + resource GPU/Storage tabs)
- resource export: drop the time portion of the date bucket by slicing the ISO
  string instead of re-parsing with dayjs (avoids a timezone day-shift)
2026-07-14 14:35:23 +08:00
micheliaandjialin efb63335e7 feat(usage): add plugin injection points for org / user-group filters and org breakdown tab
- token & resource filter bars render a plugin slot (UsageFilterBar /
  ResourceUsageFilterBar) for the organization / user-group filters
- breakdown tabs read plugin-provided extra sub-tabs (breakdownExtraTabs /
  resourceBreakdownExtraTabs) so the enterprise plugin can inject the
  Organization sub-tab
- thread the org / user-group filter state through the token filter hook and
  the resource tabs / breakdown tables / summary / export dialog
- standardize the organization label on the principal name; resource export
  date formatted to date-only
2026-07-14 14:35:23 +08:00
jialinandjialin d0b6498bda fix(llmodels): don't evaluate on cluster seed before a model is selected
The basic form seeds a default cluster on open, which fired the full cluster-change handler and triggered an evaluate request before any model was picked. Split off a seed callback that refreshes scoped options without evaluating, and guard the evaluate handler with a model-selected check as a backstop.
2026-07-13 17:14:43 +08:00
jialinandjialin 35ed397c2a fix: use branded sidebar logo, gate help menu behind plugin, add instance pending/starting filters 2026-07-13 13:59:13 +08:00
jialinandjialin cb380d676e refactor(usage): use SimpleSelect for the events-tab event-type filter
Match the other usage tabs' dropdown component instead of the raw antd
Select.
2026-07-10 17:41:54 +08:00
jialinandjialin 03c47530bd fix(usage): map owner creator fields for chart-export User column
Read creator_id / creator_name / creator_deleted from the breakdown item
root into the flat user_* fields, so the GPU Instances / Storage chart
export marks a deleted owner independently of the grouped instance/volume.
2026-07-10 17:41:54 +08:00
jialinandjialin ab92641541 fix(usage): sort deleted filter options to the bottom of every dropdown
Deleted users / models / API keys / instances / volumes now sink below the
live entries in each usage tab's filter, while the current account stays on
top and live entries keep their order (stable sort).
2026-07-10 17:41:54 +08:00
jialinandjialin 1f861c5844 feat(usage): break down chart export by instance/volume with owner user column
Group the GPU Instances and Storage chart exports by date+instance /
date+volume, add Instance/Storage and (org-admin only) User columns, and
mark deleted instances/volumes and their owner user independently in both
the preview and the exported sheet.
2026-07-10 16:31:35 +08:00
jialinandjialin f92499cbe5 refactor(llmodels): rename MyModelsStatusValueMap keys to Stopped/NotReady/Ready 2026-07-10 16:31:35 +08:00
jialinandjialin 6a9cba8020 feat(clusters): warn about K8s version requirement on registration 2026-07-09 20:36:52 +08:00
jialinandjialin 4f4b527014 fix(usage): unique filter option values so same-named entries don't co-select 2026-07-09 20:36:52 +08:00
jialinandjialin 0d5812ba88 fix(cluster): ensure gpuInstanceOptions is submitted for gpu k8s cluster 2026-07-09 20:36:52 +08:00
jialinandjialin 650091b57a fix(usage): carry deleted flag in summary-tab user filter union 2026-07-09 20:36:52 +08:00
jialinandjialin 5bdfb36721 fix(apikeys): description incorrect when empty data 2026-07-09 20:36:52 +08:00
jialinandjialin 0436872093 style(user-models): larger logo, restyle playground button on card 2026-07-09 20:36:52 +08:00
jialinandjialin 3d88b2825b feat(models): rename user-models menu to Models, reorder nav, fix glm logo 2026-07-09 20:36:52 +08:00
jialinandjialin dc54c21d70 feat(user-models): clickable model card opens API access info 2026-07-09 20:36:52 +08:00
jialinandjialin 794ac724ca refactor(usage): address PR review — null-safe mark, hook renames, lazy intl 2026-07-09 15:17:53 +08:00
jialinandjialin d91fbec9d7 refactor(cluster-management): use menu.resources i18n keys 2026-07-09 15:17:53 +08:00
jialinandjialin 555aab714e fix(usage): mark deleted rows and correct api-key header in token chart export 2026-07-09 15:17:53 +08:00
jialinandjialin eb037a3613 style(dashboard): show deleted marker in usage chart labels 2026-07-09 15:17:53 +08:00
jialinandjialin b1efa415e8 style(usage): show deleted marker in chart legends and exports 2026-07-09 15:17:53 +08:00
jialinandjialin e79d49f2a8 style(usage): unify name column header + DeletedTag across breakdown tables 2026-07-09 15:17:53 +08:00
jialinandjialin 7d9f68da2d style(usage): extract DeletedTag, unify deleted-entity tag across filters 2026-07-09 15:17:53 +08:00
jialinandjialin c691df25c0 feat(usage): mark current account first in resource tab user filters
Apply the Tokens tab's current-user-first ordering and [Current Account] marker to the Summary/Storage/Instances tab user filters.
2026-07-09 15:17:53 +08:00
gitlawrandjialin cd4b1b8629 fix(clusters): scope cluster pickers to the owning org
Cluster pickers listed every visible cluster, which includes clusters
shared with all authenticated users (e.g. the default org's "shared with
everyone" clusters). A member of a custom org could then pick another
org's cluster for a model deployment or a new worker.

Request only the current org's own clusters (mine=true) for:
- the model deploy picker (models page),
- the deploy-from-model-file picker (model files page), and
- the add-worker picker.

These resources are owner-scoped on the backend, so the model list and
the worker table (including its cluster-name column) only reference
own-org clusters. The add-model-file worker cascader is already own-org
via the owner-scoped worker list. Platform admin in the "All" view
bypasses mine and is scoped instead by the org picker.
2026-07-08 20:12:00 +08:00
jialinandjialin 70229a9757 feat(gpu-service): add status column to storage and storage-type lists 2026-07-08 16:55:42 +08:00
gitlawrandjialin 644aba3fc2 fix: scope storage type picker to the target organization
The storage create form fetched persistent-volume types once with no
tenant scope, so in the platform-admin "All" view the dropdown listed
every org's types and never re-scoped when the create-scope picker
retargeted the form — letting an org with no cluster-access grant pick
another org's types. The inline "add storage" sub-drawer in the instance
create form had the same gap plus a redundant org picker that could
retarget the storage away from the instance's org.

Both surfaces now pin the storage-type list to the chosen org (via the
X-Organization-Id header); the sub-drawer inherits the instance's scope
and hides its own picker. Page/table listings stay unscoped so every
row's type label still renders.
2026-07-08 16:31:53 +08:00
gitlawrandjialin c025257fea feat(gpu-service): clone an instance template into the caller's scope
Add a Clone action to instance template cards. It reuses the create
flow prefilled from the source, dropping the source id and ownership so
the backend assigns a fresh id and scopes the copy to the caller's own
principal. Clone is offered on every visible template — including
admin-curated Global rows a non-admin can otherwise only view — so
users can fork a Global preset into their own organization without
admin involvement.

The prefilled name defaults to a `-clone` suffix (trimmed to the
63-char limit) so cloning within the same scope doesn't collide on the
unique name. Edit/Delete remain gated on ownership; Clone is always
available.
2026-07-08 16:27:40 +08:00
gitlawrandjialin fdafdfc3d1 fix(gpu-service): show global instance templates under custom scopes
The Instance Templates management page requested the `mine` list scope,
which drops admin-curated Global templates for non-admin callers. The
Inference Backend page uses the default scope and shows Global + own
rows, so the two pages behaved inconsistently.

Use the default scope on the templates page so Global templates are
visible alongside the caller's own, and gate Edit/Delete on the card so
Global rows a non-admin can't modify render read-only.
2026-07-08 16:27:40 +08:00
jialinandjialin f18c12662b Revert "fix(usage): remove the delete tag of models and users in tokens tab"
This reverts commit 1b3d4748d4de963ba0c710e3aac91e3af0015531.
2026-07-08 10:45:23 +08:00
jialinandjialin 71840cfc8f refactor(footer): use useAtomValue for version 2026-07-07 20:37:39 +08:00
jialinandjialin e75beb7324 fix(footer): show version reactively via useAtom 2026-07-07 20:37:39 +08:00
jialinandjialin 90cc6ec753 fix(cluster): drive k8s cluster type from shared state
Replace unreliable Form.useWatch on the unregistered gpuInstanceOptions
path with explicit shared clusterType state. Fixes the type selector
being unclickable and flickering, and ensures a model cluster no longer
submits a stale gpuInstanceOptions payload.
2026-07-07 20:37:39 +08:00
jialinandjialin bd4954f161 fix(usage): remove the delete tag of models and users in tokens tab 2026-07-07 20:37:39 +08:00
jialinandjialin 32fab0fa62 style: gpu selector 2026-07-07 20:37:39 +08:00
gitlawrandjialin d12e92a916 fix(models): preselect the scoped default cluster on deploy
The deploy form's cluster select could open blank even when a valid
(default) cluster exists for the current scope. The open handler seeds
the cluster once, but that seed can be empty (options not loaded yet) or
point outside the resolved scope (the scope id and the option list both
settle after mount, and switching scope re-scopes the list), and nothing
re-picked afterwards — so the field stayed empty.

Make the form enforce the invariant directly: whenever the scope or the
available options change, if the current selection is empty or not among
the options, fall back to the scope's default cluster (then a Ready one,
then the first). A still-valid selection is left untouched.
2026-07-07 18:18:39 +08:00
jialinandjialin dc41b61d69 fix(deployments): display gpu allocated value in gpu selector 2026-07-06 17:48:22 +08:00
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
202 changed files with 2839 additions and 1901 deletions
+11
View File
@@ -88,6 +88,16 @@ Prefer action-driven updates, explicit handlers, and localized state transitions
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:
@@ -101,6 +111,7 @@ A page module lives under `src/pages/{module}` with this sub-structure: `compone
- `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
+10 -21
View File
@@ -100,6 +100,15 @@ const baseRoutes = [
path: '/models',
redirect: '/models/deployments'
},
{
name: 'userModels',
path: '/models/user-models',
key: 'userModels',
icon: 'icon-models',
selectedIcon: 'icon-models-filled',
defaultIcon: 'icon-models',
component: './llmodels/user-models'
},
{
name: 'modelCatalog',
path: '/models/catalog',
@@ -110,16 +119,6 @@ const baseRoutes = [
access: 'canSeeOrgAdmin',
component: './llmodels/catalog'
},
{
name: 'userModels',
path: '/models/user-models',
key: 'userModels',
icon: 'icon-models',
selectedIcon: 'icon-models-filled',
defaultIcon: 'icon-models',
access: 'canSeeUser',
component: './llmodels/user-models'
},
{
name: 'deployment',
path: '/models/deployments',
@@ -274,7 +273,7 @@ const baseRoutes = [
selectedIcon: 'icon-cluster2-filled',
defaultIcon: 'icon-cluster2-outline',
component: './cluster-management/clusters',
subMenu: ['/resources/clusters/detail', '/resources/clusters/create']
subMenu: ['/resources/clusters/create']
},
{
name: 'workers',
@@ -302,16 +301,6 @@ const baseRoutes = [
selectedIcon: 'icon-credential-filled',
defaultIcon: 'icon-credential-outline',
component: './cluster-management/credentials'
},
{
name: 'clusterDetail',
path: '/resources/clusters/detail',
key: 'clusterDetail',
icon: 'icon-cluster2-outline',
selectedIcon: 'icon-cluster2-filled',
defaultIcon: 'icon-cluster2-outline',
hideInMenu: true,
component: './cluster-management/cluster-detail'
}
]
},
+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.32",
"@gpustack/core-ui": "^1.0.36",
"@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",
+5 -13
View File
@@ -24,8 +24,8 @@ importers:
specifier: ^7.1.1
version: 7.1.2
'@gpustack/core-ui':
specifier: ^1.0.32
version: 1.0.32(czdvzceysqw7iv6pct2ucnb23e)
specifier: ^1.0.36
version: 1.0.36(czdvzceysqw7iv6pct2ucnb23e)
'@huggingface/gguf':
specifier: ^0.1.7
version: 0.1.18
@@ -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
@@ -1484,8 +1481,8 @@ 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.32':
resolution: {integrity: sha512-kGTazoqbK2KyZgOP6gmQaRxTiQVfF2IKLGDXjJq6w6BbmJgALXFJA2v2ROjAbjEVyfTdBzyYeXfPo/JgISpMNw==, tarball: https://registry.npmjs.org/@gpustack/core-ui/-/core-ui-1.0.32.tgz}
'@gpustack/core-ui@1.0.36':
resolution: {integrity: sha512-gF8ShMZ2SKYo3+skfsI3zo+aBSsXqKGMX+jWaKv81e38bDwJw71C33TR64d+6MyV9iUoFOoSK+ht1GefyvR17g==, tarball: https://registry.npmjs.org/@gpustack/core-ui/-/core-ui-1.0.36.tgz}
peerDependencies:
'@ant-design/icons': ^6.1.0
'@ant-design/pro-components': 3.1.0-0
@@ -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}
@@ -10808,7 +10802,7 @@ snapshots:
'@formatjs/intl-utils@2.3.0': {}
'@gpustack/core-ui@1.0.32(czdvzceysqw7iv6pct2ucnb23e)':
'@gpustack/core-ui@1.0.36(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)
@@ -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

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

+3 -2
View File
@@ -1,10 +1,10 @@
import { GPUStackVersionAtom } from '@/atoms/user';
import { getAtomStorage } from '@/atoms/utils';
import VersionInfo, { modalConfig } from '@/components/version-info';
import externalLinks from '@/constants/external-links';
import { useIntl } from '@umijs/max';
import { Button, Divider, Modal, Typography } from 'antd';
import { createStyles } from 'antd-style';
import { useAtomValue } from 'jotai';
import styled from 'styled-components';
const CompanyWrapper = styled.div`
@@ -35,6 +35,7 @@ const Footer: React.FC = () => {
const intl = useIntl();
const [modal, contextHolder] = Modal.useModal();
const { styles } = useStyles();
const version = useAtomValue(GPUStackVersionAtom);
const showVersion = () => {
modal.info({
@@ -73,7 +74,7 @@ const Footer: React.FC = () => {
</Button>
<Divider orientation="vertical" />
<Button type="link" size="small" onClick={showVersion}>
{getAtomStorage(GPUStackVersionAtom)?.version}
{version?.version}
</Button>
</div>
</div>
+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;
+3 -2
View File
@@ -1,6 +1,7 @@
import Logo from '@/assets/images/gpustack-logo.png';
import { GPUStackVersionAtom, UpdateCheckAtom, userAtom } from '@/atoms/user';
import externalLinks from '@/constants/external-links';
import { useLogo } from '@/hooks/use-logo';
import { Button } from 'antd';
import { useAtom } from 'jotai';
import React from 'react';
@@ -18,7 +19,7 @@ const VersionInfo: React.FC<{ intl: any }> = ({ intl }) => {
isProd,
isDev
} = gpuStackVersionAtom;
const { sidebarLogo } = useLogo();
// user info
const { is_admin } = userDataAtom || {};
@@ -30,7 +31,7 @@ const VersionInfo: React.FC<{ intl: any }> = ({ intl }) => {
return (
<div className="version-box">
<div className="img">
<img src={Logo} alt="logo" />
<img src={sidebarLogo || Logo} alt="logo" />
</div>
<div className="ver">
+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
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
};
}
-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
};
}
+13 -9
View File
@@ -4,6 +4,7 @@ import VersionInfo, { modalConfig } from '@/components/version-info';
import externalLinks from '@/constants/external-links';
import useBodyScroll from '@/hooks/use-body-scroll';
import { logout } from '@/pages/login/apis';
import { getGPUStackPlugin } from '@/plugins';
import { useModel } from '@@/plugin-model';
import {
DiscordOutlined,
@@ -98,6 +99,7 @@ const CustomItem = styled.div`
export const ExtraContent = (props: { isDarkTheme?: boolean }) => {
const { isDarkTheme } = props;
const plugin = getGPUStackPlugin();
const { saveScrollHeight, restoreScrollHeight } = useBodyScroll();
const [modal, contextHolder] = Modal.useModal();
const [version] = useAtom(GPUStackVersionAtom);
@@ -286,15 +288,17 @@ export const ExtraContent = (props: { isDarkTheme?: boolean }) => {
</NewLabel>
)}
</div>
<DropdownActions menu={{ ...helpMenu }} popupRender={helpPopupRender}>
<IconWrapper>
<IconFont
type="icon-help"
className="font-size-20"
style={{ color: 'var(--ant-color-text-tertiary)' }}
/>
</IconWrapper>
</DropdownActions>
{!plugin && (
<DropdownActions menu={{ ...helpMenu }} popupRender={helpPopupRender}>
<IconWrapper>
<IconFont
type="icon-help"
className="font-size-20"
style={{ color: 'var(--ant-color-text-tertiary)' }}
/>
</IconWrapper>
</DropdownActions>
)}
<PluginExtraField name="GlobalSettings" />
<DropdownActions menu={{ ...userMenu }} popupRender={userPopupRender}>
<IconWrapper>
+5 -29
View File
@@ -52,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',
@@ -269,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]);
@@ -469,15 +449,11 @@ export default (props: any) => {
unAccessible={runtimeConfig?.unAccessible}
noAccessible={runtimeConfig?.noAccessible}
>
{isNoContainerPage ? (
<Outlet />
) : (
<PageContainerInner>
<div>
<Outlet />
</div>
</PageContainerInner>
)}
<PageContainerInner>
<div>
<Outlet />
</div>
</PageContainerInner>
</Exception>
</div>
{NoResourceModal}
+2
View File
@@ -44,6 +44,8 @@ export default {
'On the Worker that needs to be added, run the following command to join it to the cluster.',
'clusters.create.addCommand.k8s.tips':
'On the Kubernetes cluster that needs to be registered, run the following command to create the Kubernetes resources and register the cluster.',
'clusters.create.addCommand.k8s.version.warning':
'The minimum supported Kubernetes version is 1.23. To use the GPU Service feature, the minimum supported Kubernetes version is 1.27.',
'clusters.create.register.tips':
'On the Kubernetes cluster that needs to be added, run the following command to join its nodes to the cluster.',
'cluster.create.checkEnv.tips':
+4
View File
@@ -267,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',
+1
View File
@@ -2,6 +2,7 @@ export default {
'gpuservice.template': 'GPU Instance Template',
'gpuservice.template.add': 'Add Instance Template',
'gpuservice.template.edit': 'Edit Instance Template',
'gpuservice.template.clone': 'Clone Instance Template',
'gpuservice.template.filter.name': 'Filter by name',
'gpuservice.template.filter.vendor': 'Filter by vendor',
'gpuservice.template.image': 'Image',
+1 -1
View File
@@ -13,7 +13,7 @@ export default {
'menu.models.modelCatalog': 'Catalog',
'menu.models.catalog': 'Model Catalog',
'menu.models.deployment': 'Deployments',
'menu.models.userModels': 'My Models',
'menu.models.userModels': 'Models',
'menu.models.benchmark': 'Benchmarks',
'menu.models.benchmarkDetail': 'Benchmark Details',
'menu.models.providers': 'Providers',
+5
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',
+2
View File
@@ -44,6 +44,8 @@ export default {
'On the Worker that needs to be added, run the following command to join it to the cluster.',
'clusters.create.addCommand.k8s.tips':
'登録する Kubernetes クラスターで以下のコマンドを実行し、Kubernetes リソースを作成してクラスターを登録します。',
'clusters.create.addCommand.k8s.version.warning':
'サポートされる Kubernetes の最小バージョンは 1.23 です。GPU Service 機能を使用する場合、サポートされる Kubernetes の最小バージョンは 1.27 です。',
'cluster.create.checkEnv.tips':
'Use the following command to check if the environment is ready.',
'clusters.create.register.tips':
+4
View File
@@ -266,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',
+1
View File
@@ -2,6 +2,7 @@ export default {
'gpuservice.template': 'GPU インスタンステンプレート',
'gpuservice.template.add': 'インスタンステンプレートを追加',
'gpuservice.template.edit': 'インスタンステンプレートを編集',
'gpuservice.template.clone': 'インスタンステンプレートを複製',
'gpuservice.template.filter.name': '名前でフィルター',
'gpuservice.template.filter.vendor': 'ベンダーでフィルター',
'gpuservice.template.image': 'コンテナイメージ',
+6 -6
View File
@@ -41,7 +41,7 @@ export default {
'menu.accessControl.organizations': 'Organizations',
'menu.resources.clusters': 'Clusters',
'menu.resources.credentials': 'Cloud Credentials',
'menu.models.userModels': 'My Models',
'menu.models.userModels': 'Models',
'menu.resources.clusterDetail': 'Cluster Detail',
'menu.resources.clusterCreate': 'Create Cluster',
'menu.models.backendsList': 'Inference Backends',
@@ -62,11 +62,11 @@ export default {
// 6. 'menu.accessControl.apikeys': 'API Keys',
// 7. 'menu.accessControl.users': 'Users',
// 8. 'menu.clusterManagement': 'Cluster Management',
// 9. 'menu.clusterManagement.clusters': 'Clusters',
// 10. 'menu.clusterManagement.credentials': 'Cloud Credentials',
// 11. 'menu.models.userModels': 'My Models'
// 12. 'menu.clusterManagement.clusterDetail': 'Cluster Detail',
// 13. 'menu.clusterManagement.clusterCreate': 'Create Cluster',
// 9. 'menu.resources.clusters': 'Clusters',
// 10. 'menu.resources.credentials': 'Cloud Credentials',
// 11. 'menu.models.userModels': 'Models'
// 12. 'menu.resources.clusterDetail': 'Cluster Detail',
// 13. 'menu.resources.clusterCreate': 'Create Cluster',
// 14. 'menu.models.backendsList': 'Inference Backends',
// 15. 'menu.models.benchmark': 'Benchmarks',
// 15. 'menu.models.provider': 'Provider',
+5
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': '一般ユーザー',
+2
View File
@@ -44,6 +44,8 @@ export default {
'На воркере, который необходимо добавить, выполните следующую команду, чтобы присоединить его к кластеру.',
'clusters.create.addCommand.k8s.tips':
'На Kubernetes-кластере, который необходимо зарегистрировать, выполните следующую команду, чтобы создать ресурсы Kubernetes и зарегистрировать этот кластер.',
'clusters.create.addCommand.k8s.version.warning':
'Минимальная поддерживаемая версия Kubernetes — 1.23. Для использования функции GPU Service минимальная поддерживаемая версия Kubernetes — 1.27.',
'cluster.create.checkEnv.tips':
'Используйте следующую команду для проверки готовности окружения',
'clusters.create.register.tips':
+4
View File
@@ -265,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': 'Введите пароль',
+1
View File
@@ -2,6 +2,7 @@ export default {
'gpuservice.template': 'Шаблон экземпляра GPU',
'gpuservice.template.add': 'Добавить шаблон экземпляра',
'gpuservice.template.edit': 'Редактировать шаблон экземпляра',
'gpuservice.template.clone': 'Клонировать шаблон экземпляра',
'gpuservice.template.filter.name': 'Фильтр по имени',
'gpuservice.template.filter.vendor': 'Фильтр по производителю',
'gpuservice.template.image': 'Образ',
+1 -1
View File
@@ -40,7 +40,7 @@ export default {
'menu.accessControl.organizations': 'Организации',
'menu.resources.clusters': 'Кластеры',
'menu.resources.credentials': 'Облачные аккаунты',
'menu.models.userModels': 'Мои модели',
'menu.models.userModels': 'Модели',
'menu.resources.clusterDetail': 'Детали кластера',
'menu.resources.clusterCreate': 'Создать кластер',
'menu.models.backendsList': 'Бэкенды запуска',
+5
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': 'Пользователь',
+2
View File
@@ -44,6 +44,8 @@ export default {
'Eklenmesi gereken İşçi Düğümde, kümeye katılması için aşağıdaki komutu çalıştırın.',
'clusters.create.addCommand.k8s.tips':
'Kaydedilmesi gereken Kubernetes kümesinde, Kubernetes kaynaklarını oluşturmak ve kümeyi kaydetmek için aşağıdaki komutu çalıştırın.',
'clusters.create.addCommand.k8s.version.warning':
'Desteklenen minimum Kubernetes sürümü 1.23tür. GPU Service özelliğini kullanmak için desteklenen minimum Kubernetes sürümü 1.27dir.',
'clusters.create.register.tips':
'Eklenmesi gereken Kubernetes kümesinde, düğümlerini kümeye katılması için aşağıdaki komutu çalıştırın.',
'cluster.create.checkEnv.tips':
+4
View File
@@ -269,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',
+1
View File
@@ -2,6 +2,7 @@ export default {
'gpuservice.template': 'GPU Örnek Şablonu',
'gpuservice.template.add': 'Örnek Şablonu Ekle',
'gpuservice.template.edit': 'Örnek Şablonunu Düzenle',
'gpuservice.template.clone': 'Örnek Şablonunu Klonla',
'gpuservice.template.filter.name': 'Ada göre filtrele',
'gpuservice.template.filter.vendor': 'Tedarikçiye göre filtrele',
'gpuservice.template.image': 'İmaj',
+2 -2
View File
@@ -13,12 +13,12 @@ export default {
'menu.models.modelCatalog': 'Katalog',
'menu.models.catalog': 'Model Kataloğu',
'menu.models.deployment': 'Dağıtımlar',
'menu.models.userModels': 'Modellerim',
'menu.models.userModels': 'Modeller',
'menu.models.benchmark': 'Kıyaslamalar',
'menu.models.benchmarkDetail': 'Kıyaslama Detayları',
'menu.models.providers': 'Sağlayıcılar',
'menu.models.routes': 'Yönlendirmeler',
'menu.models.usage': 'Usage',
'menu.models.usage': 'Kullanım',
'menu.modelCatalog': 'Katalog',
'menu.resources': 'Kaynaklar',
'menu.apikeys': 'API Anahtarları',
+5
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ı',
+2
View File
@@ -43,6 +43,8 @@ export default {
'在需要添加的节点上运行以下命令,将其加入到集群中。',
'clusters.create.addCommand.k8s.tips':
'在需要注册的 Kubernetes 集群中运行以下命令,创建 Kubernetes 资源,注册该集群。',
'clusters.create.addCommand.k8s.version.warning':
'Kubernetes 版本最低支持 1.23,如果要使用 GPU Service 功能,Kubernetes 版本最低支持 1.27。',
'clusters.create.register.tips':
'在需要添加的 Kubernetes 集群上运行以下命令,将其中的节点加入到集群中。',
'cluster.create.checkEnv.tips': '使用以下命令检查环境是否准备妥当。',
+3
View File
@@ -257,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': '请输入密码',
+1
View File
@@ -2,6 +2,7 @@ export default {
'gpuservice.template': 'GPU 实例模板',
'gpuservice.template.add': '添加实例模板',
'gpuservice.template.edit': '编辑实例模板',
'gpuservice.template.clone': '克隆实例模板',
'gpuservice.template.filter.name': '按名称过滤',
'gpuservice.template.filter.vendor': '按厂商过滤',
'gpuservice.template.image': '镜像',
+1 -1
View File
@@ -12,7 +12,7 @@ export default {
'menu.models.modelList': '部署与管理',
'menu.models.modelCatalog': '模型库',
'menu.models.deployment': '部署',
'menu.models.userModels': '我的模型',
'menu.models.userModels': '模型广场',
'menu.models.benchmark': '基准测试',
'menu.models.benchmarkDetail': '基准测试详情',
'menu.models.providers': '提供商',
+5
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': '普通用户',
+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';
+107 -40
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,43 +59,85 @@ 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 (
<div className={pageBoxCss.containerWrapper}>
<PageContainer
{...rest}
fixedHeader={false}
title={false}
pageHeaderRender={false}
style={{
flex: 1
}}
token={{
paddingInlinePageContainerContent: paddingInlinePageContainerContent
}}
>
<div className={pageBoxCss.title}>
<div className={pageBoxCss.left}>
{leftContent || pageContext.title}
</div>
<div className={pageBoxCss.right}>
{rightContent && (
<div>
{rightContent}
<Divider orientation="vertical" style={{ margin: '0 16px' }} />
</div>
)}
<ExtraContent />
</div>
</div>
<div
className={classNames(pageBoxCss.contentWrapper)}
style={styles?.containerWrapper}
ref={contentWrapperRef}
<HeaderSlotContext.Provider value={slotValue}>
<div className={pageBoxCss.containerWrapper}>
<PageContainer
{...rest}
fixedHeader={false}
title={false}
pageHeaderRender={false}
style={{
flex: 1
}}
token={{
paddingInlinePageContainerContent: paddingInlinePageContainerContent
}}
>
{children}
</div>
</PageContainer>
</div>
<div className={pageBoxCss.title}>
<div className={pageBoxCss.left}>
<div ref={setLeftEl} className={pageBoxCss.leftSlot} />
<span className={pageBoxCss.defaultTitle}>
{pageContext.title}
</span>
</div>
<div className={pageBoxCss.right}>
<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, ...contentStyleOverride }}
ref={contentWrapperRef}
>
{children}
</div>
</PageContainer>
</div>
</HeaderSlotContext.Provider>
);
};
+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 React, {
@@ -70,14 +70,44 @@
height: 56px;
border-bottom: 1px solid var(--ant-color-split);
// `.left` claims all the space between the title's left edge and `.right`.
// Because the container width is fixed regardless of inner content, the
// right side never shifts during route transitions and the content inside
// .left is anchored to the left edge — no horizontal jitter.
.left {
display: flex;
align-items: center;
flex: 1 1 auto;
min-width: 0;
}
.right {
display: flex;
align-items: center;
flex-shrink: 0;
}
.leftSlot,
.rightSlot {
display: contents;
}
.defaultTitle {
display: inline-flex;
align-items: center;
}
// Portal-driven visibility: when a page's HeaderLeft / HeaderRight
// portal target has children, hide the default title / show the divider.
// Using `:empty` (rather than React state) keeps visibility synchronous
// with the DOM mutation, so there is no inter-frame flicker on route
// transitions.
.leftSlot:not(:empty) ~ .defaultTitle {
display: none;
}
.rightSlot:empty ~ .divider {
display: none;
}
}
}
-24
View File
@@ -1,24 +0,0 @@
import { PageContainer } from '@ant-design/pro-components';
import { Access, useAccess } from '@umijs/max';
import { Button } from 'antd';
const AccessPage: React.FC = () => {
const access = useAccess();
return (
<PageContainer
ghost
header={{
title: 'permission example',
style: {
paddingInline: 'var(--layout-content-header-inlinepadding)'
}
}}
>
<Access accessible={access.canSeeAdmin}>
<Button>Only Admin can see this button</Button>
</Access>
</PageContainer>
);
};
export default AccessPage;
+4 -1
View File
@@ -187,7 +187,10 @@ const APIKeys: React.FC = () => {
loadend={dataSource.loadend}
dataSource={dataSource.dataList}
image={<IconFont type="icon-key" />}
filters={_.omit(queryParams, ['sort_by'])}
filters={{
..._.omit(queryParams, ['sort_by']),
user_id: queryParams.user_id === '*' ? undefined : queryParams.user_id
}}
noFoundText={intl.formatMessage({
id: 'noresult.keys.nofound'
})}
@@ -1,6 +1,6 @@
import FullMarkdown from '@/components/full-markdown';
import { BulbOutlined } from '@ant-design/icons';
import { AutoTooltip } from '@gpustack/core-ui';
import { FullMarkdown } from '@gpustack/core-ui/markdown';
import { useIntl } from '@umijs/max';
import { Button, Tag } from 'antd';
import _ from 'lodash';
@@ -1,5 +1,5 @@
import { IconFont } from '@gpustack/core-ui';
import { useIntl, useSearchParams } from '@umijs/max';
import { useIntl } from '@umijs/max';
import { Tabs, TabsProps } from 'antd';
import React, { useState } from 'react';
import Environment from './environment';
@@ -12,7 +12,6 @@ const Details: React.FC<{
}> = ({ tabBarExtraContent }) => {
const intl = useIntl();
const [activeKey, setActiveKey] = useState('summary');
const [searchParams] = useSearchParams();
const items: TabsProps['items'] = [
{
+6 -5
View File
@@ -2,7 +2,7 @@ import { BaseSelect, DeleteModal } from '@gpustack/core-ui';
import { useIntl, useNavigate, useSearchParams } from '@umijs/max';
import { useMemoizedFn } from 'ahooks';
import React, { useEffect, useRef } from 'react';
import { PageContainerInner } from '../_components/page-box';
import { HeaderLeft } from '../_components/page-box';
import PageBreadcrumb from '../_components/page-breadcrumb';
import { deleteBenchmark } from './apis';
import DetailContent from './components/detail-content';
@@ -122,9 +122,10 @@ const Details: React.FC = () => {
}, [id]);
return (
<PageContainerInner
leftContent={<PageBreadcrumb items={breadcrumbItems} />}
>
<>
<HeaderLeft>
<PageBreadcrumb items={breadcrumbItems} />
</HeaderLeft>
<DetailContext.Provider
value={{
detailData: detailData || {},
@@ -164,7 +165,7 @@ const Details: React.FC = () => {
onCancel={closeViewLogsModal}
></ViewLogsModal>
<DeleteModal ref={modalRef}></DeleteModal>
</PageContainerInner>
</>
);
};

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