diff --git a/client/webui/frontend/src/lib/components/common/ConfirmationDialog.tsx b/client/webui/frontend/src/lib/components/common/ConfirmationDialog.tsx new file mode 100644 index 000000000..5ffe8983b --- /dev/null +++ b/client/webui/frontend/src/lib/components/common/ConfirmationDialog.tsx @@ -0,0 +1,61 @@ +import { Button } from "@/lib/components/ui/button"; +import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@/lib/components/ui/dialog"; +import { DialogClose } from "@radix-ui/react-dialog"; + +interface BaseDialogProps { + title: string; + message: string | React.ReactNode; + onConfirm: () => void; + onClose: () => void; +} + +type ConfirmationDialogProps = + | (BaseDialogProps & { + triggerText: string; + trigger?: never; + }) + | (BaseDialogProps & { + trigger: React.ReactNode; + triggerText?: never; + }); + +export const ConfirmationDialog: React.FC = ({ title, message, triggerText, trigger, onClose, onConfirm }) => { + return ( + + {trigger ?? } + + + {title} + {message} + + + + + + + + + + + + + + ); +}; diff --git a/client/webui/frontend/src/lib/components/common/EmptyState.tsx b/client/webui/frontend/src/lib/components/common/EmptyState.tsx index 1d3adc5b6..9b1804144 100644 --- a/client/webui/frontend/src/lib/components/common/EmptyState.tsx +++ b/client/webui/frontend/src/lib/components/common/EmptyState.tsx @@ -1,12 +1,9 @@ -import type { VariantProps } from "class-variance-authority"; import { Button } from "@/lib/components/ui/button"; -import type { buttonVariants } from "@/lib/components/ui/button"; import type { ReactElement } from "react"; import { ErrorIllustration, NotFoundIllustration } from "@/lib/assets"; import { cn } from "@/lib/utils"; import { Spinner } from "../ui/spinner"; - -type ButtonVariant = VariantProps["variant"]; +import type { ButtonVariant } from "@/lib/types/ui"; export interface ButtonWithCallback { text: string; diff --git a/client/webui/frontend/src/lib/components/common/ErrorDialog.tsx b/client/webui/frontend/src/lib/components/common/ErrorDialog.tsx new file mode 100644 index 000000000..d67d062b8 --- /dev/null +++ b/client/webui/frontend/src/lib/components/common/ErrorDialog.tsx @@ -0,0 +1,36 @@ +import { Button } from "@/lib/components/ui/button"; +import { Dialog, DialogClose, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/lib/components/ui/dialog"; +import { CircleX } from "lucide-react"; + +interface ErrorDialogProps { + title: string; + error: string; + errorDetails?: string; + onClose: () => void; +} + +export const ErrorDialog: React.FC = ({ title, error, errorDetails, onClose }) => { + return ( + + + + {title} + + +
+ +
{error}
+
+ {errorDetails &&
{errorDetails}
} + + + + + + +
+
+ ); +}; diff --git a/client/webui/frontend/src/lib/components/common/LoadingBlocker.tsx b/client/webui/frontend/src/lib/components/common/LoadingBlocker.tsx new file mode 100644 index 000000000..5680f319b --- /dev/null +++ b/client/webui/frontend/src/lib/components/common/LoadingBlocker.tsx @@ -0,0 +1,19 @@ +import { Loader2 } from "lucide-react"; + +interface LoadingBlockerProps { + isLoading: boolean; + message?: string; +} + +export const LoadingBlocker: React.FC = ({ isLoading, message }) => { + if (!isLoading) { + return null; + } + + return ( +
+ + {message &&

{message}

} +
+ ); +}; diff --git a/client/webui/frontend/src/lib/components/common/index.ts b/client/webui/frontend/src/lib/components/common/index.ts index bcd02385c..faeee4010 100644 --- a/client/webui/frontend/src/lib/components/common/index.ts +++ b/client/webui/frontend/src/lib/components/common/index.ts @@ -1,4 +1,7 @@ +export { ConfirmationDialog } from "./ConfirmationDialog"; +export { EmptyState } from "./EmptyState"; +export { ErrorDialog } from "./ErrorDialog"; +export { LoadingBlocker } from "./LoadingBlocker"; export { MarkdownHTMLConverter } from "./MarkdownHTMLConverter"; export { MessageBanner } from "./MessageBanner"; -export { EmptyState } from "./EmptyState"; export * from "./messageColourVariants"; diff --git a/client/webui/frontend/src/lib/types/ui.ts b/client/webui/frontend/src/lib/types/ui.ts new file mode 100644 index 000000000..4b13d29aa --- /dev/null +++ b/client/webui/frontend/src/lib/types/ui.ts @@ -0,0 +1,4 @@ +import type { VariantProps } from "class-variance-authority"; +import type { buttonVariants } from "@/lib/components/ui/button"; + +export type ButtonVariant = VariantProps["variant"]; diff --git a/client/webui/frontend/src/stories/ConfirmationDialog.stories.tsx b/client/webui/frontend/src/stories/ConfirmationDialog.stories.tsx new file mode 100644 index 000000000..93199839d --- /dev/null +++ b/client/webui/frontend/src/stories/ConfirmationDialog.stories.tsx @@ -0,0 +1,45 @@ +import { Button, ConfirmationDialog } from "@/lib"; +import type { Meta, StoryContext, StoryFn, StoryObj } from "@storybook/react-vite"; + +const meta = { + title: "Common/ConfirmationDialog", + component: ConfirmationDialog, + parameters: { + layout: "fullscreen", + docs: { + description: { + component: "The button component", + }, + }, + }, + decorators: [ + (Story: StoryFn, context: StoryContext) => { + const storyResult = Story(context.args, context); + + return
{storyResult}
; + }, + ], +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + title: "Confirm", + triggerText: "Open Dialog", + message: "Are you sure you want to do this action", + onClose: () => alert("Action cancelled"), + onConfirm: () => alert("Action confirmed"), + }, +}; + +export const CustomTrigger: Story = { + args: { + title: "Confirm", + trigger: , + message: "Are you sure you want to do this action", + onClose: () => alert("Action cancelled"), + onConfirm: () => alert("Action confirmed"), + }, +}; diff --git a/client/webui/frontend/src/stories/ErrorDialog.stories.tsx b/client/webui/frontend/src/stories/ErrorDialog.stories.tsx new file mode 100644 index 000000000..45c93f585 --- /dev/null +++ b/client/webui/frontend/src/stories/ErrorDialog.stories.tsx @@ -0,0 +1,42 @@ +import { ErrorDialog } from "@/lib"; +import type { Meta, StoryContext, StoryFn, StoryObj } from "@storybook/react-vite"; + +const meta = { + title: "Common/ErrorDialog", + component: ErrorDialog, + parameters: { + layout: "fullscreen", + docs: { + description: { + component: "The button component", + }, + }, + }, + decorators: [ + (Story: StoryFn, context: StoryContext) => { + const storyResult = Story(context.args, context); + + return
{storyResult}
; + }, + ], +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + title: "Error", + error: "Something went wrong", + onClose: () => alert("Action cancelled"), + }, +}; + +export const WithErrorDetails = { + args: { + title: "Error", + error: "Something went wrong", + errorDetails: "This action is forbidden. Ensure you have the right authorization.", + onClose: () => alert("Action cancelled"), + }, +};