diff --git a/e2e/utils/test-util.ts b/e2e/utils/test-util.ts index 6a1e5b38b0..a7cb52459c 100644 --- a/e2e/utils/test-util.ts +++ b/e2e/utils/test-util.ts @@ -50,6 +50,70 @@ function mergeWithUndefined(target: T, source: object): T { return result; } +/** + * Remove duplicate keys within the same TOML section. + * Keeps the last occurrence of each key per section. + */ +function deduplicateTomlKeys(toml: string): string { + const lines = toml.split('\n'); + const result: string[] = []; + const seenKeys = new Set(); + let currentSection = ''; + + // First pass: track last occurrence of each key per section + const lastOccurrence = new Map(); + for (let i = 0; i < lines.length; i++) { + const line = lines[i].trim(); + if (line.startsWith('[')) { + currentSection = line; + seenKeys.clear(); + } else if (line && !line.startsWith('#')) { + const eqIndex = line.indexOf('='); + if (eqIndex > 0) { + const key = `${currentSection}::${line.substring(0, eqIndex).trim()}`; + lastOccurrence.set(key, i); + } + } + } + + // Second pass: only keep the last occurrence of duplicate keys + currentSection = ''; + const keyLineIndices = new Map(); + for (let i = 0; i < lines.length; i++) { + const line = lines[i].trim(); + if (line.startsWith('[')) { + currentSection = line; + } else if (line && !line.startsWith('#')) { + const eqIndex = line.indexOf('='); + if (eqIndex > 0) { + const key = `${currentSection}::${line.substring(0, eqIndex).trim()}`; + if (!keyLineIndices.has(key)) { + keyLineIndices.set(key, []); + } + keyLineIndices.get(key)!.push(i); + } + } + } + + const skipLines = new Set(); + for (const [, indices] of keyLineIndices) { + if (indices.length > 1) { + // Skip all but the last occurrence + for (let j = 0; j < indices.length - 1; j++) { + skipLines.add(indices[j]); + } + } + } + + for (let i = 0; i < lines.length; i++) { + if (!skipLines.has(i)) { + result.push(lines[i]); + } + } + + return result.join('\n'); +} + // Theme configuration types based on theme.schema.json type ThemeLogoConfig = { src?: string; @@ -751,7 +815,11 @@ export async function modifyConfigToml( if (!res.ok) throw new Error(`HTTP ${res.status}`); return res.text(); }); - config = TOML.parse(configToml); + // Pre-process TOML to remove duplicate keys before parsing. + // Some server configurations may have duplicate keys (e.g., debug = true + // appearing twice under [general]) which strict TOML parsers reject. + const deduplicatedToml = deduplicateTomlKeys(configToml); + config = TOML.parse(deduplicatedToml); break; // Success, exit retry loop } catch (error) { lastError = error; diff --git a/react/src/components/ProjectSelect.tsx b/react/src/components/ProjectSelect.tsx index 44e62bab0b..87925d5bf7 100644 --- a/react/src/components/ProjectSelect.tsx +++ b/react/src/components/ProjectSelect.tsx @@ -25,6 +25,7 @@ export interface ProjectSelectProps extends BAISelectProps { autoSelectDefault?: boolean; disableDefaultFilter?: boolean; lockedProjectTypes?: string[]; + fetchKey?: string; } const ProjectSelect: React.FC = ({ @@ -32,6 +33,7 @@ const ProjectSelect: React.FC = ({ domain, disableDefaultFilter, lockedProjectTypes, + fetchKey, ...selectProps }) => { const { t } = useTranslation(); @@ -74,6 +76,7 @@ const ProjectSelect: React.FC = ({ }, { fetchPolicy: 'store-and-network', + fetchKey: fetchKey, }, ); diff --git a/react/src/components/UpdateUsersModal.tsx b/react/src/components/UpdateUsersModal.tsx index ee724c8072..60d06549aa 100644 --- a/react/src/components/UpdateUsersModal.tsx +++ b/react/src/components/UpdateUsersModal.tsx @@ -16,6 +16,7 @@ import { useBAILogger, useErrorMessageResolver, useMutationWithPromise, + useUpdatableState, } from 'backend.ai-ui'; import _ from 'lodash'; import { Suspense, useRef, useState } from 'react'; @@ -50,6 +51,7 @@ const UpdateUsersModal = ({ 'use memo'; const formRef = useRef>(null); const [isPending, setIsPending] = useState(false); + const [fetchKey, updateFetchKey] = useUpdatableState('initial-fetch'); const { token } = theme.useToken(); const { t } = useTranslation(); const { message } = App.useApp(); @@ -88,6 +90,12 @@ const UpdateUsersModal = ({ okText={t('button.Update')} confirmLoading={isPending} {...modalProps} + afterOpenChange={(open) => { + if (open) { + updateFetchKey(); + } + modalProps.afterOpenChange?.(open); + }} onOk={(e) => { formRef.current ?.validateFields() @@ -232,6 +240,7 @@ const UpdateUsersModal = ({ domain={getFieldValue('domain_name')} disableDefaultFilter disabled={!getFieldValue('domain_name')} + fetchKey={fetchKey} /> )} diff --git a/react/src/components/UserSettingModal.tsx b/react/src/components/UserSettingModal.tsx index 54f1113969..afeee1790d 100644 --- a/react/src/components/UserSettingModal.tsx +++ b/react/src/components/UserSettingModal.tsx @@ -874,6 +874,7 @@ const UserSettingModal: React.FC = ({ domain={getFieldValue('domain_name')} disableDefaultFilter lockedProjectTypes={!user ? ['MODEL_STORE'] : undefined} + fetchKey={fetchKey} /> )}