Skip to content

Commit

Permalink
Ui/feat/overhaul-oauth-page (#242)
Browse files Browse the repository at this point in the history
* UI feat: overhaul oauth app creation/listing

- display oauth apps in a tile view instead of a table
- implement generic oauth app form with specific steps for each oauth provider
- remove the differentiation (for the user) between custom oauth apps and gateway oauth apps

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

* enhance: update loading spinner behavior for oauthApps

* chore: remove unused service method (supported oauth app types)
- also provides better naming conventions for the oauth app details

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

* feat: use github logo for tile view

* chore: remove commented code
- also changes copy to clipboard button to secondary

---------

Signed-off-by: Ryan Hopper-Lowe <[email protected]>
  • Loading branch information
ryanhopperlowe authored Oct 21, 2024
1 parent 7bfbe6a commit 2dd7a5f
Show file tree
Hide file tree
Showing 23 changed files with 682 additions and 370 deletions.
6 changes: 1 addition & 5 deletions ui/admin/app/components/Typography.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,11 +60,7 @@ export function TypographyH4({ children, className }: TypographyProps) {
}

export function TypographyP({ children, className }: TypographyProps) {
return (
<p className={cn(`leading-7 [&:not(:first-child)]:mt-6`, className)}>
{children}
</p>
);
return <p className={cn(`leading-7`, className)}>{children}</p>;
}

export function TypographyBlockquote({ children, className }: TypographyProps) {
Expand Down
8 changes: 5 additions & 3 deletions ui/admin/app/components/composed/ConfirmationDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,11 @@ export function ConfirmationDialog({
<Button variant="secondary">Cancel</Button>
</DialogClose>

<Button {...confirmProps} onClick={onConfirm}>
{confirmProps?.children ?? "Confirm"}
</Button>
<DialogClose onClick={onConfirm}>
<Button {...confirmProps}>
{confirmProps?.children ?? "Confirm"}
</Button>
</DialogClose>
</DialogFooter>
</DialogContent>
</Dialog>
Expand Down
79 changes: 79 additions & 0 deletions ui/admin/app/components/composed/CopyText.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { ClipboardCheckIcon, ClipboardIcon } from "lucide-react";
import { useEffect, useState } from "react";
import { toast } from "sonner";

import { cn } from "~/lib/utils";

import { Button } from "~/components/ui/button";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "~/components/ui/tooltip";

export function CopyText({
text,
displayText = text,
className,
}: {
text: string;
displayText?: string;
className?: string;
}) {
const [isCopied, setIsCopied] = useState(false);

useEffect(() => {
if (!isCopied) return;

const timeout = setTimeout(() => setIsCopied(false), 10000);

return () => clearTimeout(timeout);
}, [isCopied]);

return (
<div className={cn("flex items-center gap-2 w-full", className)}>
<TooltipProvider>
<Tooltip>
<TooltipTrigger
type="button"
onClick={() => handleCopy(text)}
className="decoration-dotted underline-offset-4 underline text-ellipsis overflow-hidden text-nowrap"
>
{displayText}
</TooltipTrigger>

<TooltipContent>
<b>Copy: </b>
{text}
</TooltipContent>
</Tooltip>
</TooltipProvider>

<Button
size="icon"
onClick={() => handleCopy(text)}
className="aspect-square"
variant="secondary"
type="button"
>
{isCopied ? (
<ClipboardCheckIcon className="text-success" />
) : (
<ClipboardIcon />
)}
</Button>
</div>
);

async function handleCopy(text: string) {
try {
await navigator.clipboard.writeText(text);
toast.success("Copied to clipboard");
setIsCopied(true);
} catch (error) {
console.error("Failed to copy text: ", error);
toast.error("Failed to copy text");
}
}
}
145 changes: 56 additions & 89 deletions ui/admin/app/components/oauth-apps/CreateOauthApp.tsx
Original file line number Diff line number Diff line change
@@ -1,113 +1,80 @@
import { DialogDescription } from "@radix-ui/react-dialog";
import { PlusIcon } from "lucide-react";
import { useState } from "react";
import { SettingsIcon } from "lucide-react";
import { toast } from "sonner";
import { mutate } from "swr";

import { OAuthAppParams } from "~/lib/model/oauthApps";
import { OAuthProvider } from "~/lib/model/oauthApps/oauth-helpers";
import { OauthAppService } from "~/lib/service/api/oauthAppService";

import { Button } from "~/components/ui/button";
import {
Command,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "~/components/ui/command";
import { Dialog, DialogContent, DialogTitle } from "~/components/ui/dialog";
Dialog,
DialogContent,
DialogTitle,
DialogTrigger,
} from "~/components/ui/dialog";
import { ScrollArea } from "~/components/ui/scroll-area";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "~/components/ui/popover";
import { useOAuthAppSpec } from "~/hooks/oauthApps/useOAuthAppSpec";
useOAuthAppInfo,
useOAuthAppList,
} from "~/hooks/oauthApps/useOAuthApps";
import { useAsync } from "~/hooks/useAsync";
import { useDisclosure } from "~/hooks/useDisclosure";

import { OAuthAppForm } from "./OAuthAppForm";
import { OAuthAppTypeIcon } from "./OAuthAppTypeIcon";

export function CreateOauthApp() {
const selectModal = useDisclosure();
const { data: spec } = useOAuthAppSpec();
export function CreateOauthApp({ type }: { type: OAuthProvider }) {
const spec = useOAuthAppInfo(type);
const modal = useDisclosure();

const [selectedAppKey, setSelectedAppKey] = useState<string | null>(null);
const createApp = useAsync(async (data: OAuthAppParams) => {
await OauthAppService.createOauthApp({
type,
refName: type,
...data,
});

const createApp = useAsync(OauthAppService.createOauthApp, {
onSuccess: () => {
mutate(OauthAppService.getOauthApps.key());
setSelectedAppKey(null);
},
});
await mutate(useOAuthAppList.key());

const selectedSpec = selectedAppKey ? spec.get(selectedAppKey) : null;
modal.onClose();
toast.success(`${spec.displayName} OAuth app created`);
});

return (
<>
<Popover
open={selectModal.isOpen}
onOpenChange={selectModal.onOpenChange}
>
<PopoverTrigger asChild>
<Button variant="outline">
<PlusIcon className="w-4 h-4 mr-2" />
New OAuth App
</Button>
</PopoverTrigger>

<PopoverContent className="p-0" side="bottom" align="end">
<Command>
<CommandInput placeholder="Search OAuth App..." />

<CommandList>
<CommandGroup>
{Array.from(spec.entries()).map(
([key, { displayName }]) => (
<CommandItem
key={key}
value={displayName}
onSelect={() => {
setSelectedAppKey(key);
selectModal.onClose();
}}
>
{displayName}
</CommandItem>
)
)}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
<Dialog open={modal.isOpen} onOpenChange={modal.onOpenChange}>
<DialogTrigger asChild>
<Button className="w-full">
<SettingsIcon className="w-4 h-4 mr-2" />
Configure {spec.displayName} OAuth App
</Button>
</DialogTrigger>

<Dialog
open={!!selectedAppKey}
onOpenChange={() => setSelectedAppKey(null)}
<DialogContent
classNames={{
overlay: "opacity-0",
}}
aria-describedby="create-oauth-app"
className="px-0"
>
<DialogContent>
{selectedAppKey && selectedSpec && (
<>
<DialogTitle>
Create {selectedSpec.displayName} OAuth App
</DialogTitle>
<DialogTitle className="flex items-center gap-2 px-4">
<OAuthAppTypeIcon type={type} />
Configure {spec.displayName} OAuth App
</DialogTitle>

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

<OAuthAppForm
appSpec={selectedSpec}
onSubmit={(data) =>
createApp.execute({
type: selectedAppKey,
...data,
})
}
/>
</>
)}
</DialogContent>
</Dialog>
</>
<ScrollArea className="max-h-[80vh] px-4">
<OAuthAppForm
type={type}
onSubmit={createApp.execute}
isLoading={createApp.isLoading}
/>
</ScrollArea>
</DialogContent>
</Dialog>
);
}
34 changes: 25 additions & 9 deletions ui/admin/app/components/oauth-apps/DeleteOAuthApp.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { TrashIcon } from "lucide-react";
import { toast } from "sonner";
import { mutate } from "swr";

import { OauthAppService } from "~/lib/service/api/oauthAppService";
Expand All @@ -12,37 +13,48 @@ import {
TooltipProvider,
TooltipTrigger,
} from "~/components/ui/tooltip";
import { useOAuthAppList } from "~/hooks/oauthApps/useOAuthApps";
import { useAsync } from "~/hooks/useAsync";

export function DeleteOAuthApp({ id }: { id: string }) {
const deleteOAuthApp = useAsync(OauthAppService.deleteOauthApp, {
onSuccess: () => mutate(OauthAppService.getOauthApps.key()),
export function DeleteOAuthApp({
id,
disableTooltip,
}: {
id: string;
disableTooltip?: boolean;
}) {
const deleteOAuthApp = useAsync(async () => {
await OauthAppService.deleteOauthApp(id);
await mutate(useOAuthAppList.key());

toast.success("OAuth app deleted");
});

return (
<div className="flex gap-2">
<TooltipProvider>
<Tooltip>
<Tooltip open={getIsOpen()}>
<ConfirmationDialog
title={`Delete OAuth App`}
description="Are you sure you want to delete this OAuth app?"
onConfirm={() => deleteOAuthApp.execute(id)}
onConfirm={deleteOAuthApp.execute}
confirmProps={{
variant: "destructive",
children: "Delete",
}}
>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
variant="destructive"
className="w-full"
disabled={deleteOAuthApp.isLoading}
>
{deleteOAuthApp.isLoading ? (
<LoadingSpinner />
<LoadingSpinner className="w-4 h-4 mr-2" />
) : (
<TrashIcon />
<TrashIcon className="w-4 h-4 mr-2" />
)}
Delete OAuth App
</Button>
</TooltipTrigger>
</ConfirmationDialog>
Expand All @@ -52,4 +64,8 @@ export function DeleteOAuthApp({ id }: { id: string }) {
</TooltipProvider>
</div>
);

function getIsOpen() {
if (disableTooltip) return false;
}
}
Loading

0 comments on commit 2dd7a5f

Please sign in to comment.