Compare commits

..
Author SHA1 Message Date
jialin a19fea0c6c 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 16:57:17 +08:00
223 changed files with 1950 additions and 2919 deletions
-18
View File
@@ -1,7 +1,6 @@
name: CI name: CI
on: on:
workflow_dispatch: {}
push: push:
branches: branches:
- 'main' - 'main'
@@ -118,20 +117,3 @@ jobs:
remote_path: releases/${{ steps.version.outputs.version }}.tar.gz remote_path: releases/${{ steps.version.outputs.version }}.tar.gz
accelerate: true accelerate: true
clean: false clean: false
trigger-backend:
needs: build-publish
if: (github.event_name == 'push' && !startsWith(github.ref, 'refs/tags/')) || github.event_name == 'workflow_dispatch'
runs-on: ubuntu-22.04
steps:
- name: Dispatch backend build
uses: peter-evans/repository-dispatch@v3
with:
token: ${{ secrets.DISPATCH_PAT }}
repository: gpustack/gpustack
event-type: ui-built
client-payload: |
{
"ref": "${{ github.ref }}",
"sha": "${{ github.sha }}"
}
-11
View File
@@ -88,16 +88,6 @@ 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. 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 # Naming conventions
A page module lives under `src/pages/{module}` with this sub-structure: `components/`, `config/`, `forms/`, `hooks/`, `services/`, `index.tsx`. File naming: A page module lives under `src/pages/{module}` with this sub-structure: `components/`, `config/`, `forms/`, `hooks/`, `services/`, `index.tsx`. File naming:
@@ -111,7 +101,6 @@ 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/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`. - `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 # Common components
+21 -10
View File
@@ -100,15 +100,6 @@ const baseRoutes = [
path: '/models', path: '/models',
redirect: '/models/deployments' 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', name: 'modelCatalog',
path: '/models/catalog', path: '/models/catalog',
@@ -119,6 +110,16 @@ const baseRoutes = [
access: 'canSeeOrgAdmin', access: 'canSeeOrgAdmin',
component: './llmodels/catalog' 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', name: 'deployment',
path: '/models/deployments', path: '/models/deployments',
@@ -273,7 +274,7 @@ const baseRoutes = [
selectedIcon: 'icon-cluster2-filled', selectedIcon: 'icon-cluster2-filled',
defaultIcon: 'icon-cluster2-outline', defaultIcon: 'icon-cluster2-outline',
component: './cluster-management/clusters', component: './cluster-management/clusters',
subMenu: ['/resources/clusters/create'] subMenu: ['/resources/clusters/detail', '/resources/clusters/create']
}, },
{ {
name: 'workers', name: 'workers',
@@ -301,6 +302,16 @@ const baseRoutes = [
selectedIcon: 'icon-credential-filled', selectedIcon: 'icon-credential-filled',
defaultIcon: 'icon-credential-outline', defaultIcon: 'icon-credential-outline',
component: './cluster-management/credentials' 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'
} }
] ]
}, },
+2 -1
View File
@@ -17,7 +17,7 @@
"@ant-design/pro-components": "3.1.0-0", "@ant-design/pro-components": "3.1.0-0",
"@antv/g6": "^5.0.51", "@antv/g6": "^5.0.51",
"@braintree/sanitize-url": "^7.1.1", "@braintree/sanitize-url": "^7.1.1",
"@gpustack/core-ui": "^1.0.36", "@gpustack/core-ui": "^1.0.32",
"@huggingface/gguf": "^0.1.7", "@huggingface/gguf": "^0.1.7",
"@huggingface/hub": "^0.15.1", "@huggingface/hub": "^0.15.1",
"@huggingface/tasks": "^0.11.6", "@huggingface/tasks": "^0.11.6",
@@ -39,6 +39,7 @@
"culori": "^4.0.2", "culori": "^4.0.2",
"dayjs": "^1.11.11", "dayjs": "^1.11.11",
"dompurify": "^3.2.6", "dompurify": "^3.2.6",
"driver.js": "^1.3.1",
"echarts": "^5.5.1", "echarts": "^5.5.1",
"file-saver": "^2.0.5", "file-saver": "^2.0.5",
"has-ansi": "^5.0.1", "has-ansi": "^5.0.1",
+13 -5
View File
@@ -24,8 +24,8 @@ importers:
specifier: ^7.1.1 specifier: ^7.1.1
version: 7.1.2 version: 7.1.2
'@gpustack/core-ui': '@gpustack/core-ui':
specifier: ^1.0.36 specifier: ^1.0.32
version: 1.0.36(czdvzceysqw7iv6pct2ucnb23e) version: 1.0.32(czdvzceysqw7iv6pct2ucnb23e)
'@huggingface/gguf': '@huggingface/gguf':
specifier: ^0.1.7 specifier: ^0.1.7
version: 0.1.18 version: 0.1.18
@@ -89,6 +89,9 @@ importers:
dompurify: dompurify:
specifier: ^3.2.6 specifier: ^3.2.6
version: 3.4.2 version: 3.4.2
driver.js:
specifier: ^1.3.1
version: 1.4.0
echarts: echarts:
specifier: ^5.5.1 specifier: ^5.5.1
version: 5.6.0 version: 5.6.0
@@ -1481,8 +1484,8 @@ packages:
resolution: {integrity: sha512-KWk80UPIzPmUg+P0rKh6TqspRw0G6eux1PuJr+zz47ftMaZ9QDwbGzHZbtzWkl5hgayM/qrKRutllRC7D/vVXQ==, tarball: https://registry.npmjs.org/@formatjs/intl-utils/-/intl-utils-2.3.0.tgz} resolution: {integrity: sha512-KWk80UPIzPmUg+P0rKh6TqspRw0G6eux1PuJr+zz47ftMaZ9QDwbGzHZbtzWkl5hgayM/qrKRutllRC7D/vVXQ==, tarball: https://registry.npmjs.org/@formatjs/intl-utils/-/intl-utils-2.3.0.tgz}
deprecated: the package is rather renamed to @formatjs/ecma-abstract with some changes in functionality (primarily selectUnit is removed and we don't plan to make any further changes to this package deprecated: the package is rather renamed to @formatjs/ecma-abstract with some changes in functionality (primarily selectUnit is removed and we don't plan to make any further changes to this package
'@gpustack/core-ui@1.0.36': '@gpustack/core-ui@1.0.32':
resolution: {integrity: sha512-gF8ShMZ2SKYo3+skfsI3zo+aBSsXqKGMX+jWaKv81e38bDwJw71C33TR64d+6MyV9iUoFOoSK+ht1GefyvR17g==, tarball: https://registry.npmjs.org/@gpustack/core-ui/-/core-ui-1.0.36.tgz} resolution: {integrity: sha512-kGTazoqbK2KyZgOP6gmQaRxTiQVfF2IKLGDXjJq6w6BbmJgALXFJA2v2ROjAbjEVyfTdBzyYeXfPo/JgISpMNw==, tarball: https://registry.npmjs.org/@gpustack/core-ui/-/core-ui-1.0.32.tgz}
peerDependencies: peerDependencies:
'@ant-design/icons': ^6.1.0 '@ant-design/icons': ^6.1.0
'@ant-design/pro-components': 3.1.0-0 '@ant-design/pro-components': 3.1.0-0
@@ -4317,6 +4320,9 @@ packages:
dot-case@3.0.4: dot-case@3.0.4:
resolution: {integrity: sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==, tarball: https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz} resolution: {integrity: sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==, tarball: https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz}
driver.js@1.4.0:
resolution: {integrity: sha512-Gm64jm6PmcU+si21sQhBrTAM1JvUrR0QhNmjkprNLxohOBzul9+pNHXgQaT9lW84gwg9GMLB3NZGuGolsz5uew==, tarball: https://registry.npmjs.org/driver.js/-/driver.js-1.4.0.tgz}
duck@0.1.12: duck@0.1.12:
resolution: {integrity: sha512-wkctla1O6VfP89gQ+J/yDesM0S7B7XLXjKGzXxMDVFg7uEn706niAtyYovKbyq1oT9YwDcly721/iUWoc8MVRg==, tarball: https://registry.npmjs.org/duck/-/duck-0.1.12.tgz} resolution: {integrity: sha512-wkctla1O6VfP89gQ+J/yDesM0S7B7XLXjKGzXxMDVFg7uEn706niAtyYovKbyq1oT9YwDcly721/iUWoc8MVRg==, tarball: https://registry.npmjs.org/duck/-/duck-0.1.12.tgz}
@@ -10802,7 +10808,7 @@ snapshots:
'@formatjs/intl-utils@2.3.0': {} '@formatjs/intl-utils@2.3.0': {}
'@gpustack/core-ui@1.0.36(czdvzceysqw7iv6pct2ucnb23e)': '@gpustack/core-ui@1.0.32(czdvzceysqw7iv6pct2ucnb23e)':
dependencies: dependencies:
'@ant-design/icons': 6.2.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@ant-design/icons': 6.2.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@ant-design/pro-components': 3.1.0-0(antd@6.3.7(date-fns@2.30.0)(moment@2.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@ant-design/pro-components': 3.1.0-0(antd@6.3.7(date-fns@2.30.0)(moment@2.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@@ -14548,6 +14554,8 @@ snapshots:
no-case: 3.0.4 no-case: 3.0.4
tslib: 2.8.1 tslib: 2.8.1
driver.js@1.4.0: {}
duck@0.1.12: duck@0.1.12:
dependencies: dependencies:
underscore: 1.13.8 underscore: 1.13.8

Before

Width:  |  Height:  |  Size: 7.0 KiB

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 196 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 640 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 801 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

+1 -2
View File
@@ -1,4 +1,3 @@
import { nsLocalJSONStorage } from '@gpustack/core-ui/utils';
import { getDefaultStore } from 'jotai'; import { getDefaultStore } from 'jotai';
import { atomWithStorage } from 'jotai/utils'; import { atomWithStorage } from 'jotai/utils';
@@ -9,7 +8,7 @@ export interface PaginationState {
export const paginationAtom = atomWithStorage<Record<string, any>>( export const paginationAtom = atomWithStorage<Record<string, any>>(
'paginationStatus', 'paginationStatus',
{}, {},
nsLocalJSONStorage, undefined,
{ getOnInit: true } { getOnInit: true }
); );
+6 -9
View File
@@ -1,5 +1,4 @@
import { COLOR_PRIMARY } from '@/config/theme'; import { COLOR_PRIMARY } from '@/config/theme';
import { nsLocal, nsLocalJSONStorage } from '@gpustack/core-ui/utils';
import { atom } from 'jotai'; import { atom } from 'jotai';
import { atomWithStorage } from 'jotai/utils'; import { atomWithStorage } from 'jotai/utils';
@@ -24,7 +23,9 @@ export const defaultSettings: UserSettings = {
export const getStorageUserSettings = () => { export const getStorageUserSettings = () => {
if (typeof window === 'undefined') return defaultSettings; if (typeof window === 'undefined') return defaultSettings;
try { try {
const savedSettings = JSON.parse(nsLocal.get('userSettings') || '{}'); const savedSettings = JSON.parse(
localStorage.getItem('userSettings') || '{}'
);
return { return {
...defaultSettings, ...defaultSettings,
...savedSettings ...savedSettings
@@ -34,13 +35,9 @@ export const getStorageUserSettings = () => {
} }
}; };
export const userSettingsAtom = atomWithStorage<UserSettings>( export const userSettingsAtom = atomWithStorage<UserSettings>('userSettings', {
'userSettings',
{
...getStorageUserSettings() ...getStorageUserSettings()
}, });
nsLocalJSONStorage
);
export const userSettingsHelperAtom = atom( export const userSettingsHelperAtom = atom(
(get) => get(userSettingsAtom), (get) => get(userSettingsAtom),
@@ -62,6 +59,6 @@ export const hideModalTemporarilyAtom = atom<boolean>(false);
export const collapsedMenuGroupsAtom = atomWithStorage<string[]>( export const collapsedMenuGroupsAtom = atomWithStorage<string[]>(
'collapsedMenuGroups', 'collapsedMenuGroups',
[], [],
nsLocalJSONStorage, undefined,
{ getOnInit: true } { getOnInit: true }
); );
+1 -3
View File
@@ -1,11 +1,9 @@
import { nsLocalJSONStorage } from '@gpustack/core-ui/utils';
import { getDefaultStore } from 'jotai'; import { getDefaultStore } from 'jotai';
import { atomWithStorage } from 'jotai/utils'; import { atomWithStorage } from 'jotai/utils';
export const tabActiveAtom = atomWithStorage<Map<string, any>>( export const tabActiveAtom = atomWithStorage<Map<string, any>>(
'tabActiveStatus', 'tabActiveStatus',
new Map(), new Map()
nsLocalJSONStorage
); );
export const setActiveStatus = (key: string, value: any) => { export const setActiveStatus = (key: string, value: any) => {
+4 -10
View File
@@ -1,12 +1,7 @@
import { nsLocal, nsLocalJSONStorage } from '@gpustack/core-ui/utils';
import { atom } from 'jotai'; import { atom } from 'jotai';
import { atomWithStorage } from 'jotai/utils'; import { atomWithStorage } from 'jotai/utils';
export const userAtom = atomWithStorage<any>( export const userAtom = atomWithStorage<any>('userInfo', null);
'userInfo',
null,
nsLocalJSONStorage
);
// Backs the `currentOrganizationId` localStorage key. Stays null in // Backs the `currentOrganizationId` localStorage key. Stays null in
// builds with no Org context (single-tenant), and is shared with any // builds with no Org context (single-tenant), and is shared with any
@@ -14,8 +9,7 @@ export const userAtom = atomWithStorage<any>(
// without one side having to import from the other. // without one side having to import from the other.
export const currentOrganizationIdAtom = atomWithStorage<number | null>( export const currentOrganizationIdAtom = atomWithStorage<number | null>(
'currentOrganizationId', 'currentOrganizationId',
null, null
nsLocalJSONStorage
); );
export const GPUStackVersionAtom = atom<{ export const GPUStackVersionAtom = atom<{
@@ -76,7 +70,7 @@ export const getCurrentOrgNamespace = (
const getStoredCurrentOrgId = (): number | null => { const getStoredCurrentOrgId = (): number | null => {
try { try {
const raw = nsLocal.get('currentOrganizationId'); const raw = localStorage.getItem('currentOrganizationId');
if (!raw) return null; if (!raw) return null;
const value = JSON.parse(raw); const value = JSON.parse(raw);
return typeof value === 'number' ? value : null; return typeof value === 'number' ? value : null;
@@ -114,7 +108,7 @@ export const getOrgById = (
const target = String(id); const target = String(id);
for (const key of ORG_CACHE_KEYS) { for (const key of ORG_CACHE_KEYS) {
try { try {
const raw = nsLocal.get(key); const raw = localStorage.getItem(key);
if (!raw) continue; if (!raw) continue;
const list = JSON.parse(raw) as CachedOrg[]; const list = JSON.parse(raw) as CachedOrg[];
if (!Array.isArray(list)) continue; if (!Array.isArray(list)) continue;
+5 -4
View File
@@ -1,17 +1,18 @@
import { defaultSettings } from '@/atoms/settings'; import { defaultSettings } from '@/atoms/settings';
import { nsLocal } from '@gpustack/core-ui/utils';
import { getDefaultStore } from 'jotai'; import { getDefaultStore } from 'jotai';
export const clearStorageUserSettings = () => { export const clearStorageUserSettings = () => {
try { try {
const savedSettings = JSON.parse(nsLocal.get('userSettings') || '{}'); const savedSettings = JSON.parse(
localStorage.getItem('userSettings') || '{}'
);
// colorPrimary is an enterprise-wide branding setting (set by admins // colorPrimary is an enterprise-wide branding setting (set by admins
// and applied by `onAppInit` from /enterprise/settings), not a per-user // and applied by `onAppInit` from /enterprise/settings), not a per-user
// preference. Preserve it across login — otherwise the next layout // preference. Preserve it across login — otherwise the next layout
// mount triggers `atomWithStorage.onMount`, re-reads localStorage, // mount triggers `atomWithStorage.onMount`, re-reads localStorage,
// and falls back to the default color until a full page refresh // and falls back to the default color until a full page refresh
// re-runs `applyEnterpriseSettings`. // re-runs `applyEnterpriseSettings`.
nsLocal.set( localStorage.setItem(
'userSettings', 'userSettings',
JSON.stringify({ JSON.stringify({
...savedSettings, ...savedSettings,
@@ -25,7 +26,7 @@ export const clearStorageUserSettings = () => {
export const resetStorageUserSettings = () => { export const resetStorageUserSettings = () => {
try { try {
nsLocal.set( localStorage.setItem(
'userSettings', 'userSettings',
JSON.stringify({ JSON.stringify({
...defaultSettings, ...defaultSettings,
+2 -3
View File
@@ -1,10 +1,10 @@
import { GPUStackVersionAtom } from '@/atoms/user'; import { GPUStackVersionAtom } from '@/atoms/user';
import { getAtomStorage } from '@/atoms/utils';
import VersionInfo, { modalConfig } from '@/components/version-info'; import VersionInfo, { modalConfig } from '@/components/version-info';
import externalLinks from '@/constants/external-links'; import externalLinks from '@/constants/external-links';
import { useIntl } from '@umijs/max'; import { useIntl } from '@umijs/max';
import { Button, Divider, Modal, Typography } from 'antd'; import { Button, Divider, Modal, Typography } from 'antd';
import { createStyles } from 'antd-style'; import { createStyles } from 'antd-style';
import { useAtomValue } from 'jotai';
import styled from 'styled-components'; import styled from 'styled-components';
const CompanyWrapper = styled.div` const CompanyWrapper = styled.div`
@@ -35,7 +35,6 @@ const Footer: React.FC = () => {
const intl = useIntl(); const intl = useIntl();
const [modal, contextHolder] = Modal.useModal(); const [modal, contextHolder] = Modal.useModal();
const { styles } = useStyles(); const { styles } = useStyles();
const version = useAtomValue(GPUStackVersionAtom);
const showVersion = () => { const showVersion = () => {
modal.info({ modal.info({
@@ -74,7 +73,7 @@ const Footer: React.FC = () => {
</Button> </Button>
<Divider orientation="vertical" /> <Divider orientation="vertical" />
<Button type="link" size="small" onClick={showVersion}> <Button type="link" size="small" onClick={showVersion}>
{version?.version} {getAtomStorage(GPUStackVersionAtom)?.version}
</Button> </Button>
</div> </div>
</div> </div>
-12
View File
@@ -1,12 +0,0 @@
// 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;
+2 -3
View File
@@ -1,7 +1,6 @@
import Logo from '@/assets/images/gpustack-logo.png'; import Logo from '@/assets/images/gpustack-logo.png';
import { GPUStackVersionAtom, UpdateCheckAtom, userAtom } from '@/atoms/user'; import { GPUStackVersionAtom, UpdateCheckAtom, userAtom } from '@/atoms/user';
import externalLinks from '@/constants/external-links'; import externalLinks from '@/constants/external-links';
import { useLogo } from '@/hooks/use-logo';
import { Button } from 'antd'; import { Button } from 'antd';
import { useAtom } from 'jotai'; import { useAtom } from 'jotai';
import React from 'react'; import React from 'react';
@@ -19,7 +18,7 @@ const VersionInfo: React.FC<{ intl: any }> = ({ intl }) => {
isProd, isProd,
isDev isDev
} = gpuStackVersionAtom; } = gpuStackVersionAtom;
const { sidebarLogo } = useLogo();
// user info // user info
const { is_admin } = userDataAtom || {}; const { is_admin } = userDataAtom || {};
@@ -31,7 +30,7 @@ const VersionInfo: React.FC<{ intl: any }> = ({ intl }) => {
return ( return (
<div className="version-box"> <div className="version-box">
<div className="img"> <div className="img">
<img src={sidebarLogo || Logo} alt="logo" /> <img src={Logo} alt="logo" />
</div> </div>
<div className="ver"> <div className="ver">
-1
View File
@@ -28,7 +28,6 @@ export default {
rowSelectedBg: 'transparent', rowSelectedBg: 'transparent',
headerSortActiveBg: 'transparent', headerSortActiveBg: 'transparent',
headerSortHoverBg: 'transparent', headerSortHoverBg: 'transparent',
bodySortBg: 'transparent',
headerBg: 'none' headerBg: 'none'
}, },
Button: { Button: {
-1
View File
@@ -31,7 +31,6 @@ export default {
rowSelectedBg: 'transparent', rowSelectedBg: 'transparent',
headerSortActiveBg: 'transparent', headerSortActiveBg: 'transparent',
headerSortHoverBg: 'transparent', headerSortHoverBg: 'transparent',
bodySortBg: 'transparent',
headerSplitColor: '#e8e8e8', headerSplitColor: '#e8e8e8',
headerBg: 'none' headerBg: 'none'
}, },
+10
View File
@@ -0,0 +1,10 @@
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
@@ -0,0 +1,58 @@
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
@@ -0,0 +1,18 @@
// 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
@@ -0,0 +1,56 @@
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
@@ -0,0 +1,23 @@
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
@@ -0,0 +1,71 @@
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
@@ -0,0 +1,136 @@
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
@@ -0,0 +1,35 @@
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
@@ -0,0 +1,106 @@
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
@@ -0,0 +1,72 @@
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
@@ -0,0 +1,280 @@
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
};
}
-4
View File
@@ -4,7 +4,6 @@ import VersionInfo, { modalConfig } from '@/components/version-info';
import externalLinks from '@/constants/external-links'; import externalLinks from '@/constants/external-links';
import useBodyScroll from '@/hooks/use-body-scroll'; import useBodyScroll from '@/hooks/use-body-scroll';
import { logout } from '@/pages/login/apis'; import { logout } from '@/pages/login/apis';
import { getGPUStackPlugin } from '@/plugins';
import { useModel } from '@@/plugin-model'; import { useModel } from '@@/plugin-model';
import { import {
DiscordOutlined, DiscordOutlined,
@@ -99,7 +98,6 @@ const CustomItem = styled.div`
export const ExtraContent = (props: { isDarkTheme?: boolean }) => { export const ExtraContent = (props: { isDarkTheme?: boolean }) => {
const { isDarkTheme } = props; const { isDarkTheme } = props;
const plugin = getGPUStackPlugin();
const { saveScrollHeight, restoreScrollHeight } = useBodyScroll(); const { saveScrollHeight, restoreScrollHeight } = useBodyScroll();
const [modal, contextHolder] = Modal.useModal(); const [modal, contextHolder] = Modal.useModal();
const [version] = useAtom(GPUStackVersionAtom); const [version] = useAtom(GPUStackVersionAtom);
@@ -288,7 +286,6 @@ export const ExtraContent = (props: { isDarkTheme?: boolean }) => {
</NewLabel> </NewLabel>
)} )}
</div> </div>
{!plugin && (
<DropdownActions menu={{ ...helpMenu }} popupRender={helpPopupRender}> <DropdownActions menu={{ ...helpMenu }} popupRender={helpPopupRender}>
<IconWrapper> <IconWrapper>
<IconFont <IconFont
@@ -298,7 +295,6 @@ export const ExtraContent = (props: { isDarkTheme?: boolean }) => {
/> />
</IconWrapper> </IconWrapper>
</DropdownActions> </DropdownActions>
)}
<PluginExtraField name="GlobalSettings" /> <PluginExtraField name="GlobalSettings" />
<DropdownActions menu={{ ...userMenu }} popupRender={userPopupRender}> <DropdownActions menu={{ ...userMenu }} popupRender={userPopupRender}>
<IconWrapper> <IconWrapper>
+5 -3
View File
@@ -1,6 +1,5 @@
import externalLinks from '@/constants/external-links'; import externalLinks from '@/constants/external-links';
import { GithubFilled } from '@ant-design/icons'; import { GithubFilled } from '@ant-design/icons';
import { nsLocal } from '@gpustack/core-ui/utils';
import { useIntl } from '@umijs/max'; import { useIntl } from '@umijs/max';
import { Tooltip } from 'antd'; import { Tooltip } from 'antd';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
@@ -67,7 +66,7 @@ type CacheEntry = { value: number; time: number };
const readCache = (): CacheEntry | null => { const readCache = (): CacheEntry | null => {
try { try {
const raw = nsLocal.get(CACHE_KEY); const raw = localStorage.getItem(CACHE_KEY);
if (!raw) return null; if (!raw) return null;
const parsed = JSON.parse(raw); const parsed = JSON.parse(raw);
if (typeof parsed?.value !== 'number' || typeof parsed?.time !== 'number') { if (typeof parsed?.value !== 'number' || typeof parsed?.time !== 'number') {
@@ -81,7 +80,10 @@ const readCache = (): CacheEntry | null => {
const writeCache = (value: number) => { const writeCache = (value: number) => {
try { try {
nsLocal.set(CACHE_KEY, JSON.stringify({ value, time: Date.now() })); localStorage.setItem(
CACHE_KEY,
JSON.stringify({ value, time: Date.now() })
);
} catch { } catch {
// ignore quota errors // ignore quota errors
} }
+24
View File
@@ -52,6 +52,21 @@ import { ExtraContent } from './extraRender';
import { patchRoutes } from './runtime'; import { patchRoutes } from './runtime';
import SiderMenu from './sider-menu'; 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 = [ const CHECK_RESOURCE_PATH = [
'/resources/workers', '/resources/workers',
'/resources/clusters/list', '/resources/clusters/list',
@@ -254,6 +269,11 @@ export default (props: any) => {
[location.pathname] [location.pathname]
); );
const isNoContainerPage = useMemo(() => {
// @ts-ignore
return NO_CONTAINER_PAGES.includes(matchedRoute?.name as string);
}, [matchedRoute]);
const collapsed = useMemo(() => { const collapsed = useMemo(() => {
return userSettings.collapsed || false; return userSettings.collapsed || false;
}, [userSettings.collapsed]); }, [userSettings.collapsed]);
@@ -449,11 +469,15 @@ export default (props: any) => {
unAccessible={runtimeConfig?.unAccessible} unAccessible={runtimeConfig?.unAccessible}
noAccessible={runtimeConfig?.noAccessible} noAccessible={runtimeConfig?.noAccessible}
> >
{isNoContainerPage ? (
<Outlet />
) : (
<PageContainerInner> <PageContainerInner>
<div> <div>
<Outlet /> <Outlet />
</div> </div>
</PageContainerInner> </PageContainerInner>
)}
</Exception> </Exception>
</div> </div>
{NoResourceModal} {NoResourceModal}
-2
View File
@@ -44,8 +44,6 @@ export default {
'On the Worker that needs to be added, run the following command to join it to the cluster.', 'On the Worker that needs to be added, run the following command to join it to the cluster.',
'clusters.create.addCommand.k8s.tips': '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.', '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': 'clusters.create.register.tips':
'On the Kubernetes cluster that needs to be added, run the following command to join its nodes to the cluster.', 'On the Kubernetes cluster that needs to be added, run the following command to join its nodes to the cluster.',
'cluster.create.checkEnv.tips': 'cluster.create.checkEnv.tips':
-4
View File
@@ -267,10 +267,6 @@ export default {
'common.select.count': '{count} selected', 'common.select.count': '{count} selected',
'common.login.auth': 'Authenticating...', 'common.login.auth': 'Authenticating...',
'common.login.auth.failed': 'Authentication failed', '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.password': 'Log in with Password',
'common.login.username.holder': 'Please enter username', 'common.login.username.holder': 'Please enter username',
'common.login.password.holder': 'Please enter password', 'common.login.password.holder': 'Please enter password',
-1
View File
@@ -2,7 +2,6 @@ export default {
'gpuservice.template': 'GPU Instance Template', 'gpuservice.template': 'GPU Instance Template',
'gpuservice.template.add': 'Add Instance Template', 'gpuservice.template.add': 'Add Instance Template',
'gpuservice.template.edit': 'Edit Instance Template', 'gpuservice.template.edit': 'Edit Instance Template',
'gpuservice.template.clone': 'Clone Instance Template',
'gpuservice.template.filter.name': 'Filter by name', 'gpuservice.template.filter.name': 'Filter by name',
'gpuservice.template.filter.vendor': 'Filter by vendor', 'gpuservice.template.filter.vendor': 'Filter by vendor',
'gpuservice.template.image': 'Image', 'gpuservice.template.image': 'Image',
+1 -1
View File
@@ -13,7 +13,7 @@ export default {
'menu.models.modelCatalog': 'Catalog', 'menu.models.modelCatalog': 'Catalog',
'menu.models.catalog': 'Model Catalog', 'menu.models.catalog': 'Model Catalog',
'menu.models.deployment': 'Deployments', 'menu.models.deployment': 'Deployments',
'menu.models.userModels': 'Models', 'menu.models.userModels': 'My Models',
'menu.models.benchmark': 'Benchmarks', 'menu.models.benchmark': 'Benchmarks',
'menu.models.benchmarkDetail': 'Benchmark Details', 'menu.models.benchmarkDetail': 'Benchmark Details',
'menu.models.providers': 'Providers', 'menu.models.providers': 'Providers',
-5
View File
@@ -12,11 +12,6 @@ export default {
'users.form.active.description': 'Enable or disable this user account', 'users.form.active.description': 'Enable or disable this user account',
'users.form.fullname': 'Full Name', 'users.form.fullname': 'Full Name',
'users.form.source': 'Source', '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.table.user': 'users',
'users.form.admin': 'Admin', 'users.form.admin': 'Admin',
'users.form.user': 'User', 'users.form.user': 'User',
-2
View File
@@ -44,8 +44,6 @@ export default {
'On the Worker that needs to be added, run the following command to join it to the cluster.', 'On the Worker that needs to be added, run the following command to join it to the cluster.',
'clusters.create.addCommand.k8s.tips': 'clusters.create.addCommand.k8s.tips':
'登録する Kubernetes クラスターで以下のコマンドを実行し、Kubernetes リソースを作成してクラスターを登録します。', '登録する Kubernetes クラスターで以下のコマンドを実行し、Kubernetes リソースを作成してクラスターを登録します。',
'clusters.create.addCommand.k8s.version.warning':
'サポートされる Kubernetes の最小バージョンは 1.23 です。GPU Service 機能を使用する場合、サポートされる Kubernetes の最小バージョンは 1.27 です。',
'cluster.create.checkEnv.tips': 'cluster.create.checkEnv.tips':
'Use the following command to check if the environment is ready.', 'Use the following command to check if the environment is ready.',
'clusters.create.register.tips': 'clusters.create.register.tips':
-4
View File
@@ -266,10 +266,6 @@ export default {
'common.select.count': '{count} selected', 'common.select.count': '{count} selected',
'common.login.auth': 'Authenticating...', 'common.login.auth': 'Authenticating...',
'common.login.auth.failed': 'Authentication failed', '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.password': 'Log in with Password',
'common.login.username.holder': 'Please enter username', 'common.login.username.holder': 'Please enter username',
'common.login.password.holder': 'Please enter password', 'common.login.password.holder': 'Please enter password',
-1
View File
@@ -2,7 +2,6 @@ export default {
'gpuservice.template': 'GPU インスタンステンプレート', 'gpuservice.template': 'GPU インスタンステンプレート',
'gpuservice.template.add': 'インスタンステンプレートを追加', 'gpuservice.template.add': 'インスタンステンプレートを追加',
'gpuservice.template.edit': 'インスタンステンプレートを編集', 'gpuservice.template.edit': 'インスタンステンプレートを編集',
'gpuservice.template.clone': 'インスタンステンプレートを複製',
'gpuservice.template.filter.name': '名前でフィルター', 'gpuservice.template.filter.name': '名前でフィルター',
'gpuservice.template.filter.vendor': 'ベンダーでフィルター', 'gpuservice.template.filter.vendor': 'ベンダーでフィルター',
'gpuservice.template.image': 'コンテナイメージ', 'gpuservice.template.image': 'コンテナイメージ',
+6 -6
View File
@@ -41,7 +41,7 @@ export default {
'menu.accessControl.organizations': 'Organizations', 'menu.accessControl.organizations': 'Organizations',
'menu.resources.clusters': 'Clusters', 'menu.resources.clusters': 'Clusters',
'menu.resources.credentials': 'Cloud Credentials', 'menu.resources.credentials': 'Cloud Credentials',
'menu.models.userModels': 'Models', 'menu.models.userModels': 'My Models',
'menu.resources.clusterDetail': 'Cluster Detail', 'menu.resources.clusterDetail': 'Cluster Detail',
'menu.resources.clusterCreate': 'Create Cluster', 'menu.resources.clusterCreate': 'Create Cluster',
'menu.models.backendsList': 'Inference Backends', 'menu.models.backendsList': 'Inference Backends',
@@ -62,11 +62,11 @@ export default {
// 6. 'menu.accessControl.apikeys': 'API Keys', // 6. 'menu.accessControl.apikeys': 'API Keys',
// 7. 'menu.accessControl.users': 'Users', // 7. 'menu.accessControl.users': 'Users',
// 8. 'menu.clusterManagement': 'Cluster Management', // 8. 'menu.clusterManagement': 'Cluster Management',
// 9. 'menu.resources.clusters': 'Clusters', // 9. 'menu.clusterManagement.clusters': 'Clusters',
// 10. 'menu.resources.credentials': 'Cloud Credentials', // 10. 'menu.clusterManagement.credentials': 'Cloud Credentials',
// 11. 'menu.models.userModels': 'Models' // 11. 'menu.models.userModels': 'My Models'
// 12. 'menu.resources.clusterDetail': 'Cluster Detail', // 12. 'menu.clusterManagement.clusterDetail': 'Cluster Detail',
// 13. 'menu.resources.clusterCreate': 'Create Cluster', // 13. 'menu.clusterManagement.clusterCreate': 'Create Cluster',
// 14. 'menu.models.backendsList': 'Inference Backends', // 14. 'menu.models.backendsList': 'Inference Backends',
// 15. 'menu.models.benchmark': 'Benchmarks', // 15. 'menu.models.benchmark': 'Benchmarks',
// 15. 'menu.models.provider': 'Provider', // 15. 'menu.models.provider': 'Provider',
-5
View File
@@ -13,11 +13,6 @@ export default {
'このユーザーアカウントを有効または無効にする', 'このユーザーアカウントを有効または無効にする',
'users.form.fullname': 'フルネーム', 'users.form.fullname': 'フルネーム',
'users.form.source': 'ソース', 'users.form.source': 'ソース',
'users.form.source.local': 'ローカル',
'users.form.source.tip.switchToLocal':
'ローカルに切り替えるには新しいパスワードが必要です。以後、ユーザーは標準のログインフォームからサインインします。',
'users.form.source.tip.switchToExternal':
'外部ソースに切り替えるとユーザーのローカルパスワードが削除され、設定済みの ID プロバイダーからサインインするようになります。',
'users.table.user': 'ユーザー', 'users.table.user': 'ユーザー',
'users.form.admin': '管理者', 'users.form.admin': '管理者',
'users.form.user': '一般ユーザー', 'users.form.user': '一般ユーザー',
-2
View File
@@ -44,8 +44,6 @@ export default {
'На воркере, который необходимо добавить, выполните следующую команду, чтобы присоединить его к кластеру.', 'На воркере, который необходимо добавить, выполните следующую команду, чтобы присоединить его к кластеру.',
'clusters.create.addCommand.k8s.tips': 'clusters.create.addCommand.k8s.tips':
'На Kubernetes-кластере, который необходимо зарегистрировать, выполните следующую команду, чтобы создать ресурсы Kubernetes и зарегистрировать этот кластер.', 'На Kubernetes-кластере, который необходимо зарегистрировать, выполните следующую команду, чтобы создать ресурсы Kubernetes и зарегистрировать этот кластер.',
'clusters.create.addCommand.k8s.version.warning':
'Минимальная поддерживаемая версия Kubernetes — 1.23. Для использования функции GPU Service минимальная поддерживаемая версия Kubernetes — 1.27.',
'cluster.create.checkEnv.tips': 'cluster.create.checkEnv.tips':
'Используйте следующую команду для проверки готовности окружения', 'Используйте следующую команду для проверки готовности окружения',
'clusters.create.register.tips': 'clusters.create.register.tips':
-4
View File
@@ -265,10 +265,6 @@ export default {
'common.select.count': '{count} Выбрано', 'common.select.count': '{count} Выбрано',
'common.login.auth': 'Аутентификация...', 'common.login.auth': 'Аутентификация...',
'common.login.auth.failed': 'Ошибка аутентификации', 'common.login.auth.failed': 'Ошибка аутентификации',
'common.login.error.source_conflict':
'Учётная запись с таким именем уже существует, но с другим источником аутентификации. Обратитесь к администратору для связывания или преобразования.',
'common.login.error.auth_failed':
'Не удалось пройти аутентификацию через провайдера идентификации. Попробуйте ещё раз или обратитесь к администратору.',
'common.login.password': 'Войти с паролем', 'common.login.password': 'Войти с паролем',
'common.login.username.holder': 'Введите имя пользователя', 'common.login.username.holder': 'Введите имя пользователя',
'common.login.password.holder': 'Введите пароль', 'common.login.password.holder': 'Введите пароль',
-1
View File
@@ -2,7 +2,6 @@ export default {
'gpuservice.template': 'Шаблон экземпляра GPU', 'gpuservice.template': 'Шаблон экземпляра GPU',
'gpuservice.template.add': 'Добавить шаблон экземпляра', 'gpuservice.template.add': 'Добавить шаблон экземпляра',
'gpuservice.template.edit': 'Редактировать шаблон экземпляра', 'gpuservice.template.edit': 'Редактировать шаблон экземпляра',
'gpuservice.template.clone': 'Клонировать шаблон экземпляра',
'gpuservice.template.filter.name': 'Фильтр по имени', 'gpuservice.template.filter.name': 'Фильтр по имени',
'gpuservice.template.filter.vendor': 'Фильтр по производителю', 'gpuservice.template.filter.vendor': 'Фильтр по производителю',
'gpuservice.template.image': 'Образ', 'gpuservice.template.image': 'Образ',
+1 -1
View File
@@ -40,7 +40,7 @@ export default {
'menu.accessControl.organizations': 'Организации', 'menu.accessControl.organizations': 'Организации',
'menu.resources.clusters': 'Кластеры', 'menu.resources.clusters': 'Кластеры',
'menu.resources.credentials': 'Облачные аккаунты', 'menu.resources.credentials': 'Облачные аккаунты',
'menu.models.userModels': 'Модели', 'menu.models.userModels': 'Мои модели',
'menu.resources.clusterDetail': 'Детали кластера', 'menu.resources.clusterDetail': 'Детали кластера',
'menu.resources.clusterCreate': 'Создать кластер', 'menu.resources.clusterCreate': 'Создать кластер',
'menu.models.backendsList': 'Бэкенды запуска', 'menu.models.backendsList': 'Бэкенды запуска',
-5
View File
@@ -13,11 +13,6 @@ export default {
'Включить или отключить эту учетную запись пользователя', 'Включить или отключить эту учетную запись пользователя',
'users.form.fullname': 'Полное имя', 'users.form.fullname': 'Полное имя',
'users.form.source': 'Источник', 'users.form.source': 'Источник',
'users.form.source.local': 'Локальный',
'users.form.source.tip.switchToLocal':
'Переключение на «Локальный» требует ввода нового пароля. После этого пользователь будет входить через стандартную форму входа.',
'users.form.source.tip.switchToExternal':
'Переключение на внешний источник удаляет локальный пароль пользователя. После этого вход будет выполняться через настроенного провайдера идентификации.',
'users.table.user': 'пользователи', 'users.table.user': 'пользователи',
'users.form.admin': 'Администратор', 'users.form.admin': 'Администратор',
'users.form.user': 'Пользователь', 'users.form.user': 'Пользователь',
-2
View File
@@ -44,8 +44,6 @@ export default {
'Eklenmesi gereken İşçi Düğümde, kümeye katılması için aşağıdaki komutu çalıştırın.', 'Eklenmesi gereken İşçi Düğümde, kümeye katılması için aşağıdaki komutu çalıştırın.',
'clusters.create.addCommand.k8s.tips': '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.', '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': '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.', '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': 'cluster.create.checkEnv.tips':
-4
View File
@@ -269,10 +269,6 @@ export default {
'common.select.count': '{count} seçildi', 'common.select.count': '{count} seçildi',
'common.login.auth': 'Kimlik doğrulanıyor...', 'common.login.auth': 'Kimlik doğrulanıyor...',
'common.login.auth.failed': 'Kimlik doğrulama başarısız', '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.password': 'Şifre ile giriş yap',
'common.login.username.holder': 'Lütfen kullanıcı adını girin', 'common.login.username.holder': 'Lütfen kullanıcı adını girin',
'common.login.password.holder': 'Lütfen şifreyi girin', 'common.login.password.holder': 'Lütfen şifreyi girin',
-1
View File
@@ -2,7 +2,6 @@ export default {
'gpuservice.template': 'GPU Örnek Şablonu', 'gpuservice.template': 'GPU Örnek Şablonu',
'gpuservice.template.add': 'Örnek Şablonu Ekle', 'gpuservice.template.add': 'Örnek Şablonu Ekle',
'gpuservice.template.edit': 'Örnek Şablonunu Düzenle', 'gpuservice.template.edit': 'Örnek Şablonunu Düzenle',
'gpuservice.template.clone': 'Örnek Şablonunu Klonla',
'gpuservice.template.filter.name': 'Ada göre filtrele', 'gpuservice.template.filter.name': 'Ada göre filtrele',
'gpuservice.template.filter.vendor': 'Tedarikçiye göre filtrele', 'gpuservice.template.filter.vendor': 'Tedarikçiye göre filtrele',
'gpuservice.template.image': 'İmaj', 'gpuservice.template.image': 'İmaj',
+2 -2
View File
@@ -13,12 +13,12 @@ export default {
'menu.models.modelCatalog': 'Katalog', 'menu.models.modelCatalog': 'Katalog',
'menu.models.catalog': 'Model Kataloğu', 'menu.models.catalog': 'Model Kataloğu',
'menu.models.deployment': 'Dağıtımlar', 'menu.models.deployment': 'Dağıtımlar',
'menu.models.userModels': 'Modeller', 'menu.models.userModels': 'Modellerim',
'menu.models.benchmark': 'Kıyaslamalar', 'menu.models.benchmark': 'Kıyaslamalar',
'menu.models.benchmarkDetail': 'Kıyaslama Detayları', 'menu.models.benchmarkDetail': 'Kıyaslama Detayları',
'menu.models.providers': 'Sağlayıcılar', 'menu.models.providers': 'Sağlayıcılar',
'menu.models.routes': 'Yönlendirmeler', 'menu.models.routes': 'Yönlendirmeler',
'menu.models.usage': 'Kullanım', 'menu.models.usage': 'Usage',
'menu.modelCatalog': 'Katalog', 'menu.modelCatalog': 'Katalog',
'menu.resources': 'Kaynaklar', 'menu.resources': 'Kaynaklar',
'menu.apikeys': 'API Anahtarları', 'menu.apikeys': 'API Anahtarları',
-5
View File
@@ -13,11 +13,6 @@ export default {
'Bu kullanıcı hesabını etkinleştir veya devre dışı bırak', 'Bu kullanıcı hesabını etkinleştir veya devre dışı bırak',
'users.form.fullname': 'Tam Ad', 'users.form.fullname': 'Tam Ad',
'users.form.source': 'Kaynak', '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.table.user': 'kullanıcılar',
'users.form.admin': 'Yönetici', 'users.form.admin': 'Yönetici',
'users.form.user': 'Kullanıcı', 'users.form.user': 'Kullanıcı',
-2
View File
@@ -43,8 +43,6 @@ export default {
'在需要添加的节点上运行以下命令,将其加入到集群中。', '在需要添加的节点上运行以下命令,将其加入到集群中。',
'clusters.create.addCommand.k8s.tips': 'clusters.create.addCommand.k8s.tips':
'在需要注册的 Kubernetes 集群中运行以下命令,创建 Kubernetes 资源,注册该集群。', '在需要注册的 Kubernetes 集群中运行以下命令,创建 Kubernetes 资源,注册该集群。',
'clusters.create.addCommand.k8s.version.warning':
'Kubernetes 版本最低支持 1.23,如果要使用 GPU Service 功能,Kubernetes 版本最低支持 1.27。',
'clusters.create.register.tips': 'clusters.create.register.tips':
'在需要添加的 Kubernetes 集群上运行以下命令,将其中的节点加入到集群中。', '在需要添加的 Kubernetes 集群上运行以下命令,将其中的节点加入到集群中。',
'cluster.create.checkEnv.tips': '使用以下命令检查环境是否准备妥当。', 'cluster.create.checkEnv.tips': '使用以下命令检查环境是否准备妥当。',
-3
View File
@@ -257,9 +257,6 @@ export default {
'common.select.count': '已选 {count} 项', 'common.select.count': '已选 {count} 项',
'common.login.auth': '认证中...', 'common.login.auth': '认证中...',
'common.login.auth.failed': '认证失败', 'common.login.auth.failed': '认证失败',
'common.login.error.source_conflict':
'已存在同名账号但来源不同。请联系管理员关联或转换该账号。',
'common.login.error.auth_failed': '身份提供商认证失败。请重试或联系管理员。',
'common.login.password': '使用密码登录', 'common.login.password': '使用密码登录',
'common.login.username.holder': '请输入用户名', 'common.login.username.holder': '请输入用户名',
'common.login.password.holder': '请输入密码', 'common.login.password.holder': '请输入密码',
-1
View File
@@ -2,7 +2,6 @@ export default {
'gpuservice.template': 'GPU 实例模板', 'gpuservice.template': 'GPU 实例模板',
'gpuservice.template.add': '添加实例模板', 'gpuservice.template.add': '添加实例模板',
'gpuservice.template.edit': '编辑实例模板', 'gpuservice.template.edit': '编辑实例模板',
'gpuservice.template.clone': '克隆实例模板',
'gpuservice.template.filter.name': '按名称过滤', 'gpuservice.template.filter.name': '按名称过滤',
'gpuservice.template.filter.vendor': '按厂商过滤', 'gpuservice.template.filter.vendor': '按厂商过滤',
'gpuservice.template.image': '镜像', 'gpuservice.template.image': '镜像',
+1 -1
View File
@@ -12,7 +12,7 @@ export default {
'menu.models.modelList': '部署与管理', 'menu.models.modelList': '部署与管理',
'menu.models.modelCatalog': '模型库', 'menu.models.modelCatalog': '模型库',
'menu.models.deployment': '部署', 'menu.models.deployment': '部署',
'menu.models.userModels': '模型广场', 'menu.models.userModels': '我的模型',
'menu.models.benchmark': '基准测试', 'menu.models.benchmark': '基准测试',
'menu.models.benchmarkDetail': '基准测试详情', 'menu.models.benchmarkDetail': '基准测试详情',
'menu.models.providers': '提供商', 'menu.models.providers': '提供商',
-5
View File
@@ -12,11 +12,6 @@ export default {
'users.form.active.description': '启用或禁用此用户账户', 'users.form.active.description': '启用或禁用此用户账户',
'users.form.fullname': '全名', 'users.form.fullname': '全名',
'users.form.source': '来源', 'users.form.source': '来源',
'users.form.source.local': '本地',
'users.form.source.tip.switchToLocal':
'切换到本地需要设置新密码,之后用户将通过标准登录表单登录。',
'users.form.source.tip.switchToExternal':
'切换到外部来源会清除该用户的本地密码,之后用户将通过所配置的身份提供商登录。',
'users.table.user': '用户', 'users.table.user': '用户',
'users.form.admin': '管理员', 'users.form.admin': '管理员',
'users.form.user': '普通用户', 'users.form.user': '普通用户',
+1 -1
View File
@@ -1,5 +1,5 @@
import useCoolColors from '@/hooks/use-cool-colors'; import useCoolColors from '@/hooks/use-cool-colors';
import { Chart } from '@gpustack/core-ui/charts'; import { Chart } from '@gpustack/core-ui';
import { formatLargeNumber } from '@gpustack/core-ui/utils'; import { formatLargeNumber } from '@gpustack/core-ui/utils';
import { Empty, Spin, theme } from 'antd'; import { Empty, Spin, theme } from 'antd';
import _ from 'lodash'; import _ from 'lodash';
+13 -80
View File
@@ -4,53 +4,28 @@ import {
RouteContext, RouteContext,
type PageContainerProps type PageContainerProps
} from '@ant-design/pro-components'; } from '@ant-design/pro-components';
import { import { useOverlayScroller } from '@gpustack/core-ui';
HeaderSlotContext,
useOverlayScroller,
type HeaderSlotContextValue
} from '@gpustack/core-ui';
import { Divider } from 'antd'; import { Divider } from 'antd';
import classNames from 'classnames'; import classNames from 'classnames';
import { import { useContext, useEffect, useRef } from 'react';
useCallback,
useContext,
useEffect,
useMemo,
useRef,
useState
} from 'react';
import pageBoxCss from './styles/page-box.less'; 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; const paddingInlinePageContainerContent = 24;
export const PageContainerInner: React.FC< export const PageContainerInner: React.FC<
PageContainerProps & { PageContainerProps & {
leftContent?: React.ReactNode;
rightContent?: React.ReactNode;
styles?: { styles?: {
containerWrapper?: React.CSSProperties; containerWrapper?: React.CSSProperties;
}; };
} }
> = ({ children, styles, title, ...rest }) => { > = ({ children, styles, title, leftContent, rightContent, ...rest }) => {
const { initialize: initialize } = useOverlayScroller({ const { initialize: initialize } = useOverlayScroller({
defer: false defer: false
}); });
const pageContext = useContext(RouteContext); const pageContext = useContext(RouteContext);
const contentWrapperRef = useRef<HTMLDivElement>(null); 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(() => { useEffect(() => {
if (contentWrapperRef.current) { if (contentWrapperRef.current) {
@@ -59,45 +34,7 @@ export const PageContainerInner: React.FC<
} }
}, [initialize, contentWrapperRef]); }, [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 ( return (
<HeaderSlotContext.Provider value={slotValue}>
<div className={pageBoxCss.containerWrapper}> <div className={pageBoxCss.containerWrapper}>
<PageContainer <PageContainer
{...rest} {...rest}
@@ -113,31 +50,27 @@ export const PageContainerInner: React.FC<
> >
<div className={pageBoxCss.title}> <div className={pageBoxCss.title}>
<div className={pageBoxCss.left}> <div className={pageBoxCss.left}>
<div ref={setLeftEl} className={pageBoxCss.leftSlot} /> {leftContent || pageContext.title}
<span className={pageBoxCss.defaultTitle}>
{pageContext.title}
</span>
</div> </div>
<div className={pageBoxCss.right}> <div className={pageBoxCss.right}>
<div ref={setRightEl} className={pageBoxCss.rightSlot} /> {rightContent && (
<Divider <div>
className={pageBoxCss.divider} {rightContent}
orientation="vertical" <Divider orientation="vertical" style={{ margin: '0 16px' }} />
style={{ margin: '0 16px' }} </div>
/> )}
<ExtraContent /> <ExtraContent />
</div> </div>
</div> </div>
<div <div
className={classNames(pageBoxCss.contentWrapper)} className={classNames(pageBoxCss.contentWrapper)}
style={{ ...styles?.containerWrapper, ...contentStyleOverride }} style={styles?.containerWrapper}
ref={contentWrapperRef} ref={contentWrapperRef}
> >
{children} {children}
</div> </div>
</PageContainer> </PageContainer>
</div> </div>
</HeaderSlotContext.Provider>
); );
}; };

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