diff --git a/ui/admin/app/components/composed/ConfirmationDialog.tsx b/ui/admin/app/components/composed/ConfirmationDialog.tsx index 2e96f125..97dd3692 100644 --- a/ui/admin/app/components/composed/ConfirmationDialog.tsx +++ b/ui/admin/app/components/composed/ConfirmationDialog.tsx @@ -39,9 +39,11 @@ export function ConfirmationDialog({ - + + + diff --git a/ui/admin/app/components/oauth-apps/CreateOauthApp.tsx b/ui/admin/app/components/oauth-apps/CreateOauthApp.tsx index 88dedaee..8b06e9cc 100644 --- a/ui/admin/app/components/oauth-apps/CreateOauthApp.tsx +++ b/ui/admin/app/components/oauth-apps/CreateOauthApp.tsx @@ -1,7 +1,9 @@ import { DialogDescription } from "@radix-ui/react-dialog"; 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"; @@ -12,22 +14,36 @@ import { DialogTitle, DialogTrigger, } from "~/components/ui/dialog"; -import { useOAuthAppInfo } from "~/hooks/oauthApps"; +import { ScrollArea } from "~/components/ui/scroll-area"; +import { + useOAuthAppInfo, + useOAuthAppList, +} from "~/hooks/oauthApps/useOAuthApps"; import { useAsync } from "~/hooks/useAsync"; +import { useDisclosure } from "~/hooks/useDisclosure"; -import { ScrollArea } from "../ui/scroll-area"; import { OAuthAppForm } from "./OAuthAppForm"; import { OAuthAppTypeIcon } from "./OAuthAppTypeIcon"; export function CreateOauthApp({ type }: { type: OAuthProvider }) { const spec = useOAuthAppInfo(type); + const modal = useDisclosure(); + + const createApp = useAsync(async (data: OAuthAppParams) => { + await OauthAppService.createOauthApp({ + type, + refName: type, + ...data, + }); + + await mutate(useOAuthAppList.key()); - const createApp = useAsync(OauthAppService.createOauthApp, { - onSuccess: () => mutate(OauthAppService.getOauthApps.key()), + modal.onClose(); + toast.success(`${spec.displayName} OAuth app created`); }); return ( - + - )} + diff --git a/ui/admin/app/components/oauth-apps/EditOAuthApp.tsx b/ui/admin/app/components/oauth-apps/EditOAuthApp.tsx index 878acc19..fbbb239d 100644 --- a/ui/admin/app/components/oauth-apps/EditOAuthApp.tsx +++ b/ui/admin/app/components/oauth-apps/EditOAuthApp.tsx @@ -1,6 +1,8 @@ -import { SquarePenIcon } from "lucide-react"; +import { GearIcon } from "@radix-ui/react-icons"; +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"; @@ -13,12 +15,9 @@ import { DialogTrigger, } from "~/components/ui/dialog"; import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from "~/components/ui/tooltip"; -import { useOAuthAppInfo } from "~/hooks/oauthApps"; + useOAuthAppInfo, + useOAuthAppList, +} from "~/hooks/oauthApps/useOAuthApps"; import { useAsync } from "~/hooks/useAsync"; import { useDisclosure } from "~/hooks/useDisclosure"; @@ -29,54 +28,48 @@ export function EditOAuthApp({ type }: { type: OAuthProvider }) { const spec = useOAuthAppInfo(type); const modal = useDisclosure(); - const updateApp = useAsync(OauthAppService.updateOauthApp, { - onSuccess: async () => { - await mutate(OauthAppService.getOauthApps.key()); - modal.onClose(); - }, - }); - const { customApp } = spec; + const updateApp = useAsync(async (data: OAuthAppParams) => { + if (!customApp) return; + + await OauthAppService.updateOauthApp(customApp.id, { + type: customApp.type, + refName: customApp.refName, + ...data, + }); + await mutate(useOAuthAppList.key()); + modal.onClose(); + toast.success(`${spec.displayName} OAuth app updated`); + }); + if (!customApp) return null; return ( - - - - - - - - - - - - Edit{" "} - {spec.displayName} OAuth Configuration - + + + + - + + + Edit {spec.displayName}{" "} + OAuth Configuration + - - updateApp.execute(customApp.id, { - type: customApp.type, - refName: customApp.refName, - ...data, - }) - } - /> - - + - Edit - - + + + ); } diff --git a/ui/admin/app/components/oauth-apps/OAuthAppDetail.tsx b/ui/admin/app/components/oauth-apps/OAuthAppDetail.tsx index 8104653e..e5b851a3 100644 --- a/ui/admin/app/components/oauth-apps/OAuthAppDetail.tsx +++ b/ui/admin/app/components/oauth-apps/OAuthAppDetail.tsx @@ -1,7 +1,4 @@ -import { QuestionMarkCircledIcon } from "@radix-ui/react-icons"; -import { CheckCircle2Icon, ClipboardIcon, SettingsIcon } from "lucide-react"; -import { ReactNode, useEffect, useState } from "react"; -import { toast } from "sonner"; +import { SettingsIcon } from "lucide-react"; import { OAuthApp } from "~/lib/model/oauthApps"; import { @@ -10,7 +7,7 @@ import { } from "~/lib/model/oauthApps/oauth-helpers"; import { cn } from "~/lib/utils"; -import { TypographyP, TypographySmall } from "~/components/Typography"; +import { TypographyP } from "~/components/Typography"; import { Button } from "~/components/ui/button"; import { Dialog, @@ -20,16 +17,11 @@ import { DialogTitle, DialogTrigger, } from "~/components/ui/dialog"; -import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from "~/components/ui/tooltip"; -import { useOAuthAppInfo } from "~/hooks/oauthApps"; +import { useOAuthAppInfo } from "~/hooks/oauthApps/useOAuthApps"; import { CreateOauthApp } from "./CreateOauthApp"; import { DeleteOAuthApp } from "./DeleteOAuthApp"; +import { EditOAuthApp } from "./EditOAuthApp"; import { OAuthAppTypeIcon } from "./OAuthAppTypeIcon"; export function OAuthAppDetail({ @@ -58,7 +50,7 @@ export function OAuthAppDetail({ - + {spec?.displayName} @@ -66,7 +58,7 @@ export function OAuthAppDetail({ {spec?.customApp ? ( - + ) : ( )} @@ -93,152 +85,32 @@ function EmptyContent({ spec }: { spec: OAuthSingleAppSpec }) { ); } -function Content({ - oauthApp, -}: { - spec: OAuthSingleAppSpec; - oauthApp: OAuthApp; -}) { - const [copied, setCopied] = useState(null); - - useEffect(() => { - const timeout = setTimeout(() => setCopied(null), 6000); - return () => clearTimeout(timeout); - }, [copied]); - +function Content({ app, spec }: { app: OAuthApp; spec: OAuthSingleAppSpec }) { return (
-
- - - {oauthApp.refName ?? "None"} - - - - {oauthApp.refName && ( - - - - - {oauthApp.refNameAssigned ? "Yes" : "No"} - - - - {oauthApp.refNameAssigned - ? "The reference name is currently active" - : "The reference name is not assigned because another OAuth App is using it"} - - - - - )} -
- - {Object.entries(oauthApp.links).map(([key, value]) => { - // "camelCase" to "Display Name" - const displayName = - key - .split("URL")[0] - .replace(/([A-Z])/g, " $1") - .replace(/^./, (str) => str.toUpperCase()) + " URL"; - - return ( - -
- - - copyToClipboard(value)} - className="flex-auto decoration-dotted underline-offset-4 underline text-ellipsis overflow-hidden text-nowrap" - > - {value} - - - {value} - - - - -
-
- ); - })} - - - - -
- ); + + You have a custom configuration for {spec.displayName} OAuth. + - async function copyToClipboard(value: string) { - try { - await navigator.clipboard.writeText(value); + + When {spec.displayName} OAuth is used, Otto will use your custom + OAuth app. + - toast.success("Copied to clipboard"); - setCopied(value); - } catch (_) { - toast.error("Failed to copy"); - } - } -} +
+ + Client ID + + {app.clientID} -function Item({ - label, - children, - className, - info, -}: { - label: string; - children: ReactNode; - className?: string; - info?: string; -}) { - return ( -
-
- - {label} - - - {info && ( - - - - - - - - {info} - - - - )} + + Client Secret + + ****************
- {children} + +
); } diff --git a/ui/admin/app/components/oauth-apps/OAuthAppForm.tsx b/ui/admin/app/components/oauth-apps/OAuthAppForm.tsx index 35dc796f..f0c28eac 100644 --- a/ui/admin/app/components/oauth-apps/OAuthAppForm.tsx +++ b/ui/admin/app/components/oauth-apps/OAuthAppForm.tsx @@ -12,20 +12,21 @@ import { import { OauthAppService } from "~/lib/service/api/oauthAppService"; import { cn } from "~/lib/utils"; +import { CopyText } from "~/components/composed/CopyText"; import { ControlledInput } from "~/components/form/controlledInputs"; +import { CustomMarkdownComponents } from "~/components/react-markdown"; +import { LoadingSpinner } from "~/components/ui/LoadingSpinner"; import { Button } from "~/components/ui/button"; import { Form } from "~/components/ui/form"; -import { useOAuthAppInfo } from "~/hooks/oauthApps"; - -import { CopyText } from "../composed/CopyText"; -import { CustomMarkdownComponents } from "../react-markdown"; +import { useOAuthAppInfo } from "~/hooks/oauthApps/useOAuthApps"; type OAuthAppFormProps = { type: OAuthProvider; onSubmit: (data: OAuthAppParams) => void; + isLoading?: boolean; }; -export function OAuthAppForm({ type, onSubmit }: OAuthAppFormProps) { +export function OAuthAppForm({ type, onSubmit, isLoading }: OAuthAppFormProps) { const spec = useOAuthAppInfo(type); useEffect(() => { OauthAppService.getSupportedOauthAppTypes(); @@ -36,9 +37,8 @@ export function OAuthAppForm({ type, onSubmit }: OAuthAppFormProps) { const fields = useMemo(() => { return Object.entries(spec.schema.shape).map(([key]) => ({ key: key as keyof OAuthAppParams, - label: spec.labels[key], })); - }, [spec.schema, spec.labels]); + }, [spec.schema]); const defaultValues = useMemo(() => { const app = spec.customApp; @@ -82,14 +82,17 @@ export function OAuthAppForm({ type, onSubmit }: OAuthAppFormProps) {
{spec.steps.map(renderStep)} - +
); function renderStep(step: OAuthFormStep) { switch (step.type) { - case "instruction": + case "markdown": return ( ); case "input": { - const isRequired = !spec.schema.shape[step.input].isOptional(); - - const label = isRequired - ? `${spec.labels[step.input]} *` - : spec.labels[step.input]; - return ( ({ + ...OauthAppService.getOauthApps.key(), + modifier: "combinedList", +}); + export function useOAuthAppList(config?: { revalidate?: boolean }) { const { revalidate = true } = config ?? {}; - const key = { - ...OauthAppService.getOauthApps.key(), - modifier: "combinedList", - }; - const { data: apps } = useSWR( - key, + key(), async () => combinedOAuthAppInfo(await OauthAppService.getOauthApps()), { fallbackData: [], revalidateOnMount: revalidate } ); return apps; } +useOAuthAppList.key = key; export function useOAuthAppInfo(type: OAuthProvider): CombinedOAuthAppInfo { const list = useOAuthAppList({ revalidate: false }); diff --git a/ui/admin/app/lib/model/oauthApps/github.ts b/ui/admin/app/lib/model/oauthApps/github.ts index abd3340d..2a66c7e3 100644 --- a/ui/admin/app/lib/model/oauthApps/github.ts +++ b/ui/admin/app/lib/model/oauthApps/github.ts @@ -7,36 +7,38 @@ import { } from "./oauth-helpers"; const schema = z.object({ - authURL: z.string(), clientID: z.string(), clientSecret: z.string(), - tokenURL: z.string().optional(), }); -const labels = { - authURL: "Authorization URL", - clientID: "Client ID", - clientSecret: "Client Secret", - tokenURL: "Token URL", -} satisfies Record; - const steps: OAuthFormStep[] = [ { - type: "instruction", + type: "markdown", + text: "### Step 1: Create a new GitHub OAuth App\n", + }, + { + type: "markdown", text: - "#### Step 1: Create a new GitHub OAuth App\n" + - "1. Navigate to [GitHub's Developer Settings](https://github.com/settings/developers) and select 'New OAuth App'.\n" + + "#### If you haven't already, create a new GitHub OAuth App\n" + + "1. Navigate to [GitHub's Developer Settings](https://github.com/settings/developers) and select `New OAuth App`.\n" + "2. The form will prompt you for an `Authorization callback Url` Make sure to use the link below: \n\n", }, + { + type: "markdown", + text: + "#### If you already have a github OAuth app created\n" + + "1. you can edit it by going to [Github's Developer Settings](https://github.com/settings/developers), and selecting `Edit` on your OAuth App\n" + + "2. Near the bottom is the `Authorization callback URL` field. Make sure it matches the link below: \n\n", + }, { type: "copy", text: getOAuthLinks("github").redirectURL, }, { - type: "instruction", + type: "markdown", text: - "#### Step 2: Register OAuth App in Otto\n" + - "Once you've created your OAuth App in GitHub, click 'Register application' and copy the client ID and client secret into this form", + "### Step 2: Register OAuth App in Otto\n" + + "Once you've created your OAuth App in GitHub, copy the client ID and client secret into this form", }, { type: "input", input: "clientID", label: "Client ID" }, { type: "input", input: "clientSecret", label: "Client Secret" }, @@ -47,6 +49,5 @@ export const GitHubOAuthApp = { refName: "github", type: "github", displayName: "GitHub", - labels, steps, } satisfies OAuthSingleAppSpec; diff --git a/ui/admin/app/lib/model/oauthApps/oauth-helpers.ts b/ui/admin/app/lib/model/oauthApps/oauth-helpers.ts index 515f8df4..82f17678 100644 --- a/ui/admin/app/lib/model/oauthApps/oauth-helpers.ts +++ b/ui/admin/app/lib/model/oauthApps/oauth-helpers.ts @@ -13,13 +13,12 @@ export const OAuthProvider = { export type OAuthProvider = (typeof OAuthProvider)[keyof typeof OAuthProvider]; export type OAuthFormStep> = - | { type: "instruction"; text: string; copy?: string } + | { type: "markdown"; text: string; copy?: string } | { type: "input"; input: keyof T; label: string } | { type: "copy"; text: string }; export type OAuthSingleAppSpec = { schema: ZodObject>; - labels: Record; displayName: string; refName: string; type: OAuthProvider;