From e118eeae55593edc6cda3d90289f82c2c049966b Mon Sep 17 00:00:00 2001 From: Ryan Hopper-Lowe Date: Wed, 29 Jan 2025 15:22:38 -0600 Subject: [PATCH] feat: add credential list to threads page sidebar --- .../components/chat/shared/thread-helpers.ts | 33 +++ ui/admin/app/components/thread/ThreadMeta.tsx | 240 +++++++++++------- ui/admin/app/lib/routers/apiRoutes.ts | 33 +-- .../lib/service/api/credentialApiService.ts | 17 +- 4 files changed, 198 insertions(+), 125 deletions(-) diff --git a/ui/admin/app/components/chat/shared/thread-helpers.ts b/ui/admin/app/components/chat/shared/thread-helpers.ts index a5a30911f..eab88ef62 100644 --- a/ui/admin/app/components/chat/shared/thread-helpers.ts +++ b/ui/admin/app/components/chat/shared/thread-helpers.ts @@ -1,9 +1,11 @@ import { toast } from "sonner"; import useSWR from "swr"; +import { CredentialNamespace } from "~/lib/model/credentials"; import { KnowledgeFileNamespace } from "~/lib/model/knowledge"; import { UpdateThread } from "~/lib/model/threads"; import { AgentService } from "~/lib/service/api/agentService"; +import { CredentialApiService } from "~/lib/service/api/credentialApiService"; import { KnowledgeFileService } from "~/lib/service/api/knowledgeFileApiService"; import { ThreadsService } from "~/lib/service/api/threadsService"; @@ -68,3 +70,34 @@ export function useThreadAgents(threadId?: Nullish) { AgentService.getAgentById(agentId) ); } + +export function useThreadCredentials(threadId: Nullish) { + const getCredentials = useSWR( + CredentialApiService.getCredentials.key( + CredentialNamespace.Threads, + threadId + ), + ({ namespace, entityId }) => + CredentialApiService.getCredentials(namespace, entityId) + ); + + const handleDeleteCredential = async (credentialName: string) => { + if (!threadId) return; + + return await CredentialApiService.deleteCredential( + CredentialNamespace.Threads, + threadId, + credentialName + ); + }; + + const deleteCredential = useAsync(handleDeleteCredential, { + onSuccess: (credentialId) => { + getCredentials.mutate((creds) => + creds?.filter((c) => c.name !== credentialId) + ); + }, + }); + + return { getCredentials, deleteCredential }; +} diff --git a/ui/admin/app/components/thread/ThreadMeta.tsx b/ui/admin/app/components/thread/ThreadMeta.tsx index d35cace00..8212eef0e 100644 --- a/ui/admin/app/components/thread/ThreadMeta.tsx +++ b/ui/admin/app/components/thread/ThreadMeta.tsx @@ -4,7 +4,10 @@ import { ExternalLink, FileIcon, FilesIcon, + KeyIcon, + LucideIcon, RotateCwIcon, + TrashIcon, } from "lucide-react"; import { $path } from "safe-routes"; @@ -16,9 +19,11 @@ import { ThreadsService } from "~/lib/service/api/threadsService"; import { cn } from "~/lib/utils"; import { + useThreadCredentials, useThreadFiles, useThreadKnowledge, } from "~/components/chat/shared/thread-helpers"; +import { ConfirmationDialog } from "~/components/composed/ConfirmationDialog"; import { Truncate } from "~/components/composed/typography"; import { Accordion, @@ -36,6 +41,7 @@ import { TooltipContent, TooltipTrigger, } from "~/components/ui/tooltip"; +import { useConfirmationDialog } from "~/hooks/component-helpers/useConfirmationDialog"; interface ThreadMetaProps { entity: Agent | Workflow; @@ -57,6 +63,11 @@ export function ThreadMeta({ entity, thread, className }: ThreadMetaProps) { const getKnowledge = useThreadKnowledge(thread.id); const { data: knowledge = [] } = getKnowledge; + const { getCredentials, deleteCredential } = useThreadCredentials(thread.id); + const { data: credentials = [] } = getCredentials; + + const { dialogProps, interceptAsync } = useConfirmationDialog(); + return ( @@ -128,97 +139,150 @@ export function ThreadMeta({ entity, thread, className }: ThreadMetaProps) { - - {files.length > 0 && ( - - -
- - - Files - - - -
-
- -
    - {files.map((file) => ( - - ThreadsService.downloadFile(thread.id, file.name) - } - > -
  • - - - - - - Download - - - {file.name} - -
  • -
    - ))} -
-
-
- )} - {knowledge.length > 0 && ( - - -
- - - Knowledge Files - - - + + + Download + + - - -
-
- -
    - {knowledge.map((file) => ( -
  • - - {file.fileName} -
  • - ))} -
-
-
- )} + {file.name} + + + + )} + /> + + getKnowledge.mutate()} + items={knowledge} + renderItem={(file) => ( +
  • + +

    {file.fileName}

    +
  • + )} + /> + + getCredentials.mutate()} + items={credentials} + renderItem={(credential) => ( +
  • +

    {credential.name}

    + + +
  • + )} + />
    + +
    ); } + +type ThreadMetaAccordionItemProps = { + value: string; + icon: LucideIcon; + title: string; + isLoading?: boolean; + onRefresh?: (e: React.MouseEvent) => void; + items: T[]; + renderItem: (item: T) => React.ReactNode; + emptyMessage?: string; +}; + +function ThreadMetaAccordionItem(props: ThreadMetaAccordionItemProps) { + const Icon = props.icon; + return ( + + +
    + + + {props.title} + + + {props.onRefresh && ( + + )} +
    +
    + +
      + {props.items.length ? ( + props.items.map((item) => props.renderItem(item)) + ) : ( +
    • +

      {props.emptyMessage || `No ${props.title.toLowerCase()}`}

      +
    • + )} +
    +
    +
    + ); +} diff --git a/ui/admin/app/lib/routers/apiRoutes.ts b/ui/admin/app/lib/routers/apiRoutes.ts index 9222a7f3a..b972013e6 100644 --- a/ui/admin/app/lib/routers/apiRoutes.ts +++ b/ui/admin/app/lib/routers/apiRoutes.ts @@ -35,34 +35,7 @@ const buildUrl = (path: string, params?: object) => { export const ApiRoutes = { assistants: { - base: () => buildUrl("/assistants"), getAssistants: () => buildUrl("/assistants"), - getCredentials: (assistantId: string) => - buildUrl(`/assistants/${assistantId}/credentials`), - deleteCredential: (assistantId: string, credentialId: string) => - buildUrl(`/assistants/${assistantId}/credentials/${credentialId}`), - getEvents: (assistantId: string) => - buildUrl(`/assistants/${assistantId}/events`), - invoke: (assistantId: string) => - buildUrl(`/assistants/${assistantId}/invoke`), - getTools: (assistantId: string) => - buildUrl(`/assistants/${assistantId}/tools`), - deleteTool: (assistantId: string, toolId: string) => - buildUrl(`/assistants/${assistantId}/tools/${toolId}`), - getFiles: (assistantId: string) => - buildUrl(`/assistants/${assistantId}/files`), - getFileById: (assistantId: string, fileId: string) => - buildUrl(`/assistants/${assistantId}/files/${fileId}`), - uploadFile: (assistantId: string) => - buildUrl(`/assistants/${assistantId}/files`), - deleteFile: (assistantId: string, fileId: string) => - buildUrl(`/assistants/${assistantId}/files/${fileId}`), - getKnowledge: (assistantId: string) => - buildUrl(`/assistants/${assistantId}/knowledge`), - addKnowledge: (assistantId: string, fileName: string) => - buildUrl(`/assistants/${assistantId}/knowledge/${fileName}`), - deleteKnowledge: (assistantId: string, fileName: string) => - buildUrl(`/assistants/${assistantId}/knowledge/${fileName}`), }, knowledgeSources: { getKnowledgeSources: ( @@ -189,10 +162,8 @@ export const ApiRoutes = { updateEnv: (entityId: string) => buildUrl(`/agents/${entityId}/env`), }, credentials: { - getCredentialsForEntity: ( - namespace: CredentialNamespace, - entityId: string - ) => buildUrl(`/${namespace}/${entityId}/credentials`), + getCredentials: (namespace: CredentialNamespace, entityId: string) => + buildUrl(`/${namespace}/${entityId}/credentials`), deleteCredential: ( namespace: CredentialNamespace, entityId: string, diff --git a/ui/admin/app/lib/service/api/credentialApiService.ts b/ui/admin/app/lib/service/api/credentialApiService.ts index a9c386950..698896b1f 100644 --- a/ui/admin/app/lib/service/api/credentialApiService.ts +++ b/ui/admin/app/lib/service/api/credentialApiService.ts @@ -1,6 +1,6 @@ import { Credential, CredentialNamespace } from "~/lib/model/credentials"; import { EntityList } from "~/lib/model/primitives"; -import { ApiRoutes } from "~/lib/routers/apiRoutes"; +import { ApiRoutes, createRevalidate } from "~/lib/routers/apiRoutes"; import { request } from "~/lib/service/api/primitives"; async function getCredentials( @@ -8,7 +8,7 @@ async function getCredentials( entityId: string ) { const { data } = await request>({ - url: ApiRoutes.credentials.getCredentialsForEntity(namespace, entityId).url, + url: ApiRoutes.credentials.getCredentials(namespace, entityId).url, }); return data.items ?? []; @@ -20,25 +20,30 @@ getCredentials.key = ( if (!entityId) return null; return { - url: ApiRoutes.credentials.getCredentialsForEntity(namespace, entityId) - .path, + url: ApiRoutes.credentials.getCredentials(namespace, entityId).path, entityId, namespace, }; }; +getCredentials.revalidate = createRevalidate( + ApiRoutes.credentials.getCredentials +); async function deleteCredential( namespace: CredentialNamespace, entityId: string, - credentialId: string + credentialName: string ) { await request({ url: ApiRoutes.credentials.deleteCredential( namespace, entityId, - credentialId + credentialName ).url, + method: "DELETE", }); + + return credentialName; } export const CredentialApiService = {