forked from boos/BoosAPI
fix: default downstream key access selections to all (#263)
* fix: default downstream key access selections to all * test: fix downstream keys test renderer typing
This commit is contained in:
@@ -569,6 +569,73 @@ describe('DownstreamKeys page', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('defaults new keys to all exact models and all group routes before saving', async () => {
|
||||||
|
apiMock.getRoutesLite.mockResolvedValue([
|
||||||
|
{ id: 11, modelPattern: 'claude-*', displayName: '默认群组', enabled: true },
|
||||||
|
{ id: 12, modelPattern: 'gpt-4.1-mini', displayName: 'GPT 4.1 Mini', enabled: true },
|
||||||
|
{ id: 13, modelPattern: 're:^gemini-2\\..*$', displayName: 'Gemini 全家桶', enabled: true },
|
||||||
|
{ id: 14, modelPattern: 'claude-opus-4-6', displayName: 'Claude Opus 4.6', enabled: true },
|
||||||
|
]);
|
||||||
|
|
||||||
|
let root!: WebTestRenderer;
|
||||||
|
try {
|
||||||
|
await act(async () => {
|
||||||
|
root = create(
|
||||||
|
<MemoryRouter initialEntries={['/downstream-keys']}>
|
||||||
|
<ToastProvider>
|
||||||
|
<DownstreamKeys />
|
||||||
|
</ToastProvider>
|
||||||
|
</MemoryRouter>,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
await flushMicrotasks();
|
||||||
|
|
||||||
|
const createBtn = root!.root.findAll((node) => node.type === 'button' && collectText(node).includes('新增下游密钥'))[0];
|
||||||
|
await act(async () => {
|
||||||
|
createBtn.props.onClick();
|
||||||
|
});
|
||||||
|
await flushMicrotasks();
|
||||||
|
|
||||||
|
const advancedBtn = root!.root.findAll((node) => node.type === 'button' && collectText(node).includes('高级配置'))[0];
|
||||||
|
await act(async () => {
|
||||||
|
advancedBtn.props.onClick();
|
||||||
|
});
|
||||||
|
await flushMicrotasks();
|
||||||
|
|
||||||
|
const panels = root!.root.findAll((node) => node.props.className === 'downstream-key-advanced-panel');
|
||||||
|
const modelPanel = panels.find((node) => collectText(node).includes('模型白名单'));
|
||||||
|
const groupPanel = panels.find((node) => collectText(node).includes('群组范围'));
|
||||||
|
expect(modelPanel).toBeTruthy();
|
||||||
|
expect(groupPanel).toBeTruthy();
|
||||||
|
expect(collectText(modelPanel!)).toContain('已选 2 个模型');
|
||||||
|
expect(collectText(groupPanel!)).toContain('已选 2 个群组');
|
||||||
|
|
||||||
|
const inputs = root!.root.findAllByType('input');
|
||||||
|
const nameInput = inputs.find((node) => node.props.placeholder === '例如:项目 A / 移动端');
|
||||||
|
const keyInput = inputs.find((node) => node.props.placeholder === 'sk-...');
|
||||||
|
await act(async () => {
|
||||||
|
nameInput!.props.onChange({ target: { value: 'default-all-key' } });
|
||||||
|
keyInput!.props.onChange({ target: { value: 'sk-default-all-key-0323' } });
|
||||||
|
});
|
||||||
|
await flushMicrotasks();
|
||||||
|
|
||||||
|
const saveBtn = root!.root.findAll((node) => node.type === 'button' && collectText(node).includes('创建密钥'))[0];
|
||||||
|
await act(async () => {
|
||||||
|
saveBtn.props.onClick();
|
||||||
|
});
|
||||||
|
await flushMicrotasks();
|
||||||
|
|
||||||
|
expect(apiMock.createDownstreamApiKey).toHaveBeenCalledWith(expect.objectContaining({
|
||||||
|
name: 'default-all-key',
|
||||||
|
key: 'sk-default-all-key-0323',
|
||||||
|
supportedModels: ['claude-opus-4-6', 'gpt-4.1-mini'],
|
||||||
|
allowedRouteIds: [11, 13],
|
||||||
|
}));
|
||||||
|
} finally {
|
||||||
|
root?.unmount();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it('uses backend batch api for selected rows', async () => {
|
it('uses backend batch api for selected rows', async () => {
|
||||||
let root!: WebTestRenderer;
|
let root!: WebTestRenderer;
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -85,6 +85,7 @@ type BatchMetadataForm = {
|
|||||||
tags: string[];
|
tags: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type DefaultRouteSelections = Pick<EditorForm, 'selectedModels' | 'selectedGroupRouteIds'>;
|
||||||
function toDateTimeLocal(isoString: string | null | undefined): string {
|
function toDateTimeLocal(isoString: string | null | undefined): string {
|
||||||
if (!isoString) return '';
|
if (!isoString) return '';
|
||||||
const ts = Date.parse(isoString);
|
const ts = Date.parse(isoString);
|
||||||
@@ -138,6 +139,21 @@ function uniqIds(values: number[]): number[] {
|
|||||||
return [...new Set(values.map((value) => Number(value)).filter((value) => Number.isFinite(value) && value > 0).map((value) => Math.trunc(value)))];
|
return [...new Set(values.map((value) => Number(value)).filter((value) => Number.isFinite(value) && value > 0).map((value) => Math.trunc(value)))];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildDefaultRouteSelections(routeOptions: RouteSelectorItem[]): DefaultRouteSelections {
|
||||||
|
return {
|
||||||
|
selectedModels: uniqStrings(
|
||||||
|
routeOptions
|
||||||
|
.filter((item) => isExactModelPattern(item.modelPattern))
|
||||||
|
.map((item) => item.modelPattern),
|
||||||
|
).sort((a, b) => a.localeCompare(b)),
|
||||||
|
selectedGroupRouteIds: uniqIds(
|
||||||
|
routeOptions
|
||||||
|
.filter(isGroupRouteOption)
|
||||||
|
.map((item) => item.id),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function parseTagText(value: string): string[] {
|
function parseTagText(value: string): string[] {
|
||||||
return normalizeTags(value.split(/[\r\n,,]+/g));
|
return normalizeTags(value.split(/[\r\n,,]+/g));
|
||||||
}
|
}
|
||||||
@@ -279,7 +295,21 @@ function DownstreamKeyCopyIconButton({ fullKey }: { fullKey: string | undefined
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildEditorForm(item?: ManagedItem | DownstreamApiKeyItem | null): EditorForm {
|
function buildEditorForm(
|
||||||
|
item?: ManagedItem | DownstreamApiKeyItem | null,
|
||||||
|
routeOptions: RouteSelectorItem[] = [],
|
||||||
|
selectAllByDefault = false,
|
||||||
|
): EditorForm {
|
||||||
|
const defaultSelections = selectAllByDefault
|
||||||
|
? buildDefaultRouteSelections(routeOptions)
|
||||||
|
: { selectedModels: [], selectedGroupRouteIds: [] };
|
||||||
|
const selectedModels = Array.isArray(item?.supportedModels)
|
||||||
|
? item.supportedModels
|
||||||
|
: defaultSelections.selectedModels;
|
||||||
|
const selectedGroupRouteIds = Array.isArray(item?.allowedRouteIds)
|
||||||
|
? item.allowedRouteIds
|
||||||
|
: defaultSelections.selectedGroupRouteIds;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name: item?.name || '',
|
name: item?.name || '',
|
||||||
key: item?.key || '',
|
key: item?.key || '',
|
||||||
@@ -290,8 +320,8 @@ function buildEditorForm(item?: ManagedItem | DownstreamApiKeyItem | null): Edit
|
|||||||
maxRequests: item?.maxRequests === null || item?.maxRequests === undefined ? '' : String(item.maxRequests),
|
maxRequests: item?.maxRequests === null || item?.maxRequests === undefined ? '' : String(item.maxRequests),
|
||||||
expiresAt: toDateTimeLocal(item?.expiresAt),
|
expiresAt: toDateTimeLocal(item?.expiresAt),
|
||||||
enabled: item?.enabled ?? true,
|
enabled: item?.enabled ?? true,
|
||||||
selectedModels: uniqStrings(Array.isArray(item?.supportedModels) ? item!.supportedModels : []),
|
selectedModels: uniqStrings(selectedModels),
|
||||||
selectedGroupRouteIds: uniqIds(Array.isArray(item?.allowedRouteIds) ? item!.allowedRouteIds : []),
|
selectedGroupRouteIds: uniqIds(selectedGroupRouteIds),
|
||||||
siteWeightMultipliersText: JSON.stringify(item?.siteWeightMultipliers || {}, null, 2),
|
siteWeightMultipliersText: JSON.stringify(item?.siteWeightMultipliers || {}, null, 2),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -823,6 +853,7 @@ export default function DownstreamKeys() {
|
|||||||
const [editorOpen, setEditorOpen] = useState(false);
|
const [editorOpen, setEditorOpen] = useState(false);
|
||||||
const [editingId, setEditingId] = useState<number | null>(null);
|
const [editingId, setEditingId] = useState<number | null>(null);
|
||||||
const [editorForm, setEditorForm] = useState<EditorForm>(() => buildEditorForm());
|
const [editorForm, setEditorForm] = useState<EditorForm>(() => buildEditorForm());
|
||||||
|
const [createDefaultsPending, setCreateDefaultsPending] = useState(false);
|
||||||
const [drawerOpen, setDrawerOpen] = useState(false);
|
const [drawerOpen, setDrawerOpen] = useState(false);
|
||||||
const [selectedId, setSelectedId] = useState<number | null>(null);
|
const [selectedId, setSelectedId] = useState<number | null>(null);
|
||||||
const [selectedIds, setSelectedIds] = useState<number[]>([]);
|
const [selectedIds, setSelectedIds] = useState<number[]>([]);
|
||||||
@@ -981,9 +1012,33 @@ export default function DownstreamKeys() {
|
|||||||
return acc;
|
return acc;
|
||||||
}, { tokens: 0, requests: 0, cost: 0, enabled: 0 }), [visibleItems]);
|
}, { tokens: 0, requests: 0, cost: 0, enabled: 0 }), [visibleItems]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!editorOpen || editingId !== null || !createDefaultsPending) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultSelections = buildDefaultRouteSelections(routeOptions);
|
||||||
|
if (defaultSelections.selectedModels.length === 0 && defaultSelections.selectedGroupRouteIds.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setEditorForm((prev) => {
|
||||||
|
if (prev.selectedModels.length > 0 || prev.selectedGroupRouteIds.length > 0) {
|
||||||
|
return prev;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...prev,
|
||||||
|
selectedModels: defaultSelections.selectedModels,
|
||||||
|
selectedGroupRouteIds: defaultSelections.selectedGroupRouteIds,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
setCreateDefaultsPending(false);
|
||||||
|
}, [createDefaultsPending, editorOpen, editingId, routeOptions]);
|
||||||
|
|
||||||
const openCreate = () => {
|
const openCreate = () => {
|
||||||
setEditingId(null);
|
setEditingId(null);
|
||||||
setEditorForm(buildEditorForm());
|
setEditorForm(buildEditorForm(null, routeOptions, true));
|
||||||
|
setCreateDefaultsPending(true);
|
||||||
setEditorOpen(true);
|
setEditorOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -998,6 +1053,7 @@ export default function DownstreamKeys() {
|
|||||||
|
|
||||||
const openEdit = (item: ManagedItem) => {
|
const openEdit = (item: ManagedItem) => {
|
||||||
setEditingId(item.id);
|
setEditingId(item.id);
|
||||||
|
setCreateDefaultsPending(false);
|
||||||
setEditorForm(buildEditorForm(rawItemMap.get(item.id) || item));
|
setEditorForm(buildEditorForm(rawItemMap.get(item.id) || item));
|
||||||
setEditorOpen(true);
|
setEditorOpen(true);
|
||||||
};
|
};
|
||||||
@@ -1005,6 +1061,7 @@ export default function DownstreamKeys() {
|
|||||||
const closeEditor = () => {
|
const closeEditor = () => {
|
||||||
setEditorOpen(false);
|
setEditorOpen(false);
|
||||||
setEditingId(null);
|
setEditingId(null);
|
||||||
|
setCreateDefaultsPending(false);
|
||||||
setEditorForm(buildEditorForm());
|
setEditorForm(buildEditorForm());
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user