Skip to content

Commit

Permalink
Ui/feat/custom-oauth-app-implementation (#368)
Browse files Browse the repository at this point in the history
* UI chore: rename customApp to appOverride in oauth apps workflow

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

* feat: implement hook to fetch custom oauth apps

* UI feat: implement basic custom OAuth app workflow

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

---------

Signed-off-by: Ryan Hopper-Lowe <[email protected]>
  • Loading branch information
ryanhopperlowe authored Oct 31, 2024
1 parent b511ee2 commit 28bbb1d
Show file tree
Hide file tree
Showing 22 changed files with 506 additions and 108 deletions.
2 changes: 1 addition & 1 deletion ui/admin/app/components/composed/ConfirmationDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export function ConfirmationDialog({
<Button variant="secondary">Cancel</Button>
</DialogClose>

<DialogClose onClick={onConfirm}>
<DialogClose onClick={onConfirm} asChild>
<Button {...confirmProps}>
{confirmProps?.children ?? "Confirm"}
</Button>
Expand Down
27 changes: 12 additions & 15 deletions ui/admin/app/components/oauth-apps/ConfigureOAuthApp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,7 @@ import {
DialogTrigger,
} from "~/components/ui/dialog";
import { ScrollArea } from "~/components/ui/scroll-area";
import {
useOAuthAppInfo,
useOAuthAppList,
} from "~/hooks/oauthApps/useOAuthApps";
import { useOAuthAppInfo } from "~/hooks/oauthApps/useOAuthApps";
import { useAsync } from "~/hooks/useAsync";
import { useDisclosure } from "~/hooks/useDisclosure";

Expand All @@ -29,40 +26,40 @@ import { OAuthAppTypeIcon } from "./OAuthAppTypeIcon";

export function ConfigureOAuthApp({ type }: { type: OAuthProvider }) {
const spec = useOAuthAppInfo(type);
const { customApp } = spec;
const isEdit = !!customApp;
const { appOverride } = spec;
const isEdit = !!appOverride;

const modal = useDisclosure();
const successModal = useDisclosure();

const createApp = useAsync(async (data: OAuthAppParams) => {
await OauthAppService.createOauthApp({
...data,
type,
refName: type,
global: true,
integration: type,
...data,
});

await mutate(useOAuthAppList.key());
await mutate(OauthAppService.getOauthApps.key());

modal.onClose();
successModal.onOpen();
toast.success(`${spec.displayName} OAuth configuration created`);
});

const updateApp = useAsync(async (data: OAuthAppParams) => {
if (!customApp) throw new Error("Custom app not found");
if (!appOverride) throw new Error("Custom app not found");

await OauthAppService.updateOauthApp(customApp.id, {
type: customApp.type,
refName: customApp.refName,
global: customApp.global,
integration: customApp.integration,
await OauthAppService.updateOauthApp(appOverride.id, {
...data,
type: appOverride.type,
refName: appOverride.refName,
global: appOverride.global,
integration: appOverride.integration,
});

await mutate(useOAuthAppList.key());
await mutate(OauthAppService.getOauthApps.key());

modal.onClose();
successModal.onOpen();
Expand Down
7 changes: 2 additions & 5 deletions ui/admin/app/components/oauth-apps/DeleteOAuthApp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,7 @@ import {
TooltipProvider,
TooltipTrigger,
} from "~/components/ui/tooltip";
import {
useOAuthAppInfo,
useOAuthAppList,
} from "~/hooks/oauthApps/useOAuthApps";
import { useOAuthAppInfo } from "~/hooks/oauthApps/useOAuthApps";
import { useAsync } from "~/hooks/useAsync";

export function DeleteOAuthApp({
Expand All @@ -32,7 +29,7 @@ export function DeleteOAuthApp({

const deleteOAuthApp = useAsync(async () => {
await OauthAppService.deleteOauthApp(id);
await mutate(useOAuthAppList.key());
await mutate(OauthAppService.getOauthApps.key());

toast.success(`${spec.displayName} OAuth configuration deleted`);
});
Expand Down
4 changes: 2 additions & 2 deletions ui/admin/app/components/oauth-apps/OAuthAppDetail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,8 @@ export function OAuthAppDetail({

{spec.disableConfiguration ? (
<DisabledContent spec={spec} />
) : spec?.customApp ? (
<Content app={spec.customApp} spec={spec} />
) : spec?.appOverride ? (
<Content app={spec.appOverride} spec={spec} />
) : (
<EmptyContent spec={spec} />
)}
Expand Down
18 changes: 4 additions & 14 deletions ui/admin/app/components/oauth-apps/OAuthAppList.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { TypographyH2, TypographyP } from "~/components/Typography";
import { TypographyH3 } from "~/components/Typography";
import { useOAuthAppList } from "~/hooks/oauthApps/useOAuthApps";

import { OAuthAppTile } from "./OAuthAppTile";
Expand All @@ -7,20 +7,10 @@ export function OAuthAppList() {
const apps = useOAuthAppList();

return (
<div className="space-y-10">
<div>
<TypographyH2 className="mb-4">
Supported OAuth Apps
</TypographyH2>
<div className="space-y-4">
<TypographyH3>Default OAuth Apps</TypographyH3>

<TypographyP className="!mt-0">
These are the currently supported OAuth apps for Otto. These
are here to allow users to access the following services via
tools.
</TypographyP>
</div>

<div className="flex flex-wrap gap-10">
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 2xl:grid-cols-4 gap-4">
{apps.map(({ type }) => (
<OAuthAppTile
key={type}
Expand Down
80 changes: 61 additions & 19 deletions ui/admin/app/components/oauth-apps/OAuthAppTile.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,22 @@
import { OAuthProvider } from "~/lib/model/oauthApps/oauth-helpers";
import { cn } from "~/lib/utils";

import { TypographyH3 } from "~/components/Typography";
import { useTheme } from "~/components/theme";
import { Card } from "~/components/ui/card";
import { Badge } from "~/components/ui/badge";
import { Card, CardContent, CardHeader } from "~/components/ui/card";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "~/components/ui/tooltip";
import { useOAuthAppInfo } from "~/hooks/oauthApps/useOAuthApps";

import { OAuthAppDetail } from "./OAuthAppDetail";

export function OAuthAppTile({
type,
className,
}: {
type: OAuthProvider;
className?: string;
Expand All @@ -24,31 +31,66 @@ export function OAuthAppTile({

const { displayName } = info;

if (info.type == "slack") {
console.log(info);
}

const getSrc = () => {
if (isDark) return info.darkLogo ?? info.logo;
return info.logo;
};

return (
<Card
className={cn(
"self-center relative w-[300px] h-[150px] px-6 flex gap-4 justify-center items-center",
className
)}
className={cn("w-full flex flex-col", {
"border-2 border-primary": info.appOverride,
})}
>
<img
src={getSrc()}
alt={displayName}
className={cn("m-4 aspect-auto", {
"dark:invert": info.invertDark,
})}
/>

<OAuthAppDetail type={type} className="absolute top-2 right-2" />
<CardHeader className="flex flex-row justify-between pb-2">
<div className="flex flex-wrap gap-2 items-center">
<TypographyH3 className="min-w-fit">
{displayName}
</TypographyH3>

{info.appOverride ? (
<TooltipProvider>
<Tooltip>
<TooltipTrigger>
<Badge>Custom</Badge>
</TooltipTrigger>

<TooltipContent>
OAuth for {displayName} is configured by
your organization.
</TooltipContent>
</Tooltip>
</TooltipProvider>
) : (
<TooltipProvider>
<Tooltip>
<TooltipTrigger>
<Badge variant="secondary">Default</Badge>
</TooltipTrigger>

<TooltipContent>
OAuth for {displayName} is handled by
default by the Acorn Gateway
</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
</div>

<OAuthAppDetail type={type} />
</CardHeader>

<CardContent className="flex-grow flex items-center justify-center">
<div className="h-[100px] flex justify-center items-center overflow-clip">
<img
src={getSrc()}
alt={displayName}
className={cn("max-w-full max-h-[100px] aspect-auto", {
"dark:invert": info.invertDark,
})}
/>
</div>
</CardContent>
</Card>
);
}
1 change: 1 addition & 0 deletions ui/admin/app/components/oauth-apps/OAuthAppTypeIcon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const IconMap = {
[OAuthProvider.Google]: FaGoogle,
[OAuthProvider.Microsoft365]: FaMicrosoft,
[OAuthProvider.Notion]: NotionLogoIcon,
[OAuthProvider.Custom]: KeyIcon,
};

export function OAuthAppTypeIcon({
Expand Down
60 changes: 60 additions & 0 deletions ui/admin/app/components/oauth-apps/custom/CreateCustomOAuthApp.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { PlusIcon } from "lucide-react";
import { useState } from "react";
import { mutate } from "swr";

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

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

import { CustomOAuthAppForm } from "./CustomOAuthAppForm";

export function CreateCustomOAuthApp() {
const [isOpen, setIsOpen] = useState(false);

const createApp = useAsync(OauthAppService.createOauthApp, {
onSuccess: () => {
setIsOpen(false);
mutate(OauthAppService.getOauthApps.key());
},
});

return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
<Button variant="outline" className="flex items-center gap-2">
<PlusIcon className="h-4 w-4" /> Create a Custom OAuth App
</Button>
</DialogTrigger>

<DialogContent>
<DialogHeader>
<DialogTitle>Create Custom OAuth App</DialogTitle>
</DialogHeader>

<DialogDescription hidden>
Create Custom OAuth App
</DialogDescription>

<CustomOAuthAppForm
onSubmit={(data) =>
createApp.execute({
type: "custom",
global: true,
refName: data.integration,
...data,
})
}
/>
</DialogContent>
</Dialog>
);
}
Loading

0 comments on commit 28bbb1d

Please sign in to comment.