From 7e0dd906f29681af87cea73876edff7658c8ecb8 Mon Sep 17 00:00:00 2001 From: Jimmy Ben Klieve Date: Sat, 28 Feb 2026 19:21:51 +0800 Subject: [PATCH] refactor: update admin ui (#13280) ### What problem does this PR solve? Update for Admin UI: - Update file picker input in **Registration whitelist** > **Import from Excel** modal - Modify DOM structure of **Sandbox Settings** and move several hardcoded texts into translation files ### Type of change - [x] Refactoring --- web/src/components/ui/audio-button.tsx | 2 +- web/src/locales/en.ts | 55 ++- .../pages/admin/forms/import-excel-form.tsx | 51 ++- web/src/pages/admin/sandbox-settings.tsx | 407 +++++++++++------- web/src/services/admin.service.d.ts | 36 +- 5 files changed, 363 insertions(+), 188 deletions(-) diff --git a/web/src/components/ui/audio-button.tsx b/web/src/components/ui/audio-button.tsx index 098662feb6..c7e418422e 100644 --- a/web/src/components/ui/audio-button.tsx +++ b/web/src/components/ui/audio-button.tsx @@ -389,7 +389,7 @@ export const AudioButton = ({
)} diff --git a/web/src/locales/en.ts b/web/src/locales/en.ts index f3fb3db126..e6f793b87a 100644 --- a/web/src/locales/en.ts +++ b/web/src/locales/en.ts @@ -1010,22 +1010,18 @@ Example: Virtual Hosted Style`, 'Connect to your SeaFile server to synchronise files and documents from your libraries.', seafileUrlTip: 'The full URL of your SeaFile server including the protocol. Example: https://seafile.example.com - Do not include a trailing slash or any path after the domain.', - seafileAccountScopeTip: + seafileAccountScopeTip: 'Syncs all libraries visible to the Account API Token below.', - seafileTokenPanelHeading: - 'Provide one of these authentication methods:', - seafileTokenPanelAccountBullet: - '- grants access to all your libraries.', - seafileTokenPanelLibraryBullet: - '— scoped to a single library only (more secure).', - seafileValidationAccountTokenRequired: + seafileTokenPanelHeading: 'Provide one of these authentication methods:', + seafileTokenPanelAccountBullet: '- grants access to all your libraries.', + seafileTokenPanelLibraryBullet: + '— scoped to a single library only (more secure).', + seafileValidationAccountTokenRequired: 'Account API Token is required for Entire Account scope', - seafileValidationTokenRequired: + seafileValidationTokenRequired: 'Provide either an Account API Token or a Library Token', - seafileValidationLibraryIdRequired: - 'Library ID is required', - seafileValidationDirectoryPathRequired: - 'Directory Path is required', + seafileValidationLibraryIdRequired: 'Library ID is required', + seafileValidationDirectoryPathRequired: 'Directory Path is required', seafileSyncScopeTip: 'Controls what gets synchronised: ' + '(1) Entire Account - Syncs all libraries your token has access to. Requires an Account API Token. ' + @@ -2533,6 +2529,39 @@ Important structured information may include: names, dates, locations, events, k roles: 'Roles', monitoring: 'Monitoring', + sandboxSettingsPage: { + description: + 'Configure your code execution sandbox provider. The sandbox is used by the Code component in agents.', + providerSelection: 'Provider selection', + providerSelectionDescription: + 'Choose a sandbox provider for code execution', + + namedProviderConfiguration: '{{name}} configuration', + namedProviderConfigurationDescription: + 'Configure the connection settings for {{name}}.', + + saveConfiguration: 'Save configuration', + saving: 'Saving...', + + testConnectionResultModal: { + title: 'Connection test result', + testing: 'Testing connection to sandbox provider...', + success: 'Successfully connected to sandbox provider', + failed: 'Failed to connect to sandbox provider', + + exitCode: 'Exit code', + executionTime: 'Execution time', + stdout: 'Standard output', + stderr: 'Error output / stack trace', + }, + + testConnection: 'Test connection', + testing: 'Testing...', + }, + + selectFile: 'Select file', + noFileSelected: 'No file selected', + back: 'Back', active: 'Active', inactive: 'Inactive', diff --git a/web/src/pages/admin/forms/import-excel-form.tsx b/web/src/pages/admin/forms/import-excel-form.tsx index 1ea35bffbd..64f136f87e 100644 --- a/web/src/pages/admin/forms/import-excel-form.tsx +++ b/web/src/pages/admin/forms/import-excel-form.tsx @@ -8,7 +8,7 @@ import { } from '@/components/ui/form'; import { Input } from '@/components/ui/input'; import { zodResolver } from '@hookform/resolvers/zod'; -import { useCallback, useId, useMemo } from 'react'; +import { useCallback, useId, useMemo, useRef } from 'react'; import { useForm } from 'react-hook-form'; import { Trans, useTranslation } from 'react-i18next'; import { z } from 'zod'; @@ -30,6 +30,7 @@ export const ImportExcelForm = ({ onSubmit = () => {}, }: ImportExcelFormProps) => { const { t } = useTranslation(); + const filePickerRef = useRef(null); return (
@@ -50,17 +51,47 @@ export const ImportExcelForm = ({ - { - const files = e.target.files; - onChange(files?.[0]); + + + { + const files = e.target.files; + onChange(files?.[0]); + }} + {...field} + ref={(ref) => { + filePickerRef.current = ref; + field.ref(ref); + }} + /> + )} diff --git a/web/src/pages/admin/sandbox-settings.tsx b/web/src/pages/admin/sandbox-settings.tsx index ad43fd2b21..5c1083a4f2 100644 --- a/web/src/pages/admin/sandbox-settings.tsx +++ b/web/src/pages/admin/sandbox-settings.tsx @@ -5,7 +5,9 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { LucideCloud, + LucideLink, LucideLoader2, + LucideSave, LucideServer, LucideZap, } from 'lucide-react'; @@ -39,7 +41,10 @@ import { testSandboxConnection, } from '@/services/admin-service'; +import Spotlight from '@/components/spotlight'; import message from '@/components/ui/message'; +import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'; +import { ScrollArea } from '@/components/ui/scroll-area'; // Provider icons mapping const PROVIDER_ICONS: Record = { @@ -74,7 +79,11 @@ function AdminSandboxSettings() { }); // Fetch current config - const { data: currentConfig, isLoading: configLoading } = useQuery({ + const { + data: currentConfig, + isLoading: configLoading, + refetch: refetchConfig, + } = useQuery({ queryKey: ['admin/getSandboxConfig'], queryFn: async () => (await getSandboxConfig()).data.data, }); @@ -95,7 +104,8 @@ function AdminSandboxSettings() { }) => (await setSandboxConfig(params)).data, onSuccess: () => { message.success('Sandbox configuration updated successfully'); - queryClient.invalidateQueries({ queryKey: ['admin/getSandboxConfig'] }); + // queryClient.invalidateQueries({ queryKey: ['admin/getSandboxConfig'] }); + refetchConfig(); }, onError: (error: Error) => { message.error(`Failed to update configuration: ${error.message}`); @@ -186,15 +196,15 @@ function AdminSandboxSettings() { schema: AdminService.SandboxConfigField, ) => { const value = configValues[fieldName] ?? schema.default ?? ''; - const isSecret = schema.secret ?? false; switch (schema.type) { case 'string': - if (isSecret) { + if (schema.secret) { return ( @@ -226,6 +236,7 @@ function AdminSandboxSettings() { min={schema.min} max={schema.max} value={value as number} + className="h-10" onChange={(e) => handleConfigValueChange(fieldName, parseInt(e.target.value) || 0) } @@ -248,171 +259,233 @@ function AdminSandboxSettings() { } }; - if (providersLoading || configLoading) { - return ( -
- -
- ); - } - const selectedProviderData = providers.find((p) => p.id === selectedProvider); const ProviderIcon = selectedProvider ? PROVIDER_ICONS[selectedProvider] || LucideServer : LucideServer; return ( -
-
-

Sandbox Settings

-

- Configure your code execution sandbox provider. The sandbox is used by - the Code component in agents. -

-
+ <> + + - {/* Provider Selection */} - - - Provider Selection - - Choose a sandbox provider for code execution - - - -
- {providers.map((provider) => { - const Icon = PROVIDER_ICONS[provider.id] || LucideServer; - return ( -
handleProviderChange(provider.id)} - > -
- -
-

{provider.name}

-

- {provider.description} -

-
- {provider.tags.map((tag) => ( - - {tag} - - ))} -
-
-
-
- ); - })} -
-
-
- - {/* Provider Configuration */} - {selectedProvider && selectedProviderData && ( - - -
-
- - - {selectedProviderData.name} Configuration - - - Configure the connection settings for{' '} - {selectedProviderData.name} - -
-
- - -
-
+ + + + {t('admin.sandboxSettings')} + + + {t('admin.sandboxSettingsPage.description')} + + -
- {Object.entries(providerSchema).map(([fieldName, schema]) => ( -
- - {renderConfigField(fieldName, schema)} - {schema.description && ( -

- {schema.description} -

+ {providersLoading || configLoading ? ( +
+ ) : ( + <> + {/* Provider Selection */} + + + + {t('admin.sandboxSettingsPage.providerSelection')} + + + + {t( + 'admin.sandboxSettingsPage.providerSelectionDescription', + )} + + + + {providers.map((provider) => { + const Icon = + PROVIDER_ICONS[provider.id] || LucideServer; + + return ( + + ); + })} + + + + {/* Provider Configuration */} + {selectedProvider && selectedProviderData && ( + + +
+
+
+

+ + + {t( + 'admin.sandboxSettingsPage.namedProviderConfiguration', + { name: selectedProviderData.name }, + )} +

+ +

+ {t( + 'admin.sandboxSettingsPage.namedProviderConfigurationDescription', + { name: selectedProviderData.name }, + )} +

+
+ +
+ + + +
+
+ +
+ {Object.entries(providerSchema).map( + ([fieldName, schema]) => ( +
+ + +
+ {renderConfigField(fieldName, schema)} +
+ + {schema.type === 'integer' && + (schema.min !== undefined || + schema.max !== undefined) && ( +

+ {schema.min !== undefined && + `Minimum: ${schema.min}`} + {schema.min !== undefined && + schema.max !== undefined && + ' • '} + {schema.max !== undefined && + `Maximum: ${schema.max}`} +

+ )} +
+ ), + )} +
+
+ +
)} - {schema.type === 'integer' && - (schema.min !== undefined || schema.max !== undefined) && ( -

- {schema.min !== undefined && `Minimum: ${schema.min}`} - {schema.min !== undefined && - schema.max !== undefined && - ' • '} - {schema.max !== undefined && `Maximum: ${schema.max}`} -

- )} -
- ))} -
+
+ + )} -
- )} + + {/* Test Result Modal */} - Connection Test Result - - {testResult === null - ? 'Testing connection to sandbox provider...' - : testResult.success - ? 'Successfully connected to sandbox provider' - : 'Failed to connect to sandbox provider'} - + + {t('admin.sandboxSettingsPage.testConnectionResultModal.title')} + + + + {testResult === null + ? t('admin.sandboxSettingsPage.testConnectionResultModal.testing') + : testResult.success + ? t( + 'admin.sandboxSettingsPage.testConnectionResultModal.success', + ) + : t( + 'admin.sandboxSettingsPage.testConnectionResultModal.failed', + )} + + {testResult === null ? (
@@ -438,11 +511,21 @@ function AdminSandboxSettings() { {/* Exit code and execution time */}
- Exit Code:{' '} + + {t( + 'admin.sandboxSettingsPage.testConnectionResultModal.exitCode', + )} + : + {' '} {testResult.details.exit_code}
- Execution Time:{' '} + + {t( + 'admin.sandboxSettingsPage.testConnectionResultModal.executionTime', + )} + : + {' '} {testResult.details.execution_time?.toFixed(2)}s
@@ -451,7 +534,10 @@ function AdminSandboxSettings() { {testResult.details.stdout && (

- Standard Output: + {t( + 'admin.sandboxSettingsPage.testConnectionResultModal.stdout', + )} + :

                         {testResult.details.stdout}
@@ -463,7 +549,10 @@ function AdminSandboxSettings() {
                   {testResult.details.stderr && (
                     

- Error Output / Stack Trace: + {t( + 'admin.sandboxSettingsPage.testConnectionResultModal.stderr', + )} + :

                         {testResult.details.stderr}
@@ -475,11 +564,13 @@ function AdminSandboxSettings() {
             
)} - +
-
+ ); } diff --git a/web/src/services/admin.service.d.ts b/web/src/services/admin.service.d.ts index 8b9b9d598f..ac206ee8d4 100644 --- a/web/src/services/admin.service.d.ts +++ b/web/src/services/admin.service.d.ts @@ -1,4 +1,4 @@ -declare module AdminService { +declare namespace AdminService { export type LoginData = { access_token: string; avatar: unknown; @@ -175,18 +175,42 @@ declare module AdminService { tags: string[]; }; - export type SandboxConfigField = { - type: 'string' | 'integer' | 'boolean' | 'json'; + export type SandboxConfigFieldBase = { required?: boolean; label?: string; placeholder?: string; - default?: string | number | boolean; - min?: number; - max?: number; description?: string; + }; + + export type SandboxConfigStringField = SandboxConfigFieldBase & { + type: 'string'; + default?: string; secret?: boolean; }; + export type SandboxConfigIntegerField = SandboxConfigFieldBase & { + type: 'integer'; + default?: number; + min?: number; + max?: number; + }; + + export type SandboxConfigBooleanField = SandboxConfigFieldBase & { + type: 'boolean'; + default?: boolean; + }; + + export type SandboxConfigJsonField = SandboxConfigFieldBase & { + type: 'json'; + default?: unknown; + }; + + export type SandboxConfigField = + | SandboxConfigStringField + | SandboxConfigIntegerField + | SandboxConfigBooleanField + | SandboxConfigJsonField; + export type SandboxConfig = { provider_type: string; config: Record;