diff --git a/web/features/new-rag/__tests__/knowledge-settings-form.spec.tsx b/web/features/new-rag/__tests__/knowledge-settings-form.spec.tsx index 7db99327162..7fca489ed7a 100644 --- a/web/features/new-rag/__tests__/knowledge-settings-form.spec.tsx +++ b/web/features/new-rag/__tests__/knowledge-settings-form.spec.tsx @@ -949,6 +949,59 @@ describe('KnowledgeSettingsForm', () => { expect(thresholdSwitch).toHaveAttribute('aria-checked', 'true') }) + it('keeps a rejected score threshold draft and explains permission failures', async () => { + const user = userEvent.setup() + serviceMock.patchSettings.mockRejectedValueOnce( + new Response( + JSON.stringify({ + code: 'knowledge_fs_access_denied', + message: 'You do not have permission to perform this KnowledgeFS operation.', + status: 403, + }), + { status: 403 }, + ), + ) + renderForm({ + settings: { + ...settings, + retrieval: { + ...settings.retrieval, + score_threshold: { + ...settings.retrieval.score_threshold, + enabled: true, + }, + }, + }, + }) + + const thresholdInput = screen.getByRole('spinbutton', { + name: 'appDebug.datasetConfig.score_threshold', + }) + await user.clear(thresholdInput) + await user.type(thresholdInput, '0.72') + await user.tab() + + expect(await screen.findByText('dataset.newKnowledge.permissionRestricted')).toBeInTheDocument() + expect(screen.queryByText('dataset.newKnowledge.settings.saveFailed')).not.toBeInTheDocument() + expect(thresholdInput).toHaveValue(0.72) + + await user.click(screen.getByRole('button', { name: 'common.operation.retry' })) + + await waitFor(() => expect(serviceMock.patchSettings).toHaveBeenCalledTimes(2)) + expect(serviceMock.patchSettings).toHaveBeenLastCalledWith( + { + body: { + expectedRevision: 5, + retrieval: expect.objectContaining({ + scoreThreshold: expect.objectContaining({ value: 0.72 }), + }), + }, + params: { control_space_id: 'space-1' }, + }, + expect.anything(), + ) + }) + it('requires a rerank model for a legacy knowledge base and saves it as enabled', async () => { const user = userEvent.setup() const onDraftFinish = vi.fn() diff --git a/web/features/new-rag/knowledge-settings-form.tsx b/web/features/new-rag/knowledge-settings-form.tsx index 96f87441ae2..79a127a2b00 100644 --- a/web/features/new-rag/knowledge-settings-form.tsx +++ b/web/features/new-rag/knowledge-settings-form.tsx @@ -64,6 +64,10 @@ const RERANK_MODEL_ERROR_ID = 'knowledge-rerank-model-error' type BasicSaveSlice = 'members' | 'space' type SaveErrorSlice = 'basic' | 'externalAccess' | 'settings' +type SaveError = { + kind: 'generic' | 'permission' + slice: SaveErrorSlice +} type ExternalAccessDraft = { apiEnabled: boolean @@ -286,7 +290,7 @@ export function KnowledgeSettingsForm({ }), ) const [nameTouched, setNameTouched] = useState(false) - const [saveErrorSlice, setSaveErrorSlice] = useState() + const [saveError, setSaveError] = useState() const [isBasicRefreshing, setIsBasicRefreshing] = useState(false) const [pendingMigrationId, setPendingMigrationId] = useState() const [pendingEmbeddingModel, setPendingEmbeddingModel] = useState() @@ -408,7 +412,9 @@ export function KnowledgeSettingsForm({ consoleQuery.knowledgeFs.spaces.byControlSpaceId.externalAccess.put.mutationOptions(), ) const settingsMutation = useMutation( - consoleQuery.knowledgeFs.spaces.byControlSpaceId.settings.patch.mutationOptions(), + consoleQuery.knowledgeFs.spaces.byControlSpaceId.settings.patch.mutationOptions({ + context: { silent: true }, + }), ) const migrationQuery = useQuery({ ...consoleQuery.knowledgeFs.spaces.byControlSpaceId.settings.migrations.byMigrationId.get.queryOptions( @@ -463,7 +469,7 @@ export function KnowledgeSettingsForm({ setVisibility(space.visibility) setSelectedMemberIds(initialSelectedMemberIds) setNameTouched(false) - if (saveErrorSlice === 'basic') setSaveErrorSlice(undefined) + if (saveError?.slice === 'basic') setSaveError(undefined) onDraftFinish?.() } @@ -537,7 +543,7 @@ export function KnowledgeSettingsForm({ // oxlint-disable-next-line eslint-react/set-state-in-effect -- A terminal remote migration retires the local polling guard. setPendingMigrationId(undefined) // oxlint-disable-next-line eslint-react/set-state-in-effect -- The authoritative failed migration transitions the save UI to its retry state. - setSaveErrorSlice('settings') + setSaveError({ kind: 'generic', slice: 'settings' }) } }, [ invalidateSettingsQueries, @@ -550,7 +556,7 @@ export function KnowledgeSettingsForm({ const performBasicSave = async () => { if (saveDisabled || !canEdit) return - setSaveErrorSlice(undefined) + setSaveError(undefined) try { const saveSlice = async ( @@ -605,7 +611,7 @@ export function KnowledgeSettingsForm({ () => setIsBasicRefreshing(false), ) } catch { - setSaveErrorSlice('basic') + setSaveError({ kind: 'generic', slice: 'basic' }) } } @@ -621,7 +627,7 @@ export function KnowledgeSettingsForm({ let nextDraft: ExternalAccessDraft | undefined = draft while (nextDraft) { queuedExternalAccessDraftRef.current = undefined - setSaveErrorSlice(undefined) + setSaveError(undefined) try { await externalAccessMutation.mutateAsync({ body: { @@ -639,7 +645,7 @@ export function KnowledgeSettingsForm({ externalAccessSaveInFlightRef.current = false setApiEnabled(externalAccessBaselineRef.current.apiEnabled) setWorkflowEnabled(externalAccessBaselineRef.current.workflowEnabled) - setSaveErrorSlice('externalAccess') + setSaveError({ kind: 'generic', slice: 'externalAccess' }) return } } @@ -691,7 +697,7 @@ export function KnowledgeSettingsForm({ } } - setSaveErrorSlice(undefined) + setSaveError(undefined) try { const result = await settingsMutation.mutateAsync({ body, @@ -714,8 +720,11 @@ export function KnowledgeSettingsForm({ } await invalidateSettingsQueries() return 'saved' as const - } catch { - setSaveErrorSlice('settings') + } catch (error) { + setSaveError({ + kind: error instanceof Response && error.status === 403 ? 'permission' : 'generic', + slice: 'settings', + }) return 'failed' as const } } @@ -799,11 +808,11 @@ export function KnowledgeSettingsForm({ } const retrySave = () => { - if (saveErrorSlice === 'externalAccess') { + if (saveError?.slice === 'externalAccess') { void performExternalAccessSave(pendingExternalAccessRef.current) return } - if (saveErrorSlice === 'settings') { + if (saveError?.slice === 'settings') { void performSettingsSave(pendingSettingsDraftRef.current ?? liveSettingsDraftRef.current) return } @@ -976,14 +985,16 @@ export function KnowledgeSettingsForm({ /> )} - {saveErrorSlice && ( + {saveError && (
- {t(($) => $['newKnowledge.settings.saveFailed'])} + {saveError.kind === 'permission' + ? t(($) => $['newKnowledge.permissionRestricted']) + : t(($) => $['newKnowledge.settings.saveFailed'])}