Skip to content

Commit

Permalink
Ui/feat/edit-oauth-apps (#216)
Browse files Browse the repository at this point in the history
* UI feat: allow users to edit oauth apps

Signed-off-by: Ryan Hopper-Lowe <[email protected]>

* UI enhance: improve delete oauth app workflow

* UI enhance: update oauth app delete button variant

* UI chore: add error messages to oauth app api calls

Signed-off-by: Ryan Hopper-Lowe <[email protected]>

* UI refactor: prepopulate cache with oath data on page load

* UI chore: reduce complexity + improve typesafety of OAuth app spec

* Add header content for OAuth Apps

* UI enhance: improve coloring on delete button confirmation

* UI fix: prevent global swr revalidation on focus

---------

Signed-off-by: Ryan Hopper-Lowe <[email protected]>
  • Loading branch information
ryanhopperlowe authored Oct 19, 2024
1 parent d999952 commit 75b7fc4
Show file tree
Hide file tree
Showing 12 changed files with 333 additions and 164 deletions.
49 changes: 49 additions & 0 deletions ui/admin/app/components/composed/ConfirmationDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { ComponentProps, ReactNode } from "react";

import { Button } from "~/components/ui/button";
import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogTitle,
DialogTrigger,
} from "~/components/ui/dialog";

