mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-06-26 21:39:43 +02:00
refactor(api): rename personal access tokens to API keys and update related UI components for consistency
This commit is contained in:
parent
96b64166b1
commit
d5e2540e51
9 changed files with 162 additions and 81 deletions
|
|
@ -1,10 +1,20 @@
|
|||
"use client";
|
||||
|
||||
import { Check, Copy, Info, Plus, Trash2 } from "lucide-react";
|
||||
import { Check, Copy, Info, Trash2 } from "lucide-react";
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from "@/components/ui/alert-dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
|
|
@ -16,6 +26,7 @@ import {
|
|||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { Spinner } from "@/components/ui/spinner";
|
||||
import { usePats } from "@/hooks/use-pats";
|
||||
import { copyToClipboard as copyToClipboardUtil } from "@/lib/utils";
|
||||
|
||||
|
|
@ -26,6 +37,7 @@ export function ApiKeyContent() {
|
|||
const [label, setLabel] = useState("");
|
||||
const [expiresInDays, setExpiresInDays] = useState("");
|
||||
const [copiedToken, setCopiedToken] = useState(false);
|
||||
const [deleteTarget, setDeleteTarget] = useState<{ id: number; label: string } | null>(null);
|
||||
|
||||
const sortedTokens = useMemo(() => tokens, [tokens]);
|
||||
|
||||
|
|
@ -51,93 +63,112 @@ export function ApiKeyContent() {
|
|||
}
|
||||
}, [createdToken]);
|
||||
|
||||
const handleDelete = useCallback(
|
||||
async (id: number, tokenLabel: string) => {
|
||||
if (!window.confirm(`Delete personal access token "${tokenLabel}"? This cannot be undone.`)) {
|
||||
return;
|
||||
}
|
||||
await deleteToken(id);
|
||||
},
|
||||
[deleteToken]
|
||||
);
|
||||
const handleConfirmDelete = useCallback(async () => {
|
||||
if (!deleteTarget) return;
|
||||
|
||||
await deleteToken(deleteTarget.id);
|
||||
setDeleteTarget(null);
|
||||
}, [deleteTarget, deleteToken]);
|
||||
|
||||
return (
|
||||
<div className="space-y-6 min-w-0 overflow-hidden">
|
||||
<div className="space-y-6 min-w-0">
|
||||
<Alert>
|
||||
<Info />
|
||||
<AlertDescription>
|
||||
Personal access tokens are long-lived credentials for extensions, Obsidian, and
|
||||
programmatic API clients. Copy a token when you create it; it is shown only once.
|
||||
API keys let extensions, Obsidian, and other apps connect to SurfSense.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold tracking-tight">Personal access tokens</h3>
|
||||
<h3 className="text-sm font-semibold tracking-tight">API keys</h3>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Expired tokens stay listed until you delete them.
|
||||
Expired API keys stay listed until you delete them.
|
||||
</p>
|
||||
</div>
|
||||
<Button size="sm" onClick={() => setCreateOpen(true)}>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
Create token
|
||||
Create API key
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="min-w-0 overflow-hidden rounded-lg border border-border/60">
|
||||
{isLoading ? (
|
||||
<div className="space-y-3 p-4">
|
||||
<Skeleton className="h-12 w-full" />
|
||||
<Skeleton className="h-12 w-full" />
|
||||
</div>
|
||||
) : sortedTokens.length > 0 ? (
|
||||
<div className="divide-y divide-border/60">
|
||||
{sortedTokens.map((token) => {
|
||||
const expiresAt = token.expires_at ? new Date(token.expires_at) : null;
|
||||
const isExpired = expiresAt ? expiresAt.getTime() <= Date.now() : false;
|
||||
return (
|
||||
<div key={token.id} className="flex items-center gap-3 p-4">
|
||||
{isLoading ? (
|
||||
<div className="-m-1 grid grid-cols-1 gap-3 p-1">
|
||||
{["skeleton-a", "skeleton-b"].map((key) => (
|
||||
<Card
|
||||
key={key}
|
||||
className="group relative overflow-hidden transition-all duration-200 border-accent bg-accent/20 hover:shadow-md h-full"
|
||||
>
|
||||
<CardContent className="p-4 flex flex-col gap-3 h-full min-h-24">
|
||||
<Skeleton className="h-4 w-32 md:w-40 bg-accent" />
|
||||
<Skeleton className="h-3 w-full bg-accent" />
|
||||
<Skeleton className="h-3 w-24 md:w-28 bg-accent" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
) : sortedTokens.length > 0 ? (
|
||||
<div className="-m-1 grid grid-cols-1 gap-3 p-1">
|
||||
{sortedTokens.map((token) => {
|
||||
const expiresAt = token.expires_at ? new Date(token.expires_at) : null;
|
||||
const isExpired = expiresAt ? expiresAt.getTime() <= Date.now() : false;
|
||||
return (
|
||||
<Card
|
||||
key={token.id}
|
||||
className="group relative overflow-hidden transition-all duration-200 border-accent bg-accent/20 hover:shadow-md h-full"
|
||||
>
|
||||
<CardContent className="flex min-h-24 items-center gap-3 p-4">
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<p className="truncate text-sm font-medium">{token.label}</p>
|
||||
{isExpired ? <Badge variant="secondary">Expired</Badge> : null}
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<h4 className="truncate text-sm font-semibold tracking-tight">
|
||||
{token.label}
|
||||
</h4>
|
||||
{isExpired ? (
|
||||
<span className="rounded-md border-0 bg-muted px-1.5 py-0.5 text-[10px] font-medium text-muted-foreground">
|
||||
Expired
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
<p className="truncate font-mono text-xs text-muted-foreground">
|
||||
{token.prefix}...
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Expires: {expiresAt ? expiresAt.toLocaleDateString() : "Never"} · Last used:{" "}
|
||||
{token.last_used_at ? new Date(token.last_used_at).toLocaleString() : "Never"}
|
||||
</p>
|
||||
</div>
|
||||
<p className="font-mono text-xs text-muted-foreground">{token.prefix}...</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Expires: {expiresAt ? expiresAt.toLocaleDateString() : "Never"} · Last used:{" "}
|
||||
{token.last_used_at ? new Date(token.last_used_at).toLocaleString() : "Never"}
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
disabled={isMutating}
|
||||
onClick={() => handleDelete(token.id, token.label)}
|
||||
onClick={() => setDeleteTarget({ id: token.id, label: token.label })}
|
||||
className="h-7 w-7 shrink-0 rounded-lg text-muted-foreground transition-opacity duration-150 hover:text-accent-foreground sm:opacity-0 sm:pointer-events-none sm:group-hover:opacity-100 sm:group-hover:pointer-events-auto"
|
||||
>
|
||||
<Trash2 className="h-4 w-4 text-muted-foreground" />
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<p className="p-6 text-center text-sm text-muted-foreground">
|
||||
No personal access tokens yet.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<p className="py-6 text-center text-sm text-muted-foreground">
|
||||
No API keys yet.
|
||||
</p>
|
||||
)}
|
||||
|
||||
<Dialog open={createOpen} onOpenChange={setCreateOpen}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Create personal access token</DialogTitle>
|
||||
<DialogTitle>Create API key</DialogTitle>
|
||||
<DialogDescription>
|
||||
Name this token so you can recognize where it is used later.
|
||||
Name this API key so you can recognize where it is used later.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="pat-label">Label</Label>
|
||||
<Label htmlFor="pat-label">Name</Label>
|
||||
<Input
|
||||
id="pat-label"
|
||||
value={label}
|
||||
|
|
@ -158,11 +189,24 @@ export function ApiKeyContent() {
|
|||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setCreateOpen(false)}>
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={() => setCreateOpen(false)}
|
||||
disabled={isMutating}
|
||||
className="text-sm h-9"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button disabled={isMutating || !label.trim()} onClick={handleCreate}>
|
||||
Create token
|
||||
<Button
|
||||
size="sm"
|
||||
disabled={isMutating || !label.trim()}
|
||||
onClick={handleCreate}
|
||||
className="relative text-sm h-9 min-w-[128px]"
|
||||
>
|
||||
<span className={isMutating ? "opacity-0" : ""}>Create API key</span>
|
||||
{isMutating && <Spinner size="sm" className="absolute" />}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
|
|
@ -171,16 +215,21 @@ export function ApiKeyContent() {
|
|||
<Dialog open={!!createdToken} onOpenChange={(open) => !open && setCreatedToken(null)}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Copy your token now</DialogTitle>
|
||||
<DialogTitle>Copy your API key now</DialogTitle>
|
||||
<DialogDescription>
|
||||
This token is shown only once. Store it somewhere secure before closing this dialog.
|
||||
This API key is shown only once. Store it somewhere secure before closing this dialog.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="flex items-center gap-2 rounded-md border border-border/60 bg-muted/30 p-2">
|
||||
<code className="min-w-0 flex-1 overflow-x-auto whitespace-nowrap text-xs">
|
||||
{createdToken?.token}
|
||||
</code>
|
||||
<Button variant="outline" size="sm" onClick={copyCreatedToken}>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={copyCreatedToken}
|
||||
className="border-0 bg-muted/30 hover:bg-muted/50"
|
||||
>
|
||||
{copiedToken ? <Check className="h-4 w-4" /> : <Copy className="h-4 w-4" />}
|
||||
</Button>
|
||||
</div>
|
||||
|
|
@ -189,6 +238,41 @@ export function ApiKeyContent() {
|
|||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<AlertDialog
|
||||
open={deleteTarget !== null}
|
||||
onOpenChange={(open) => !open && setDeleteTarget(null)}
|
||||
>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Delete API key?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
<span className="font-medium text-foreground">{deleteTarget?.label}</span> will be
|
||||
permanently removed. This cannot be undone.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel disabled={isMutating}>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
disabled={isMutating}
|
||||
className="bg-destructive text-white hover:bg-destructive/90"
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
void handleConfirmDelete();
|
||||
}}
|
||||
>
|
||||
{isMutating ? (
|
||||
<span className="inline-flex items-center gap-2">
|
||||
<Spinner size="xs" />
|
||||
Deleting...
|
||||
</span>
|
||||
) : (
|
||||
"Delete"
|
||||
)}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,13 +38,13 @@ export function CommunityPromptsContent() {
|
|||
const list = prompts ?? [];
|
||||
|
||||
return (
|
||||
<div className="space-y-6 min-w-0 overflow-hidden">
|
||||
<div className="space-y-6 min-w-0">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Prompts shared by other users. Add any to your collection with one click.
|
||||
</p>
|
||||
|
||||
{isLoading && (
|
||||
<div className="space-y-2">
|
||||
<div className="-m-1 space-y-2 p-1">
|
||||
{["skeleton-a", "skeleton-b", "skeleton-c"].map((key) => (
|
||||
<Card key={key} className="border-accent bg-accent/20">
|
||||
<CardContent className="p-4 flex flex-col gap-3 min-h-24">
|
||||
|
|
@ -76,7 +76,7 @@ export function CommunityPromptsContent() {
|
|||
)}
|
||||
|
||||
{!isLoading && !isError && list.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
<div className="-m-1 space-y-2 p-1">
|
||||
{list.map((prompt) => (
|
||||
<Card
|
||||
key={prompt.id}
|
||||
|
|
|
|||
|
|
@ -148,7 +148,7 @@ export function PromptsContent() {
|
|||
const list = prompts ?? [];
|
||||
|
||||
return (
|
||||
<div className="space-y-6 min-w-0 overflow-hidden">
|
||||
<div className="space-y-6 min-w-0">
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Create prompt templates triggered with <ShortcutKbd keys={["/"]} className="ml-0" /> in
|
||||
|
|
@ -276,7 +276,7 @@ export function PromptsContent() {
|
|||
</Dialog>
|
||||
|
||||
{isLoading && (
|
||||
<div className="space-y-2">
|
||||
<div className="-m-1 space-y-2 p-1">
|
||||
{["skeleton-a", "skeleton-b", "skeleton-c"].map((key) => (
|
||||
<Card key={key} className="border-accent bg-accent/20">
|
||||
<CardContent className="p-4 flex flex-col gap-3 min-h-24">
|
||||
|
|
@ -308,7 +308,7 @@ export function PromptsContent() {
|
|||
)}
|
||||
|
||||
{!isLoading && !isError && list.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
<div className="-m-1 space-y-2 p-1">
|
||||
{list.map((prompt) => (
|
||||
<div
|
||||
key={prompt.id}
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ export const deletePublicChatSnapshotMutationAtom = atomWithMutation(() => ({
|
|||
toast.success("Public link deleted");
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
console.error("Failed to delete public chat link:", error);
|
||||
console.error("Failed to delete public chat:", error);
|
||||
toast.error("Failed to delete public link");
|
||||
},
|
||||
}));
|
||||
|
|
|
|||
|
|
@ -15,9 +15,9 @@ This update brings **public sharing, image generation**, a redesigned Documents
|
|||
|
||||
#### Public Sharing
|
||||
|
||||
- **Public Chat Links**: Share snapshots of chats via public links.
|
||||
- **Public Chats**: Share snapshots of chats via public links.
|
||||
- **Sharing Permissions**: Search Space owners control who can create and manage public links.
|
||||
- **Link Management Page**: View and revoke all public chat links from Search Space Settings.
|
||||
- **Link Management Page**: View and revoke all public chats from Search Space Settings.
|
||||
|
||||
#### Auto (Load Balanced) Mode
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +1,10 @@
|
|||
import { Link2Off } from "lucide-react";
|
||||
|
||||
interface PublicChatSnapshotsEmptyStateProps {
|
||||
title?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export function PublicChatSnapshotsEmptyState({
|
||||
title = "No public chat links",
|
||||
title = "No public chats",
|
||||
description = "When you create public links to share chats, they will appear here.",
|
||||
}: PublicChatSnapshotsEmptyStateProps) {
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -115,7 +115,7 @@ export function PublicChatSnapshotsManager({
|
|||
<Alert variant="destructive">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertDescription>
|
||||
Failed to load public chat links. Please try again later.
|
||||
Failed to load public chats. Please try again later.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
);
|
||||
|
|
@ -127,7 +127,7 @@ export function PublicChatSnapshotsManager({
|
|||
<Alert>
|
||||
<Info />
|
||||
<AlertDescription>
|
||||
You don't have permission to view public chat links in this search space.
|
||||
You don't have permission to view public chats in this search space.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
);
|
||||
|
|
@ -140,8 +140,8 @@ export function PublicChatSnapshotsManager({
|
|||
<Alert>
|
||||
<Info />
|
||||
<AlertDescription>
|
||||
Public chat links allow anyone with the URL to view a snapshot of a chat. These links do
|
||||
not update when the original chat changes.
|
||||
Public chats allow anyone with the URL to view a snapshot of a chat. They do not update
|
||||
when the original chat changes.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
|
|
|
|||
|
|
@ -208,10 +208,9 @@ export function GeneralSettingsManager({ searchSpaceId }: GeneralSettingsManager
|
|||
|
||||
<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>
|
||||
<Label htmlFor="api-access-enabled">API key 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.
|
||||
Allow API keys to access this search space.
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
|
|
|
|||
|
|
@ -746,8 +746,8 @@
|
|||
"nav_agent_models_desc": "Models with prompts & citations",
|
||||
"nav_system_instructions": "System Instructions",
|
||||
"nav_system_instructions_desc": "SearchSpace-wide AI instructions",
|
||||
"nav_public_links": "Public Chat Links",
|
||||
"nav_public_links_desc": "Manage publicly shared chat links",
|
||||
"nav_public_links": "Public Chats",
|
||||
"nav_public_links_desc": "Manage publicly shared chats",
|
||||
"nav_team_roles": "Team Roles",
|
||||
"nav_team_roles_desc": "Manage team roles & permissions",
|
||||
"general_name_label": "Name",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue