Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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<ConfirmationDialogProps> = ({ title, message, triggerText, trigger, onClose, onConfirm }) => {
return (
<Dialog>
<DialogTrigger asChild>{trigger ?? <Button>{triggerText}</Button>}</DialogTrigger>
<DialogContent className="w-xl max-w-xl sm:max-w-xl">
<DialogHeader>
<DialogTitle className="flex max-w-[400px] flex-row gap-1">{title}</DialogTitle>
<DialogDescription>{message}</DialogDescription>
</DialogHeader>

<DialogFooter>
<DialogClose asChild>
<Button
variant="ghost"
title="Cancel"
onClick={event => {
event.stopPropagation();
onClose();
}}
>
Cancel
</Button>
</DialogClose>

<DialogClose>
<Button
title="Confirm"
onClick={event => {
event.stopPropagation();
onConfirm();
}}
>
Confirm
</Button>
</DialogClose>
</DialogFooter>
</DialogContent>
</Dialog>
);
};
Original file line number Diff line number Diff line change
@@ -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<typeof buttonVariants>["variant"];
import type { ButtonVariant } from "@/lib/types/ui";

export interface ButtonWithCallback {
text: string;
Expand Down
36 changes: 36 additions & 0 deletions client/webui/frontend/src/lib/components/common/ErrorDialog.tsx
Original file line number Diff line number Diff line change
@@ -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<ErrorDialogProps> = ({ title, error, errorDetails, onClose }) => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto for this component, feel free to improve.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i didn't use a dialog trigger and left it open by default because i assume you wouldn't click into an error dialog

return (
<Dialog defaultOpen={true}>
<DialogContent className="w-xl max-w-xl sm:max-w-xl">
<DialogHeader>
<DialogTitle className="flex max-w-[400px] flex-row gap-1">{title}</DialogTitle>
</DialogHeader>

<div className="flex flex-row items-center gap-2">
<CircleX className="h-6 w-6 flex-shrink-0 self-start text-[var(--color-error-wMain)]" />
<div>{error}</div>
</div>
{errorDetails && <div className="text-[var(--color-secondary-text-wMain)]">{errorDetails}</div>}

<DialogFooter>
<DialogClose asChild>
<Button variant="outline" testid="closeButton" type="button" title="Close" onClick={onClose}>
Close
</Button>
</DialogClose>
</DialogFooter>
</DialogContent>
</Dialog>
);
};
19 changes: 19 additions & 0 deletions client/webui/frontend/src/lib/components/common/LoadingBlocker.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Loader2 } from "lucide-react";

interface LoadingBlockerProps {
isLoading: boolean;
message?: string;
}

export const LoadingBlocker: React.FC<LoadingBlockerProps> = ({ isLoading, message }) => {
if (!isLoading) {
return null;
}

return (
<div className="fixed inset-0 z-50 flex flex-col items-center justify-center bg-black/50">
<Loader2 className="size-8 animate-spin text-[var(--color-brand-wMain)]" />
{message && <p className="text-muted-foreground mt-4 text-sm">{message}</p>}
</div>
);
};
5 changes: 4 additions & 1 deletion client/webui/frontend/src/lib/components/common/index.ts
Original file line number Diff line number Diff line change
@@ -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";
4 changes: 4 additions & 0 deletions client/webui/frontend/src/lib/types/ui.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import type { VariantProps } from "class-variance-authority";
import type { buttonVariants } from "@/lib/components/ui/button";

export type ButtonVariant = VariantProps<typeof buttonVariants>["variant"];
45 changes: 45 additions & 0 deletions client/webui/frontend/src/stories/ConfirmationDialog.stories.tsx
Original file line number Diff line number Diff line change
@@ -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 <div style={{ height: "100vh", width: "100vw", display: "flex", justifyContent: "center", alignItems: "center" }}>{storyResult}</div>;
},
],
} satisfies Meta<typeof ConfirmationDialog>;

export default meta;
type Story = StoryObj<typeof meta>;

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: <Button variant="outline"> Custom trigger</Button>,
message: "Are you sure you want to do this action",
onClose: () => alert("Action cancelled"),
onConfirm: () => alert("Action confirmed"),
},
};
42 changes: 42 additions & 0 deletions client/webui/frontend/src/stories/ErrorDialog.stories.tsx
Original file line number Diff line number Diff line change
@@ -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 <div style={{ height: "100vh", width: "100vw", display: "flex", justifyContent: "center", alignItems: "center" }}>{storyResult}</div>;
},
],
} satisfies Meta<typeof ErrorDialog>;

export default meta;
type Story = StoryObj<typeof meta>;

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"),
},
};
Loading