Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 69 additions & 1 deletion e2e/utils/test-util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,70 @@ function mergeWithUndefined<T extends object>(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<string>();
let currentSection = '';

// First pass: track last occurrence of each key per section
const lastOccurrence = new Map<string, number>();
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<string, number[]>();
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<number>();
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;
Expand Down Expand Up @@ -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);
Comment thread
ironAiken2 marked this conversation as resolved.
break; // Success, exit retry loop
} catch (error) {
lastError = error;
Expand Down
3 changes: 3 additions & 0 deletions react/src/components/ProjectSelect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,15 @@ export interface ProjectSelectProps extends BAISelectProps {
autoSelectDefault?: boolean;
disableDefaultFilter?: boolean;
lockedProjectTypes?: string[];
fetchKey?: string;
}

const ProjectSelect: React.FC<ProjectSelectProps> = ({
onSelectProject,
domain,
disableDefaultFilter,
lockedProjectTypes,
fetchKey,
...selectProps
}) => {
const { t } = useTranslation();
Expand Down Expand Up @@ -74,6 +76,7 @@ const ProjectSelect: React.FC<ProjectSelectProps> = ({
},
{
fetchPolicy: 'store-and-network',
fetchKey: fetchKey,
},
);

Expand Down
9 changes: 9 additions & 0 deletions react/src/components/UpdateUsersModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
useBAILogger,
useErrorMessageResolver,
useMutationWithPromise,
useUpdatableState,
} from 'backend.ai-ui';
import _ from 'lodash';
import { Suspense, useRef, useState } from 'react';
Expand Down Expand Up @@ -50,6 +51,7 @@ const UpdateUsersModal = ({
'use memo';
const formRef = useRef<FormInstance<UpdateUsersFormValues>>(null);
const [isPending, setIsPending] = useState(false);
const [fetchKey, updateFetchKey] = useUpdatableState('initial-fetch');
const { token } = theme.useToken();
const { t } = useTranslation();
const { message } = App.useApp();
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -232,6 +240,7 @@ const UpdateUsersModal = ({
domain={getFieldValue('domain_name')}
disableDefaultFilter
disabled={!getFieldValue('domain_name')}
fetchKey={fetchKey}
/>
</Form.Item>
)}
Expand Down
1 change: 1 addition & 0 deletions react/src/components/UserSettingModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -874,6 +874,7 @@ const UserSettingModal: React.FC<UserSettingModalProps> = ({
domain={getFieldValue('domain_name')}
disableDefaultFilter
lockedProjectTypes={!user ? ['MODEL_STORE'] : undefined}
fetchKey={fetchKey}
/>
</Form.Item>
)}
Expand Down
Loading