From b2de7dc0b9c194f751c5d8e407f71370d8a79439 Mon Sep 17 00:00:00 2001 From: Lewis Carhart Date: Wed, 19 Mar 2025 13:21:38 +0000 Subject: [PATCH 1/3] feat: implement loading state and fallback UI for organization settings - Added a new `Loading` component to display a fallback UI while organization settings are loading. - Created `SettingsFallback` component with placeholders for organization name, website, and delete confirmation. - Wrapped organization settings in `Suspense` to handle loading state effectively. --- .../(app)/(dashboard)/settings/loading.tsx | 9 +++ .../(app)/(dashboard)/settings/page.tsx | 14 ++-- .../components/loaders/settings-fallback.tsx | 65 +++++++++++++++++++ 3 files changed, 82 insertions(+), 6 deletions(-) create mode 100644 apps/app/src/app/[locale]/(app)/(dashboard)/settings/loading.tsx create mode 100644 apps/app/src/components/loaders/settings-fallback.tsx diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/settings/loading.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/settings/loading.tsx new file mode 100644 index 0000000000..0c52d4c31c --- /dev/null +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/settings/loading.tsx @@ -0,0 +1,9 @@ +import { SettingsFallback } from "@/components/loaders/settings-fallback"; + +export default function Loading() { + return ( +
+ +
+ ); +} diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/settings/page.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/settings/page.tsx index a618b73fe0..12f922038b 100644 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/settings/page.tsx +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/settings/page.tsx @@ -7,7 +7,7 @@ import { db } from "@bubba/db"; import type { Metadata } from "next"; import { setStaticParamsLocale } from "next-international/server"; import { redirect } from "next/navigation"; -import { cache } from "react"; +import { cache, Suspense } from "react"; export default async function OrganizationSettings({ params, @@ -26,11 +26,13 @@ export default async function OrganizationSettings({ const organization = await organizationDetails(session.user.organizationId); return ( -
- - - -
+ +
+ + + +
+
); } diff --git a/apps/app/src/components/loaders/settings-fallback.tsx b/apps/app/src/components/loaders/settings-fallback.tsx new file mode 100644 index 0000000000..f6f967465a --- /dev/null +++ b/apps/app/src/components/loaders/settings-fallback.tsx @@ -0,0 +1,65 @@ +import { Button } from "@bubba/ui/button"; +import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@bubba/ui/card"; +import { Input } from "@bubba/ui/input"; +import { getI18n } from "@/locales/server"; +import { AlertDialog, AlertDialogTrigger } from "@bubba/ui/alert-dialog"; + +export async function SettingsFallback() { + const t = await getI18n(); + + return ( +
+ + + {t("settings.general.org_name")} + + {t("settings.general.org_name_description")} + + + + + + +
{t("settings.general.org_name_tip")}
+ +
+
+ + + + {t("settings.general.org_website")} + + {t("settings.general.org_website_description")} + + + + + + +
{t("settings.general.org_website_tip")}
+ +
+
+ + + + {t("settings.general.org_delete")} + + {t("settings.general.org_delete_description")} + + + + + + +
+ + + + + + + +
+ ); +} \ No newline at end of file From 593bf309f5866ce42afe6f40fb93dfb9cece975e Mon Sep 17 00:00:00 2001 From: Lewis Carhart Date: Wed, 19 Mar 2025 15:07:32 +0000 Subject: [PATCH 2/3] refactor: enhance settings actions and loading states - Updated `createApiKeyAction` and `revokeApiKeyAction` to include path revalidation after successful operations. - Improved error handling for API key actions with specific messages for unique and foreign key constraints. - Replaced the `SettingsFallback` component with a more structured loading UI using `Card` components for organization settings. - Introduced a new `Loading` component for the API keys page, featuring a spinner and descriptive cards. - Removed the deprecated `SettingsFallback` component to streamline the codebase. --- .../organization/create-api-key-action.ts | 204 ++++----- .../organization/revoke-api-key-action.ts | 82 ++-- .../(dashboard)/settings/api-keys/loading.tsx | 34 ++ .../(dashboard)/settings/api-keys/page.tsx | 75 +++- .../(app)/(dashboard)/settings/loading.tsx | 61 ++- .../(dashboard)/settings/members/loading.tsx | 56 +++ .../components/loaders/settings-fallback.tsx | 65 --- apps/app/src/components/logo-spinner.tsx | 86 ++++ .../src/components/tables/api-keys/index.tsx | 406 ++++++++---------- 9 files changed, 623 insertions(+), 446 deletions(-) create mode 100644 apps/app/src/app/[locale]/(app)/(dashboard)/settings/api-keys/loading.tsx create mode 100644 apps/app/src/app/[locale]/(app)/(dashboard)/settings/members/loading.tsx delete mode 100644 apps/app/src/components/loaders/settings-fallback.tsx create mode 100644 apps/app/src/components/logo-spinner.tsx diff --git a/apps/app/src/actions/organization/create-api-key-action.ts b/apps/app/src/actions/organization/create-api-key-action.ts index a2f24d66a7..106cb1642b 100644 --- a/apps/app/src/actions/organization/create-api-key-action.ts +++ b/apps/app/src/actions/organization/create-api-key-action.ts @@ -4,114 +4,116 @@ import { authActionClient } from "@/actions/safe-action"; import { apiKeySchema } from "@/actions/schema"; import { generateApiKey, generateSalt, hashApiKey } from "@/lib/api-key"; import { db } from "@bubba/db"; +import { revalidatePath } from "next/cache"; export const createApiKeyAction = authActionClient - .schema(apiKeySchema) - .metadata({ - name: "createApiKey", - track: { - event: "createApiKey", - channel: "server", - }, - }) - .action(async ({ parsedInput, ctx }) => { - try { - const { name, expiresAt } = parsedInput; - console.log(`Creating API key "${name}" with expiration: ${expiresAt}`); + .schema(apiKeySchema) + .metadata({ + name: "createApiKey", + track: { + event: "createApiKey", + channel: "server", + }, + }) + .action(async ({ parsedInput, ctx }) => { + try { + const { name, expiresAt } = parsedInput; + console.log(`Creating API key "${name}" with expiration: ${expiresAt}`); - // Generate a new API key and salt - const apiKey = generateApiKey(); - const salt = generateSalt(); - const hashedKey = hashApiKey(apiKey, salt); - console.log( - `Generated new API key for organization: ${ctx.user.organizationId}` - ); + // Generate a new API key and salt + const apiKey = generateApiKey(); + const salt = generateSalt(); + const hashedKey = hashApiKey(apiKey, salt); + console.log( + `Generated new API key for organization: ${ctx.user.organizationId}`, + ); - // Parse the expiration date - let expirationDate: Date | null = null; - if (expiresAt && expiresAt !== "never") { - const now = new Date(); - switch (expiresAt) { - case "30days": - expirationDate = new Date(now.setDate(now.getDate() + 30)); - break; - case "90days": - expirationDate = new Date(now.setDate(now.getDate() + 90)); - break; - case "1year": - expirationDate = new Date(now.setFullYear(now.getFullYear() + 1)); - break; - } - console.log(`Set expiration date to: ${expirationDate?.toISOString()}`); - } else { - console.log("No expiration date set for API key"); - } + // Parse the expiration date + let expirationDate: Date | null = null; + if (expiresAt && expiresAt !== "never") { + const now = new Date(); + switch (expiresAt) { + case "30days": + expirationDate = new Date(now.setDate(now.getDate() + 30)); + break; + case "90days": + expirationDate = new Date(now.setDate(now.getDate() + 90)); + break; + case "1year": + expirationDate = new Date(now.setFullYear(now.getFullYear() + 1)); + break; + } + console.log(`Set expiration date to: ${expirationDate?.toISOString()}`); + } else { + console.log("No expiration date set for API key"); + } - // Create the API key in the database - const apiKeyRecord = await db.organizationApiKey.create({ - data: { - name, - key: hashedKey, - salt, // Store the salt with the hashed key - expiresAt: expirationDate, - organizationId: ctx.user.organizationId!, - }, - select: { - id: true, - name: true, - createdAt: true, - expiresAt: true, - }, - }); - console.log(`Successfully created API key with ID: ${apiKeyRecord.id}`); + // Create the API key in the database + const apiKeyRecord = await db.organizationApiKey.create({ + data: { + name, + key: hashedKey, + salt, // Store the salt with the hashed key + expiresAt: expirationDate, + organizationId: ctx.user.organizationId!, + }, + select: { + id: true, + name: true, + createdAt: true, + expiresAt: true, + }, + }); + console.log(`Successfully created API key with ID: ${apiKeyRecord.id}`); - // Return the API key (this is the only time the plain text key will be available) - return { - success: true, - data: { - ...apiKeyRecord, - key: apiKey, - createdAt: apiKeyRecord.createdAt.toISOString(), - expiresAt: apiKeyRecord.expiresAt - ? apiKeyRecord.expiresAt.toISOString() - : null, - }, - }; - } catch (error) { - console.error("Error creating API key:", error); + revalidatePath("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/settings/api-keys"); - // Provide more specific error messages based on error type - if (error instanceof Error) { - console.error(`Error details: ${error.message}`); + return { + success: true, + data: { + ...apiKeyRecord, + key: apiKey, + createdAt: apiKeyRecord.createdAt.toISOString(), + expiresAt: apiKeyRecord.expiresAt + ? apiKeyRecord.expiresAt.toISOString() + : null, + }, + }; + } catch (error) { + console.error("Error creating API key:", error); - if (error.message.includes("Unique constraint")) { - return { - success: false, - error: { - code: "DUPLICATE_NAME", - message: "An API key with this name already exists", - }, - }; - } + // Provide more specific error messages based on error type + if (error instanceof Error) { + console.error(`Error details: ${error.message}`); - if (error.message.includes("Foreign key constraint")) { - return { - success: false, - error: { - code: "INVALID_ORGANIZATION", - message: - "The organization does not exist or you don't have access", - }, - }; - } - } + if (error.message.includes("Unique constraint")) { + return { + success: false, + error: { + code: "DUPLICATE_NAME", + message: "An API key with this name already exists", + }, + }; + } - return { - success: false, - error: { - code: "INTERNAL_ERROR", - message: "An unexpected error occurred while creating the API key", - }, - }; - } - }); + if (error.message.includes("Foreign key constraint")) { + return { + success: false, + error: { + code: "INVALID_ORGANIZATION", + message: + "The organization does not exist or you don't have access", + }, + }; + } + } + + return { + success: false, + error: { + code: "INTERNAL_ERROR", + message: "An unexpected error occurred while creating the API key", + }, + }; + } + }); diff --git a/apps/app/src/actions/organization/revoke-api-key-action.ts b/apps/app/src/actions/organization/revoke-api-key-action.ts index 77663176fa..43534697e7 100644 --- a/apps/app/src/actions/organization/revoke-api-key-action.ts +++ b/apps/app/src/actions/organization/revoke-api-key-action.ts @@ -3,51 +3,53 @@ import { authActionClient } from "@/actions/safe-action"; import { z } from "zod"; import { db } from "@bubba/db"; +import { revalidatePath } from "next/cache"; const revokeApiKeySchema = z.object({ - id: z.string().min(1), + id: z.string().min(1), }); export const revokeApiKeyAction = authActionClient - .schema(revokeApiKeySchema) - .metadata({ - name: "revokeApiKey", - track: { - event: "revokeApiKey", - channel: "server", - }, - }) - .action(async ({ parsedInput, ctx }) => { - try { - const { id } = parsedInput; + .schema(revokeApiKeySchema) + .metadata({ + name: "revokeApiKey", + track: { + event: "revokeApiKey", + channel: "server", + }, + }) + .action(async ({ parsedInput, ctx }) => { + try { + const { id } = parsedInput; - // Update the API key to set isActive to false - const result = await db.organizationApiKey.updateMany({ - where: { - id, - organizationId: ctx.user.organizationId!, - }, - data: { - isActive: false, - }, - }); + const result = await db.organizationApiKey.updateMany({ + where: { + id, + organizationId: ctx.user.organizationId!, + }, + data: { + isActive: false, + }, + }); - if (result.count === 0) { - return { - success: false, - error: "API key not found or not authorized to revoke", - }; - } + if (result.count === 0) { + return { + success: false, + error: "API key not found or not authorized to revoke", + }; + } - return { - success: true, - message: "API key revoked successfully", - }; - } catch (error) { - console.error("Error revoking API key:", error); - return { - success: false, - error: "An error occurred while revoking the API key", - }; - } - }); + revalidatePath("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/settings/api-keys"); + + return { + success: true, + message: "API key revoked successfully", + }; + } catch (error) { + console.error("Error revoking API key:", error); + return { + success: false, + error: "An error occurred while revoking the API key", + }; + } + }); diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/settings/api-keys/loading.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/settings/api-keys/loading.tsx new file mode 100644 index 0000000000..02e23ad072 --- /dev/null +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/settings/api-keys/loading.tsx @@ -0,0 +1,34 @@ +import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@bubba/ui/card"; +import { Button } from "@bubba/ui/button"; +import { getI18n } from "@/locales/server"; +import { Plus } from "lucide-react"; +import { LogoSpinner } from "@/components/logo-spinner"; + +export default async function Loading() { + const t = await getI18n(); + + return ( +
+ + +
+ {t("settings.api_keys.list_title")} + + {t("settings.api_keys.list_description")} + +
+ +
+ + + + + {t("settings.api_keys.security_note")} + +
+
+ ); +} \ No newline at end of file diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/settings/api-keys/page.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/settings/api-keys/page.tsx index ed94f37055..5d5c5d8074 100644 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/settings/api-keys/page.tsx +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/settings/api-keys/page.tsx @@ -4,39 +4,70 @@ import { getI18n } from "@/locales/server"; import type { Metadata } from "next"; import { setStaticParamsLocale } from "next-international/server"; import { redirect } from "next/navigation"; +import { db } from "@bubba/db"; +import { cache } from "react"; export default async function ApiKeysPage({ - params, + params, }: { - params: Promise<{ locale: string }>; + params: Promise<{ locale: string }>; }) { - const { locale } = await params; - setStaticParamsLocale(locale); - const t = await getI18n(); + const { locale } = await params; + setStaticParamsLocale(locale); + const t = await getI18n(); - const session = await auth(); + const session = await auth(); - if (!session?.user.organizationId) { - return redirect("/"); - } + if (!session?.user.organizationId) { + return redirect("/"); + } - return ( -
- -
- ); + const apiKeys = await getApiKeys(session.user.organizationId); + + return ( +
+ +
+ ); } export async function generateMetadata({ - params, + params, }: { - params: Promise<{ locale: string }>; + params: Promise<{ locale: string }>; }): Promise { - const { locale } = await params; - setStaticParamsLocale(locale); - const t = await getI18n(); + const { locale } = await params; + setStaticParamsLocale(locale); + const t = await getI18n(); - return { - title: t("settings.api_keys.title"), - }; + return { + title: t("settings.api_keys.title"), + }; } + +const getApiKeys = cache(async (organizationId: string) => { + const apiKeys = await db.organizationApiKey.findMany({ + where: { + organizationId, + isActive: true, + }, + select: { + id: true, + name: true, + createdAt: true, + expiresAt: true, + lastUsedAt: true, + isActive: true, + }, + orderBy: { + createdAt: "desc", + }, + }); + + return apiKeys.map((key) => ({ + ...key, + createdAt: key.createdAt.toISOString(), + expiresAt: key.expiresAt ? key.expiresAt.toISOString() : null, + lastUsedAt: key.lastUsedAt ? key.lastUsedAt.toISOString() : null, + })); +}); diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/settings/loading.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/settings/loading.tsx index 0c52d4c31c..72b8d2c18b 100644 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/settings/loading.tsx +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/settings/loading.tsx @@ -1,9 +1,64 @@ -import { SettingsFallback } from "@/components/loaders/settings-fallback"; +import { Button } from "@bubba/ui/button"; +import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@bubba/ui/card"; +import { Input } from "@bubba/ui/input"; +import { getI18n } from "@/locales/server"; +import { AlertDialog, AlertDialogTrigger } from "@bubba/ui/alert-dialog"; + +export default async function Loading() { + const t = await getI18n(); -export default function Loading() { return (
- + + + {t("settings.general.org_name")} + + {t("settings.general.org_name_description")} + + + + + + +
{t("settings.general.org_name_tip")}
+ +
+
+ + + + {t("settings.general.org_website")} + + {t("settings.general.org_website_description")} + + + + + + +
{t("settings.general.org_website_tip")}
+ +
+
+ + + + {t("settings.general.org_delete")} + + {t("settings.general.org_delete_description")} + + + +
+ + + + + + +
); } diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/settings/members/loading.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/settings/members/loading.tsx new file mode 100644 index 0000000000..b9353c994c --- /dev/null +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/settings/members/loading.tsx @@ -0,0 +1,56 @@ +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@bubba/ui/tabs"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@bubba/ui/card"; +import { Avatar, AvatarFallback } from "@bubba/ui/avatar"; +import { Badge } from "@bubba/ui/badge"; +import { getI18n } from "@/locales/server"; +import { LogoSpinner } from "@/components/logo-spinner"; + +export default async function Loading() { + const t = await getI18n(); + + return ( +
+ + + + {t("settings.team.tabs.members")} + + + {t("settings.team.tabs.invite")} + + + + + + + {t("settings.team.members.title")} + + + + + + + + + +
+ + + {t("settings.team.invite.title")} + + {t("settings.team.invite.description")} + + + + + +
+
+
+ +
+ + +
+ ); +} \ No newline at end of file diff --git a/apps/app/src/components/loaders/settings-fallback.tsx b/apps/app/src/components/loaders/settings-fallback.tsx deleted file mode 100644 index f6f967465a..0000000000 --- a/apps/app/src/components/loaders/settings-fallback.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import { Button } from "@bubba/ui/button"; -import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@bubba/ui/card"; -import { Input } from "@bubba/ui/input"; -import { getI18n } from "@/locales/server"; -import { AlertDialog, AlertDialogTrigger } from "@bubba/ui/alert-dialog"; - -export async function SettingsFallback() { - const t = await getI18n(); - - return ( -
- - - {t("settings.general.org_name")} - - {t("settings.general.org_name_description")} - - - - - - -
{t("settings.general.org_name_tip")}
- -
-
- - - - {t("settings.general.org_website")} - - {t("settings.general.org_website_description")} - - - - - - -
{t("settings.general.org_website_tip")}
- -
-
- - - - {t("settings.general.org_delete")} - - {t("settings.general.org_delete_description")} - - - - - - -
- - - - - - - -
- ); -} \ No newline at end of file diff --git a/apps/app/src/components/logo-spinner.tsx b/apps/app/src/components/logo-spinner.tsx new file mode 100644 index 0000000000..1f53af0c2c --- /dev/null +++ b/apps/app/src/components/logo-spinner.tsx @@ -0,0 +1,86 @@ +"use client" + +import { motion, type SVGMotionProps } from "framer-motion" +import type React from "react" +import { useEffect, useRef, useState } from "react" +import { cn } from "@bubba/ui/cn" + +export interface LogoSpinnerProps extends React.SVGProps { + size?: number + className?: string + raceColor?: string +} + +export const LogoSpinner = ({ + size = 40, + className, + raceColor = "#00DC73", + ...props +}: LogoSpinnerProps) => { + const pathRef = useRef(null) + const [pathLength, setPathLength] = useState(0) + + useEffect(() => { + if (pathRef.current) { + const length = pathRef.current.getTotalLength() + setPathLength(length) + } + }, []) + + const cShapePath = + "M22.033 7.327a0.802 0.802 0 0 1 0.937 0l5.126 3.68a0.802 0.802 0 0 1 0 1.306l-2.431 1.744a0.52 0.52 0 0 1 -0.598 -0.001l-2.097 -1.505a0.802 0.802 0 0 0 -0.937 0l-6.979 5.01a0.802 0.802 0 0 0 0 1.306l1.955 1.402 2.63 1.891 2.395 1.718a0.802 0.802 0 0 0 0.938 0l6.979 -5.015a0.802 0.802 0 0 0 0 -1.305l-1.654 -1.19a0.263 0.263 0 0 1 0 -0.427l2.873 -2.06a0.802 0.802 0 0 1 0.937 0l5.123 3.679a0.802 0.802 0 0 1 0 1.306l-2.433 1.747 -11.825 8.491a0.802 0.802 0 0 1 -0.937 0l-6.034 -4.333 -2.63 -1.886 -3.163 -2.27 -2.431 -1.747a0.802 0.802 0 0 1 0 -1.306z" + + return ( +
+ )} + > + {/* Original logo path - uses currentColor from className */} + + + {/* Racing line - uses the raceColor prop */} + + +
+ ) +} + diff --git a/apps/app/src/components/tables/api-keys/index.tsx b/apps/app/src/components/tables/api-keys/index.tsx index 0be3d26d7f..7bd43e77cd 100644 --- a/apps/app/src/components/tables/api-keys/index.tsx +++ b/apps/app/src/components/tables/api-keys/index.tsx @@ -1,32 +1,32 @@ "use client"; import { revokeApiKeyAction } from "@/actions/organization/revoke-api-key-action"; -import { useApiKeys } from "@/hooks/use-api-keys"; +import type { ApiKey } from "@/hooks/use-api-keys"; import { useI18n } from "@/locales/client"; import { Button } from "@bubba/ui/button"; import { - Card, - CardContent, - CardDescription, - CardFooter, - CardHeader, - CardTitle, + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, } from "@bubba/ui/card"; import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, } from "@bubba/ui/dialog"; import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, } from "@bubba/ui/table"; import { Loader2, Plus, Trash2 } from "lucide-react"; import { useAction } from "next-safe-action/hooks"; @@ -34,208 +34,184 @@ import { useState } from "react"; import { toast } from "sonner"; import { CreateApiKeyDialog } from "./create-api-key-dialog"; -export function ApiKeysTable() { - const t = useI18n(); - const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false); - const [keyToRevoke, setKeyToRevoke] = useState(null); - const [isRevokeDialogOpen, setIsRevokeDialogOpen] = useState(false); +export function ApiKeysTable({ apiKeys }: { apiKeys: ApiKey[] }) { + const t = useI18n(); + const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false); + const [keyToRevoke, setKeyToRevoke] = useState(null); + const [isRevokeDialogOpen, setIsRevokeDialogOpen] = useState(false); - // Use the custom hook for API keys data fetching - const { apiKeys, isLoading, error, refresh: refreshApiKeys } = useApiKeys(); + const { execute: revokeApiKey, status: isRevoking } = useAction( + revokeApiKeyAction, + { + onSuccess: () => { + toast.success(t("settings.api_keys.revoked_success")); + setIsRevokeDialogOpen(false); + setKeyToRevoke(null); + }, + onError: () => { + toast.error(t("settings.api_keys.revoked_error")); + setIsRevokeDialogOpen(false); + setKeyToRevoke(null); + }, + }, + ); - // Use the useAction hook for revoking API keys - const { execute: revokeApiKey, status: isRevoking } = useAction( - revokeApiKeyAction, - { - onSuccess: () => { - toast.success(t("settings.api_keys.revoked_success")); - setIsRevokeDialogOpen(false); - setKeyToRevoke(null); - // Refresh the API keys data after successful revocation - refreshApiKeys(); - }, - onError: () => { - toast.error(t("settings.api_keys.revoked_error")); - setIsRevokeDialogOpen(false); - setKeyToRevoke(null); - }, - }, - ); + const handleRevokeClick = (id: string) => { + setKeyToRevoke(id); + setIsRevokeDialogOpen(true); + }; - const handleRevokeClick = (id: string) => { - setKeyToRevoke(id); - setIsRevokeDialogOpen(true); - }; + const handleConfirmRevoke = () => { + if (keyToRevoke) { + revokeApiKey({ id: keyToRevoke }); + } + }; - const handleConfirmRevoke = () => { - if (keyToRevoke) { - revokeApiKey({ id: keyToRevoke }); - } - }; + const handleCancelRevoke = () => { + setIsRevokeDialogOpen(false); + setKeyToRevoke(null); + }; - const handleCancelRevoke = () => { - setIsRevokeDialogOpen(false); - setKeyToRevoke(null); - }; + const formatDate = (dateString: string | null) => { + if (!dateString) return "-"; + return new Date(dateString).toLocaleDateString(); + }; - const formatDate = (dateString: string | null) => { - if (!dateString) return "-"; - return new Date(dateString).toLocaleDateString(); - }; + return ( + <> + + +
+ {t("settings.api_keys.list_title")} + + {t("settings.api_keys.list_description")} + +
+ +
+ + {apiKeys.length === 0 ? ( +
+ {t("settings.api_keys.no_keys")} +
+ ) : ( +
+ + + + {t("settings.api_keys.name")} + + {t("settings.api_keys.created")} + + + {t("settings.api_keys.expires")} + + + {t("settings.api_keys.last_used")} + + + {t("settings.api_keys.actions")} + + + + + {apiKeys.map((apiKey) => ( + + +
+ {apiKey.name} +
+ {t("settings.api_keys.created")}:{" "} + {formatDate(apiKey.createdAt)} +
+
+ {t("settings.api_keys.expires")}:{" "} + {apiKey.expiresAt + ? formatDate(apiKey.expiresAt) + : t("settings.api_keys.never")} +
+
+
+ + {formatDate(apiKey.createdAt)} + + + {apiKey.expiresAt + ? formatDate(apiKey.expiresAt) + : t("settings.api_keys.never")} + + + {apiKey.lastUsedAt + ? formatDate(apiKey.lastUsedAt) + : t("settings.api_keys.never_used")} + + + + +
+ ))} +
+
+
+ )} +
+ + {t("settings.api_keys.security_note")} + - if (error) { - return ( - - - {t("settings.api_keys.list_title")} - - -
{error}
-
-
- ); - } + +
- return ( - <> - - -
- {t("settings.api_keys.list_title")} - - {t("settings.api_keys.list_description")} - -
- -
- - {isLoading ? ( -
- -
- ) : apiKeys.length === 0 ? ( -
- {t("settings.api_keys.no_keys")} -
- ) : ( -
- - - - {t("settings.api_keys.name")} - - {t("settings.api_keys.created")} - - - {t("settings.api_keys.expires")} - - - {t("settings.api_keys.last_used")} - - - {t("settings.api_keys.actions")} - - - - - {apiKeys.map((apiKey) => ( - - -
- {apiKey.name} -
- {t("settings.api_keys.created")}:{" "} - {formatDate(apiKey.createdAt)} -
-
- {t("settings.api_keys.expires")}:{" "} - {apiKey.expiresAt - ? formatDate(apiKey.expiresAt) - : t("settings.api_keys.never")} -
-
-
- - {formatDate(apiKey.createdAt)} - - - {apiKey.expiresAt - ? formatDate(apiKey.expiresAt) - : t("settings.api_keys.never")} - - - {apiKey.lastUsedAt - ? formatDate(apiKey.lastUsedAt) - : t("settings.api_keys.never_used")} - - - - -
- ))} -
-
-
- )} -
- - {t("settings.api_keys.security_note")} - - - -
- - - - - {t("settings.api_keys.revoke_title")} - - {t("settings.api_keys.revoke_confirm")} - - - - - - - - - - ); + + + + {t("settings.api_keys.revoke_title")} + + {t("settings.api_keys.revoke_confirm")} + + + + + + + + + + ); } From 5c13cda7da07d372bd8f0b572423d903d5275072 Mon Sep 17 00:00:00 2001 From: Lewis Carhart Date: Wed, 19 Mar 2025 15:16:51 +0000 Subject: [PATCH 3/3] refactor: update RealtimeStatus component in create-organization-form - Replaced Loader2 with LogoSpinner for improved loading indication. - Removed completed status section to streamline the UI. - Cleaned up unnecessary code and ensured consistent formatting. --- .../forms/create-organization-form.tsx | 26 +++---------------- 1 file changed, 4 insertions(+), 22 deletions(-) diff --git a/apps/app/src/components/forms/create-organization-form.tsx b/apps/app/src/components/forms/create-organization-form.tsx index 6dabef7ab0..fead4419e9 100644 --- a/apps/app/src/components/forms/create-organization-form.tsx +++ b/apps/app/src/components/forms/create-organization-form.tsx @@ -28,6 +28,7 @@ import { Checkbox } from "@bubba/ui/checkbox"; import { cn } from "@bubba/ui/cn"; import { useRealtimeRun } from "@trigger.dev/react-hooks"; import { useRouter } from "next/navigation"; +import { LogoSpinner } from "../logo-spinner"; function RealtimeStatus({ runId, publicAccessToken }: { runId: string; publicAccessToken: string }) { const t = useI18n(); @@ -53,7 +54,7 @@ function RealtimeStatus({ runId, publicAccessToken }: { runId: string; publicAcc
{run?.status !== "FAILED" && run?.status !== "COMPLETED" && (
- +

{t("onboarding.trigger.title")}

@@ -62,20 +63,6 @@ function RealtimeStatus({ runId, publicAccessToken }: { runId: string; publicAcc

)} - - {run?.status === "COMPLETED" && ( -
-

- {t("onboarding.trigger.completed")} -

-
- -
-
- )}
); } @@ -115,11 +102,6 @@ function OnboardingClient({ frameworks }: { frameworks: Framework[] }) { return (
-
- - - -
@@ -145,8 +127,8 @@ function OnboardingClient({ frameworks }: { frameworks: Framework[] }) {
-