diff --git a/ui/admin/app/components/agent/AgentDropdownActions.tsx b/ui/admin/app/components/agent/AgentDropdownActions.tsx new file mode 100644 index 000000000..6ba23dcdb --- /dev/null +++ b/ui/admin/app/components/agent/AgentDropdownActions.tsx @@ -0,0 +1,73 @@ +import { EllipsisVerticalIcon } from "lucide-react"; +import { useNavigate } from "react-router"; +import { $path } from "safe-routes"; +import { toast } from "sonner"; +import { mutate } from "swr"; + +import { Agent } from "~/lib/model/agents"; +import { AgentService } from "~/lib/service/api/agentService"; + +import { ConfirmationDialog } from "~/components/composed/ConfirmationDialog"; +import { Button } from "~/components/ui/button"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "~/components/ui/dropdown-menu"; +import { useConfirmationDialog } from "~/hooks/component-helpers/useConfirmationDropdown"; +import { useAsync } from "~/hooks/useAsync"; + +export function AgentDropdownActions({ agent }: { agent: Agent }) { + const navigate = useNavigate(); + + const deleteAgent = useAsync(AgentService.deleteAgent, { + onSuccess: () => { + mutate(AgentService.getAgents.key()); + toast.success("Agent deleted"); + navigate($path("/agents")); + }, + onError: (error) => { + if (error instanceof Error) return toast.error(error.message); + + toast.error("Something went wrong"); + }, + }); + + const { dialogProps, interceptAsync } = useConfirmationDialog(); + + const handleDelete = () => + interceptAsync(() => deleteAgent.executeAsync(agent.id)); + + return ( + <> + + + + + + + + Delete Agent + + + + + + + ); +} diff --git a/ui/admin/app/components/agent/AgentPublishStatus.tsx b/ui/admin/app/components/agent/AgentPublishStatus.tsx index fae0d9310..7a03d971a 100644 --- a/ui/admin/app/components/agent/AgentPublishStatus.tsx +++ b/ui/admin/app/components/agent/AgentPublishStatus.tsx @@ -8,6 +8,7 @@ import { ConsumptionUrl } from "~/lib/routers/baseRouter"; import { AssistantApiService } from "~/lib/service/api/assistantApiService"; import { TypographySmall } from "~/components/Typography"; +import { AgentDropdownActions } from "~/components/agent/AgentDropdownActions"; import { Publish } from "~/components/agent/Publish"; import { Unpublish } from "~/components/agent/Unpublish"; import { CopyText } from "~/components/composed/CopyText"; @@ -40,14 +41,18 @@ export function AgentPublishStatus({
{renderAgentRef()} - {agent.alias ? ( - onChange({ alias: "" })} /> - ) : ( - onChange({ alias })} - /> - )} +
+ {agent.alias ? ( + onChange({ alias: "" })} /> + ) : ( + onChange({ alias })} + /> + )} + + +
); diff --git a/ui/admin/app/components/ui/dropdown-menu.tsx b/ui/admin/app/components/ui/dropdown-menu.tsx index b837c5600..d6633a82e 100644 --- a/ui/admin/app/components/ui/dropdown-menu.tsx +++ b/ui/admin/app/components/ui/dropdown-menu.tsx @@ -4,6 +4,7 @@ import { ChevronRightIcon, DotFilledIcon, } from "@radix-ui/react-icons"; +import { VariantProps, cva } from "class-variance-authority"; import * as React from "react"; import { cn } from "~/lib/utils"; @@ -77,19 +78,33 @@ const DropdownMenuContent = React.forwardRef< )); DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName; +const dropdownMenuItemVariants = cva( + "relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors data-[disabled]:pointer-events-none data-[disabled]:opacity-50", + { + variants: { + variant: { + default: "focus:bg-accent focus:text-accent-foreground", + destructive: + "text-destructive focus:text-destructive-foreground focus:bg-destructive", + }, + inset: { true: "pl-8", false: "" }, + }, + defaultVariants: { variant: "default", inset: false }, + } +); + +type DropdownMenuItemProps = React.ComponentPropsWithoutRef< + typeof DropdownMenuPrimitive.Item +> & + VariantProps; + const DropdownMenuItem = React.forwardRef< React.ElementRef, - React.ComponentPropsWithoutRef & { - inset?: boolean; - } ->(({ className, inset, ...props }, ref) => ( + DropdownMenuItemProps +>(({ className, inset, variant, ...props }, ref) => ( )); diff --git a/ui/admin/app/hooks/component-helpers/useConfirmationDropdown.tsx b/ui/admin/app/hooks/component-helpers/useConfirmationDropdown.tsx new file mode 100644 index 000000000..6fbc051bc --- /dev/null +++ b/ui/admin/app/hooks/component-helpers/useConfirmationDropdown.tsx @@ -0,0 +1,54 @@ +import { ComponentProps, useCallback, useState } from "react"; + +import { noop } from "~/lib/utils"; + +import { ConfirmationDialog } from "~/components/composed/ConfirmationDialog"; + +type ConfirmationDialogProps = ComponentProps; +type Handler = (e: React.MouseEvent) => void; +type AsyncHandler = ( + e: React.MouseEvent +) => Promise; + +export function useConfirmationDialog( + baseProps?: Partial +) { + const [props, setProps] = useState({ + title: "Are you sure?", + onConfirm: noop, + open: false, + ...baseProps, + }); + + const updateProps = useCallback( + (props: Partial) => + setProps((prev) => ({ ...prev, ...props })), + [] + ); + + const intercept = useCallback( + (handler: Handler, props?: Partial) => + updateProps({ + onConfirm: handler, + onCancel: () => updateProps({ open: false }), + open: true, + onOpenChange: (open) => updateProps({ open }), + ...props, + }), + [updateProps] + ); + + const interceptAsync = useCallback( + (handler: AsyncHandler, props?: Partial) => + intercept( + async (e) => { + await handler(e); + updateProps({ open: false }); + }, + { closeOnConfirm: false, ...props } + ), + [intercept, updateProps] + ); + + return { dialogProps: props, intercept, interceptAsync }; +}