feat: add API access toggle to search space settings

This commit is contained in:
Anish Sarkar 2026-06-19 20:27:17 +05:30
parent 54a3ba122e
commit 630880bf7a
4 changed files with 102 additions and 1 deletions

View file

@ -3,6 +3,7 @@ import { toast } from "sonner";
import type {
CreateSearchSpaceRequest,
DeleteSearchSpaceRequest,
UpdateSearchSpaceApiAccessRequest,
UpdateSearchSpaceRequest,
} from "@/contracts/types/search-space.types";
import { searchSpacesApiService } from "@/lib/apis/search-spaces-api.service";
@ -50,6 +51,28 @@ export const updateSearchSpaceMutationAtom = atomWithMutation((get) => {
};
});
export const updateSearchSpaceApiAccessMutationAtom = atomWithMutation((get) => {
const activeSearchSpaceId = get(activeSearchSpaceIdAtom);
return {
mutationKey: ["update-search-space-api-access", activeSearchSpaceId],
enabled: !!activeSearchSpaceId,
mutationFn: async (request: UpdateSearchSpaceApiAccessRequest) => {
return searchSpacesApiService.updateSearchSpaceApiAccess(request);
},
onSuccess: (_, request: UpdateSearchSpaceApiAccessRequest) => {
toast.success("API access updated successfully");
queryClient.invalidateQueries({
queryKey: cacheKeys.searchSpaces.all,
});
queryClient.invalidateQueries({
queryKey: cacheKeys.searchSpaces.detail(String(request.id)),
});
},
};
});
export const deleteSearchSpaceMutationAtom = atomWithMutation((get) => {
const activeSearchSpaceId = get(activeSearchSpaceIdAtom);

View file

@ -5,11 +5,15 @@ import { useAtomValue } from "jotai";
import { useTranslations } from "next-intl";
import { useCallback, useEffect, useState } from "react";
import { toast } from "sonner";
import { updateSearchSpaceMutationAtom } from "@/atoms/search-spaces/search-space-mutation.atoms";
import {
updateSearchSpaceApiAccessMutationAtom,
updateSearchSpaceMutationAtom,
} from "@/atoms/search-spaces/search-space-mutation.atoms";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Skeleton } from "@/components/ui/skeleton";
import { Switch } from "@/components/ui/switch";
import { searchSpacesApiService } from "@/lib/apis/search-spaces-api.service";
import { authenticatedFetch } from "@/lib/auth-utils";
import { buildBackendUrl } from "@/lib/env-config";
@ -35,10 +39,14 @@ export function GeneralSettingsManager({ searchSpaceId }: GeneralSettingsManager
});
const { mutateAsync: updateSearchSpace } = useAtomValue(updateSearchSpaceMutationAtom);
const { mutateAsync: updateSearchSpaceApiAccess } = useAtomValue(
updateSearchSpaceApiAccessMutationAtom
);
const [name, setName] = useState("");
const [description, setDescription] = useState("");
const [saving, setSaving] = useState(false);
const [savingApiAccess, setSavingApiAccess] = useState(false);
const [isExporting, setIsExporting] = useState(false);
const hasSearchSpace = !!searchSpace;
const searchSpaceName = searchSpace?.name;
@ -113,6 +121,25 @@ export function GeneralSettingsManager({ searchSpaceId }: GeneralSettingsManager
handleSave();
};
const handleApiAccessToggle = useCallback(
async (enabled: boolean) => {
try {
setSavingApiAccess(true);
await updateSearchSpaceApiAccess({
id: searchSpaceId,
api_access_enabled: enabled,
});
await fetchSearchSpace();
} catch (error) {
console.error("Error updating API access:", error);
toast.error(error instanceof Error ? error.message : "Failed to update API access");
} finally {
setSavingApiAccess(false);
}
},
[fetchSearchSpace, searchSpaceId, updateSearchSpaceApiAccess]
);
if (loading) {
return (
<div className="space-y-4 md:space-y-6">
@ -179,6 +206,22 @@ export function GeneralSettingsManager({ searchSpaceId }: GeneralSettingsManager
</div>
</form>
<div className="border-t pt-6 flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
<div className="space-y-1">
<Label htmlFor="api-access-enabled">Programmatic API access</Label>
<p className="text-xs text-muted-foreground">
Allow personal access tokens to use this search space. Web and desktop sessions are
not affected.
</p>
</div>
<Switch
id="api-access-enabled"
checked={!!searchSpace?.api_access_enabled}
disabled={savingApiAccess}
onCheckedChange={handleApiAccessToggle}
/>
</div>
<div className="border-t pt-6 flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
<div className="space-y-1">
<Label>Export knowledge base</Label>

View file

@ -8,6 +8,7 @@ export const searchSpace = z.object({
created_at: z.string(),
user_id: z.string(),
citations_enabled: z.boolean(),
api_access_enabled: z.boolean().optional().default(false),
qna_custom_instructions: z.string().nullable(),
shared_memory_md: z.string().nullable().optional(),
ai_file_sort_enabled: z.boolean().optional().default(false),
@ -55,6 +56,7 @@ export const updateSearchSpaceRequest = z.object({
name: true,
description: true,
citations_enabled: true,
api_access_enabled: true,
qna_custom_instructions: true,
ai_file_sort_enabled: true,
})
@ -63,6 +65,16 @@ export const updateSearchSpaceRequest = z.object({
export const updateSearchSpaceResponse = searchSpace.omit({ member_count: true, is_owner: true });
export const updateSearchSpaceApiAccessRequest = z.object({
id: z.number(),
api_access_enabled: z.boolean(),
});
export const updateSearchSpaceApiAccessResponse = searchSpace.omit({
member_count: true,
is_owner: true,
});
/**
* Delete search space
*/
@ -89,5 +101,7 @@ export type GetSearchSpaceRequest = z.infer<typeof getSearchSpaceRequest>;
export type GetSearchSpaceResponse = z.infer<typeof getSearchSpaceResponse>;
export type UpdateSearchSpaceRequest = z.infer<typeof updateSearchSpaceRequest>;
export type UpdateSearchSpaceResponse = z.infer<typeof updateSearchSpaceResponse>;
export type UpdateSearchSpaceApiAccessRequest = z.infer<typeof updateSearchSpaceApiAccessRequest>;
export type UpdateSearchSpaceApiAccessResponse = z.infer<typeof updateSearchSpaceApiAccessResponse>;
export type DeleteSearchSpaceRequest = z.infer<typeof deleteSearchSpaceRequest>;
export type DeleteSearchSpaceResponse = z.infer<typeof deleteSearchSpaceResponse>;

View file

@ -14,7 +14,10 @@ import {
getSearchSpacesResponse,
leaveSearchSpaceResponse,
type UpdateSearchSpaceRequest,
type UpdateSearchSpaceApiAccessRequest,
updateSearchSpaceRequest,
updateSearchSpaceApiAccessRequest,
updateSearchSpaceApiAccessResponse,
updateSearchSpaceResponse,
} from "@/contracts/types/search-space.types";
import { ValidationError } from "../error";
@ -102,6 +105,24 @@ class SearchSpacesApiService {
});
};
updateSearchSpaceApiAccess = async (request: UpdateSearchSpaceApiAccessRequest) => {
const parsedRequest = updateSearchSpaceApiAccessRequest.safeParse(request);
if (!parsedRequest.success) {
console.error("Invalid request:", parsedRequest.error);
const errorMessage = parsedRequest.error.issues.map((issue) => issue.message).join(", ");
throw new ValidationError(`Invalid request: ${errorMessage}`);
}
return baseApiService.put(
`/api/v1/searchspaces/${request.id}/api-access`,
updateSearchSpaceApiAccessResponse,
{
body: { api_access_enabled: parsedRequest.data.api_access_enabled },
}
);
};
/**
* Delete a search space
*/