export function ConfirmationDialog({
children,
title,
description,
onConfirm,
onCancel,
confirmProps,
...dialogProps
}: ComponentProps<typeof Dialog> & {
children?: ReactNode;
title: ReactNode;
description?: ReactNode;
onConfirm: () => void;
onCancel?: () => void;
confirmProps?: Omit<Partial<ComponentProps<typeof Button>>, "onClick">;
}) {
return (
<Dialog {...dialogProps}>
{children && <DialogTrigger asChild>{children}</DialogTrigger>}

<DialogContent>
<DialogTitle>{title}</DialogTitle>
<DialogDescription>{description}</DialogDescription>
<DialogFooter>
<DialogClose onClick={onCancel} asChild>
<Button variant="secondary">Cancel</Button>
</DialogClose>

<Button {...confirmProps} onClick={onConfirm}>
{confirmProps?.children ?? "Confirm"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
8 changes: 2 additions & 6 deletions ui/admin/app/components/header/HeaderNav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ function getHeaderContent(route: string) {
}

if (new RegExp($path("/agents")).test(route)) {
return <AgentsContent />;
return <>Agents</>;
}

if (new RegExp($path("/threads")).test(route)) {
Expand All @@ -93,14 +93,10 @@ function getHeaderContent(route: string) {
}

if (new RegExp($path("/users")).test(route)) {
return <UsersContent />;
return <>Users</>;
}
}

const UsersContent = () => <>Users</>;

const AgentsContent = () => <>Agents</>;

const AgentEditContent = () => {
const params = useParams();
const { agent: agentId } = $params("/agents/:agent", params);
Expand Down
30 changes: 12 additions & 18 deletions ui/admin/app/components/oauth-apps/CreateOauthApp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { PlusIcon } from "lucide-react";
import { useState } from "react";
import { mutate } from "swr";

import { OAuthAppSpec, OAuthAppType } from "~/lib/model/oauthApps";
import { OauthAppService } from "~/lib/service/api/oauthAppService";

import { Button } from "~/components/ui/button";
Expand All @@ -20,21 +19,17 @@ import {
PopoverContent,
PopoverTrigger,
} from "~/components/ui/popover";
import { useOAuthAppSpec } from "~/hooks/oauthApps/useOAuthAppSpec";
import { useAsync } from "~/hooks/useAsync";
import { useDisclosure } from "~/hooks/useDisclosure";

import { OAuthAppForm } from "./OAuthAppForm";

type CreateOauthAppProps = {
spec: OAuthAppSpec;
};

export function CreateOauthApp({ spec }: CreateOauthAppProps) {
export function CreateOauthApp() {
const selectModal = useDisclosure();
const { data: spec } = useOAuthAppSpec();

const [selectedAppKey, setSelectedAppKey] = useState<OAuthAppType | null>(
null
);
const [selectedAppKey, setSelectedAppKey] = useState<string | null>(null);

const createApp = useAsync(OauthAppService.createOauthApp, {
onSuccess: () => {
Expand All @@ -43,6 +38,8 @@ export function CreateOauthApp({ spec }: CreateOauthAppProps) {
},
});

const selectedSpec = selectedAppKey ? spec.get(selectedAppKey) : null;

return (
<>
<Popover
Expand All @@ -62,15 +59,13 @@ export function CreateOauthApp({ spec }: CreateOauthAppProps) {

<CommandList>
<CommandGroup>
{Object.entries(spec).map(
{Array.from(spec.entries()).map(
([key, { displayName }]) => (
<CommandItem
key={key}
value={displayName}
onSelect={() => {
setSelectedAppKey(
key as OAuthAppType
);
setSelectedAppKey(key);
selectModal.onClose();
}}
>
Expand All @@ -89,20 +84,19 @@ export function CreateOauthApp({ spec }: CreateOauthAppProps) {
onOpenChange={() => setSelectedAppKey(null)}
>
<DialogContent>
{selectedAppKey && spec[selectedAppKey] && (
{selectedAppKey && selectedSpec && (
<>
<DialogTitle>
Create {spec[selectedAppKey].displayName} OAuth
App
Create {selectedSpec.displayName} OAuth App
</DialogTitle>

<DialogDescription>
Create a new OAuth app for{" "}
{spec[selectedAppKey].displayName}
{selectedSpec.displayName}
</DialogDescription>

<OAuthAppForm
appSpec={spec[selectedAppKey]}
appSpec={selectedSpec}
onSubmit={(data) =>
createApp.execute({
type: selectedAppKey,
Expand Down
99 changes: 31 additions & 68 deletions ui/admin/app/components/oauth-apps/DeleteOAuthApp.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { Check, TrashIcon, XIcon } from "lucide-react";
import { TrashIcon } from "lucide-react";
import { mutate } from "swr";

import { OauthAppService } from "~/lib/service/api/oauthAppService";

import { ConfirmationDialog } from "~/components/composed/ConfirmationDialog";
import { LoadingSpinner } from "~/components/ui/LoadingSpinner";
import { Button } from "~/components/ui/button";
import {
Expand All @@ -12,81 +13,43 @@ import {
TooltipTrigger,
} from "~/components/ui/tooltip";
import { useAsync } from "~/hooks/useAsync";
import { useDisclosure } from "~/hooks/useDisclosure";

export function DeleteOAuthApp({ id }: { id: string }) {
const confirmation = useDisclosure();

const deleteOAuthApp = useAsync(OauthAppService.deleteOauthApp, {
onSuccess: () => mutate(OauthAppService.getOauthApps.key()),
});

return (
<div className="flex gap-2">
{confirmation.isOpen ? (
<>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="secondary"
size="icon"
onClick={confirmation.onClose}
>
<XIcon />
</Button>
</TooltipTrigger>

<TooltipContent>Cancel</TooltipContent>
</Tooltip>
</TooltipProvider>

<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="destructive"
size="icon"
disabled={deleteOAuthApp.isLoading}
onClick={() => {
deleteOAuthApp.execute(id);
confirmation.onClose();
}}
>
<Check />
</Button>
</TooltipTrigger>

<TooltipContent>Confirm Delete</TooltipContent>
</Tooltip>
</TooltipProvider>
</>
) : (
<>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="destructive"
size="icon"
disabled={deleteOAuthApp.isLoading}
onClick={confirmation.onOpen}
>
{deleteOAuthApp.isLoading ? (
<LoadingSpinner />
) : (
<TrashIcon />
)}
</Button>
</TooltipTrigger>

<TooltipContent>Delete</TooltipContent>
</Tooltip>
</TooltipProvider>

<Button className="invisible" size="icon" />
</>
)}
<TooltipProvider>
<Tooltip>
<ConfirmationDialog
title={`Delete OAuth App`}
description="Are you sure you want to delete this OAuth app?"
onConfirm={() => deleteOAuthApp.execute(id)}
confirmProps={{
variant: "destructive",
children: "Delete",
}}
>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
disabled={deleteOAuthApp.isLoading}
>
{deleteOAuthApp.isLoading ? (
<LoadingSpinner />
) : (
<TrashIcon />
)}
</Button>
</TooltipTrigger>
</ConfirmationDialog>

<TooltipContent>Delete</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
);
}
68 changes: 68 additions & 0 deletions ui/admin/app/components/oauth-apps/EditOAuthApp.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { SquarePenIcon } from "lucide-react";
import { mutate } from "swr";

import { OAuthApp } from "~/lib/model/oauthApps";
import { OauthAppService } from "~/lib/service/api/oauthAppService";

import { Button } from "~/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogTitle,
DialogTrigger,
} from "~/components/ui/dialog";
import { useOAuthAppSpec } from "~/hooks/oauthApps/useOAuthAppSpec";
import { useAsync } from "~/hooks/useAsync";
import { useDisclosure } from "~/hooks/useDisclosure";

import { OAuthAppForm } from "./OAuthAppForm";

type EditOAuthAppProps = {
oauthApp: OAuthApp;
};

export function EditOAuthApp({ oauthApp }: EditOAuthAppProps) {
const updateApp = useAsync(OauthAppService.updateOauthApp, {
onSuccess: async () => {
await mutate(OauthAppService.getOauthApps.key());
modal.onClose();
},
});

const modal = useDisclosure();

const { data: spec } = useOAuthAppSpec();

const typeSpec = spec.get(oauthApp.type);
if (!typeSpec) return null;

return (
<Dialog open={modal.isOpen} onOpenChange={modal.onOpenChange}>
<DialogTrigger asChild>
<Button variant="ghost" size="icon">
<SquarePenIcon />
</Button>
</DialogTrigger>

<DialogContent>
<DialogTitle>Edit OAuth App ({oauthApp.type})</DialogTitle>

<DialogDescription hidden>
Update the OAuth app settings.
</DialogDescription>

<OAuthAppForm
appSpec={typeSpec}
oauthApp={oauthApp}
onSubmit={(data) =>
updateApp.execute(oauthApp.id, {
type: oauthApp.type,
...data,
})
}
/>
</DialogContent>
</Dialog>
);
}
Loading

0 comments on commit 75b7fc4

Please sign in to comment.