mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
refactor(site): add <Drawer /> and migrate build logs drawer (#28009)
> 🤖 This PR was written by Coder Agents on behalf of Jake Howell. Follow-up to #27798. That PR introduced a shared `Drawer` primitive by adding [`vaul`](https://github.com/emilkowalski/vaul) as a dependency. As raised in review, `vaul` is currently unmaintained, and this drawer is the only place in the UI using that concept. This PR takes the [suggested](https://github.com/coder/coder/pull/27798#pullrequestreview-4901095865) route: a dead-simple, self-owned drawer that covers this one case well, built directly on Radix UI's `Dialog` (already a dependency via `radix-ui`) instead of `vaul`. It keeps the same shadcn-style API (`Drawer`, `DrawerTrigger`, `DrawerContent`, `DrawerHeader`, `DrawerFooter`, `DrawerTitle`, `DrawerDescription`, `DrawerClose`) so usage stays familiar, and migrates `CreateTemplatePage`'s `BuildLogsDrawer` off MUI onto it. ### What changed - Add `site/src/components/Drawer/Drawer.tsx`: a reusable drawer/sheet built on `radix-ui` `Dialog`. Supports a `direction` prop (`top`/`bottom`/`left`/`right`, default `right`) with slide animations via `tailwindcss-animate`. No new dependencies. - Migrate `BuildLogsDrawer` from `@mui/material/Drawer` to the new component. Desktop panel stays at 800px via `min(800px, 100%)` so it stays within the viewport on mobile. - Storybook coverage with `play()` functions for both the generic `Drawer` (open/close, direction) and `BuildLogsDrawer` (close via the X button and via Escape assert `onClose`), addressing the earlier P1 review note about covering the controlled close path. ### Why Radix instead of vaul - `radix-ui` `Dialog` is already a dependency and provides accessibility, focus management, and open/close state. - No dependency on an unmaintained package for a single-use concept. - `vaul`'s drag-to-dismiss gesture is not needed for this build-logs use case.
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import { expect, screen, userEvent, waitFor, within } from "storybook/test";
|
||||
import { Button } from "#/components/Button/Button";
|
||||
import {
|
||||
Drawer,
|
||||
DrawerClose,
|
||||
DrawerContent,
|
||||
DrawerDescription,
|
||||
DrawerFooter,
|
||||
DrawerHeader,
|
||||
DrawerTitle,
|
||||
DrawerTrigger,
|
||||
} from "./Drawer";
|
||||
|
||||
const meta: Meta<typeof Drawer> = {
|
||||
title: "components/Drawer",
|
||||
component: Drawer,
|
||||
args: {
|
||||
children: (
|
||||
<>
|
||||
<DrawerTrigger asChild>
|
||||
<Button>Open Drawer</Button>
|
||||
</DrawerTrigger>
|
||||
<DrawerContent>
|
||||
<DrawerHeader>
|
||||
<DrawerTitle>Example Drawer Title</DrawerTitle>
|
||||
<DrawerDescription>Drawer description text</DrawerDescription>
|
||||
</DrawerHeader>
|
||||
<DrawerFooter>
|
||||
<DrawerClose asChild>
|
||||
<Button variant="outline">Cancel</Button>
|
||||
</DrawerClose>
|
||||
<Button>Submit</Button>
|
||||
</DrawerFooter>
|
||||
</DrawerContent>
|
||||
</>
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof Drawer>;
|
||||
|
||||
export const Closed: Story = {};
|
||||
|
||||
export const Open: Story = {
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
await userEvent.click(canvas.getByRole("button", { name: "Open Drawer" }));
|
||||
// The drawer renders into a portal on `document.body`, so query the screen.
|
||||
await waitFor(() =>
|
||||
expect(screen.getByText("Example Drawer Title")).toBeInTheDocument(),
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const OpenLeft: Story = {
|
||||
args: {
|
||||
direction: "left",
|
||||
children: (
|
||||
<>
|
||||
<DrawerTrigger asChild>
|
||||
<Button>Open Left Drawer</Button>
|
||||
</DrawerTrigger>
|
||||
<DrawerContent>
|
||||
<DrawerHeader>
|
||||
<DrawerTitle>Left-side drawer</DrawerTitle>
|
||||
<DrawerDescription>
|
||||
Drawers can slide in from any edge via the direction prop.
|
||||
</DrawerDescription>
|
||||
</DrawerHeader>
|
||||
<DrawerFooter>
|
||||
<DrawerClose asChild>
|
||||
<Button variant="outline">Close</Button>
|
||||
</DrawerClose>
|
||||
</DrawerFooter>
|
||||
</DrawerContent>
|
||||
</>
|
||||
),
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
await userEvent.click(
|
||||
canvas.getByRole("button", { name: "Open Left Drawer" }),
|
||||
);
|
||||
await waitFor(() =>
|
||||
expect(screen.getByText("Left-side drawer")).toBeInTheDocument(),
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const CloseWithButton: Story = {
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
await userEvent.click(canvas.getByRole("button", { name: "Open Drawer" }));
|
||||
await waitFor(() =>
|
||||
expect(screen.getByText("Example Drawer Title")).toBeInTheDocument(),
|
||||
);
|
||||
await userEvent.click(screen.getByRole("button", { name: "Cancel" }));
|
||||
await waitFor(() =>
|
||||
expect(
|
||||
screen.queryByText("Example Drawer Title"),
|
||||
).not.toBeInTheDocument(),
|
||||
);
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,147 @@
|
||||
import { cva } from "class-variance-authority";
|
||||
import { Dialog as DialogPrimitive } from "radix-ui";
|
||||
import { createContext, useContext } from "react";
|
||||
import { cn } from "#/utils/cn";
|
||||
|
||||
type DrawerDirection = "top" | "bottom" | "left" | "right";
|
||||
|
||||
const DrawerDirectionContext = createContext<DrawerDirection>("right");
|
||||
|
||||
type DrawerProps = React.ComponentPropsWithRef<typeof DialogPrimitive.Root> & {
|
||||
/** The edge of the screen the drawer slides in from. Defaults to "right". */
|
||||
direction?: DrawerDirection;
|
||||
};
|
||||
|
||||
export const Drawer: React.FC<DrawerProps> = ({
|
||||
direction = "right",
|
||||
...props
|
||||
}) => {
|
||||
return (
|
||||
<DrawerDirectionContext.Provider value={direction}>
|
||||
<DialogPrimitive.Root {...props} />
|
||||
</DrawerDirectionContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const DrawerTrigger = DialogPrimitive.Trigger;
|
||||
|
||||
export const DrawerClose = DialogPrimitive.Close;
|
||||
|
||||
const DrawerPortal = DialogPrimitive.Portal;
|
||||
|
||||
const DrawerOverlay: React.FC<
|
||||
React.ComponentPropsWithRef<typeof DialogPrimitive.Overlay>
|
||||
> = ({ className, ...props }) => {
|
||||
return (
|
||||
<DialogPrimitive.Overlay
|
||||
className={cn(
|
||||
"fixed inset-0 z-50 bg-overlay",
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out",
|
||||
"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||
"data-[state=open]:duration-300 data-[state=closed]:duration-100",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const drawerContentVariants = cva(
|
||||
cn(
|
||||
"fixed z-50 flex h-auto flex-col bg-surface-tertiary outline-none will-change-transform",
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out",
|
||||
"data-[state=open]:duration-500 data-[state=closed]:duration-300",
|
||||
),
|
||||
{
|
||||
variants: {
|
||||
direction: {
|
||||
top: cn(
|
||||
"inset-x-0 top-0 max-h-[80vh] w-full border-b border-border",
|
||||
"data-[state=open]:slide-in-from-top data-[state=closed]:slide-out-to-top",
|
||||
),
|
||||
bottom: cn(
|
||||
"inset-x-0 bottom-0 max-h-[80vh] w-full border-t border-border",
|
||||
"data-[state=open]:slide-in-from-bottom data-[state=closed]:slide-out-to-bottom",
|
||||
),
|
||||
left: cn(
|
||||
"inset-y-0 left-0 h-full w-3/4 border-r border-border sm:max-w-sm",
|
||||
"data-[state=open]:slide-in-from-left data-[state=closed]:slide-out-to-left",
|
||||
),
|
||||
right: cn(
|
||||
"inset-y-0 right-0 h-full w-3/4 border-l border-border sm:max-w-sm",
|
||||
"data-[state=open]:slide-in-from-right data-[state=closed]:slide-out-to-right",
|
||||
),
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
direction: "right",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
export const DrawerContent: React.FC<
|
||||
React.ComponentPropsWithRef<typeof DialogPrimitive.Content>
|
||||
> = ({ className, children, ...props }) => {
|
||||
const direction = useContext(DrawerDirectionContext);
|
||||
|
||||
return (
|
||||
<DrawerPortal>
|
||||
<DrawerOverlay />
|
||||
<DialogPrimitive.Content
|
||||
className={cn(drawerContentVariants({ direction }), className)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</DialogPrimitive.Content>
|
||||
</DrawerPortal>
|
||||
);
|
||||
};
|
||||
|
||||
export const DrawerHeader: React.FC<React.ComponentPropsWithRef<"div">> = ({
|
||||
className,
|
||||
...props
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
className={cn("flex flex-col gap-0.5 p-4 md:gap-1.5", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const DrawerFooter: React.FC<React.ComponentPropsWithRef<"div">> = ({
|
||||
className,
|
||||
...props
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const DrawerTitle: React.FC<
|
||||
React.ComponentPropsWithRef<typeof DialogPrimitive.Title>
|
||||
> = ({ className, ...props }) => {
|
||||
return (
|
||||
<DialogPrimitive.Title
|
||||
className={cn(
|
||||
"text-lg font-semibold leading-none tracking-tight text-content-primary",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const DrawerDescription: React.FC<
|
||||
React.ComponentPropsWithRef<typeof DialogPrimitive.Description>
|
||||
> = ({ className, ...props }) => {
|
||||
return (
|
||||
<DialogPrimitive.Description
|
||||
className={cn("text-sm text-content-secondary", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -1,5 +1,8 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import { useRef, useState } from "react";
|
||||
import { expect, fn, screen, userEvent, waitFor, within } from "storybook/test";
|
||||
import { JobError } from "#/api/queries/templates";
|
||||
import { Button } from "#/components/Button/Button";
|
||||
import {
|
||||
MockProvisionerJob,
|
||||
MockTemplateVersion,
|
||||
@@ -13,6 +16,8 @@ const meta: Meta<typeof BuildLogsDrawer> = {
|
||||
component: BuildLogsDrawer,
|
||||
args: {
|
||||
open: true,
|
||||
onClose: fn(),
|
||||
onFillVariables: fn(),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -21,6 +26,63 @@ type Story = StoryObj<typeof BuildLogsDrawer>;
|
||||
|
||||
export const Loading: Story = {};
|
||||
|
||||
export const CloseWithButton: Story = {
|
||||
play: async ({ args }) => {
|
||||
// The drawer portals its content onto `document.body`, so query the screen.
|
||||
await userEvent.click(
|
||||
screen.getByRole("button", { name: "Close build logs" }),
|
||||
);
|
||||
await waitFor(() => expect(args.onClose).toHaveBeenCalled());
|
||||
},
|
||||
};
|
||||
|
||||
export const CloseWithEscape: Story = {
|
||||
play: async ({ args }) => {
|
||||
await userEvent.keyboard("{Escape}");
|
||||
await waitFor(() => expect(args.onClose).toHaveBeenCalled());
|
||||
},
|
||||
};
|
||||
|
||||
// When opened from a button outside the drawer, focus must return to that
|
||||
// button on close instead of falling back to the document body. The parent
|
||||
// owns this via `onCloseAutoFocus`.
|
||||
export const RestoresFocusToOpener: Story = {
|
||||
render: () => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const openerRef = useRef<HTMLButtonElement>(null);
|
||||
return (
|
||||
<>
|
||||
<Button ref={openerRef} onClick={() => setOpen(true)}>
|
||||
Show build logs
|
||||
</Button>
|
||||
<BuildLogsDrawer
|
||||
open={open}
|
||||
onClose={() => setOpen(false)}
|
||||
onFillVariables={fn()}
|
||||
onCloseAutoFocus={(event) => {
|
||||
event.preventDefault();
|
||||
openerRef.current?.focus();
|
||||
}}
|
||||
error={undefined}
|
||||
templateVersion={undefined}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
const opener = canvas.getByRole("button", { name: "Show build logs" });
|
||||
await userEvent.click(opener);
|
||||
await waitFor(() =>
|
||||
expect(screen.getByText("Creating template...")).toBeInTheDocument(),
|
||||
);
|
||||
await userEvent.click(
|
||||
screen.getByRole("button", { name: "Close build logs" }),
|
||||
);
|
||||
await waitFor(() => expect(opener).toHaveFocus());
|
||||
},
|
||||
};
|
||||
|
||||
export const MissingVariables: Story = {
|
||||
args: {
|
||||
templateVersion: MockTemplateVersion,
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
import Drawer from "@mui/material/Drawer";
|
||||
import { TriangleAlertIcon, XIcon } from "lucide-react";
|
||||
import type { FC } from "react";
|
||||
import { JobError } from "#/api/queries/templates";
|
||||
import type { TemplateVersion } from "#/api/typesGenerated";
|
||||
import { Button } from "#/components/Button/Button";
|
||||
import {
|
||||
Drawer,
|
||||
DrawerClose,
|
||||
DrawerContent,
|
||||
DrawerTitle,
|
||||
} from "#/components/Drawer/Drawer";
|
||||
import { Loader } from "#/components/Loader/Loader";
|
||||
import { AlertVariant } from "#/modules/provisioners/ProvisionerAlert";
|
||||
import { ProvisionerStatusAlert } from "#/modules/provisioners/ProvisionerStatusAlert";
|
||||
@@ -15,15 +20,25 @@ type BuildLogsDrawerProps = {
|
||||
error: unknown;
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
/** Invoked when the user opts to fill the missing template variables. */
|
||||
onFillVariables: () => void;
|
||||
/**
|
||||
* Forwarded to the drawer's `onCloseAutoFocus`. The drawer has no trigger,
|
||||
* so the parent owns where focus lands on close (e.g. the opener button).
|
||||
*/
|
||||
onCloseAutoFocus?: React.ComponentProps<
|
||||
typeof DrawerContent
|
||||
>["onCloseAutoFocus"];
|
||||
templateVersion: TemplateVersion | undefined;
|
||||
variablesSectionRef: React.RefObject<HTMLDivElement | null>;
|
||||
};
|
||||
|
||||
export const BuildLogsDrawer: FC<BuildLogsDrawerProps> = ({
|
||||
templateVersion,
|
||||
error,
|
||||
variablesSectionRef,
|
||||
...drawerProps
|
||||
open,
|
||||
onClose,
|
||||
onFillVariables,
|
||||
onCloseAutoFocus,
|
||||
}) => {
|
||||
const logs = useWatchVersionLogs(templateVersion);
|
||||
|
||||
@@ -37,52 +52,59 @@ export const BuildLogsDrawer: FC<BuildLogsDrawerProps> = ({
|
||||
const hasLogs = logs && logs.length > 0;
|
||||
|
||||
return (
|
||||
<Drawer anchor="right" {...drawerProps}>
|
||||
<div className="flex h-full w-[800px] flex-col">
|
||||
<header
|
||||
className="flex items-center justify-between border-b border-border px-6"
|
||||
style={{ height: navHeight }}
|
||||
>
|
||||
<h3 className="m-0 text-base font-medium">Creating template...</h3>
|
||||
<Button size="icon-lg" variant="subtle" onClick={drawerProps.onClose}>
|
||||
<XIcon />
|
||||
<span className="sr-only">Close build logs</span>
|
||||
</Button>
|
||||
</header>
|
||||
<Drawer
|
||||
open={open}
|
||||
onOpenChange={(isOpen) => {
|
||||
if (!isOpen) {
|
||||
onClose();
|
||||
}
|
||||
}}
|
||||
direction="right"
|
||||
>
|
||||
<DrawerContent
|
||||
className="!w-[min(800px,100%)] !max-w-full"
|
||||
onCloseAutoFocus={onCloseAutoFocus}
|
||||
>
|
||||
<div className="flex h-full flex-col">
|
||||
<header
|
||||
className="flex items-center justify-between border-b border-border px-6 bg-surface-secondary"
|
||||
style={{ height: navHeight }}
|
||||
>
|
||||
<DrawerTitle className="m-0 text-base font-medium">
|
||||
Creating template...
|
||||
</DrawerTitle>
|
||||
<DrawerClose asChild>
|
||||
<Button size="icon-lg" variant="subtle">
|
||||
<XIcon />
|
||||
<span className="sr-only">Close build logs</span>
|
||||
</Button>
|
||||
</DrawerClose>
|
||||
</header>
|
||||
|
||||
{isMissingVariables ? (
|
||||
<MissingVariablesBanner
|
||||
onFillVariables={() => {
|
||||
variablesSectionRef.current?.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
});
|
||||
const firstVariableInput =
|
||||
variablesSectionRef.current?.querySelector("input");
|
||||
firstVariableInput?.focus();
|
||||
drawerProps.onClose();
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
{(matchingProvisioners === 0 || !hasLogs) && (
|
||||
<ProvisionerStatusAlert
|
||||
matchingProvisioners={matchingProvisioners}
|
||||
availableProvisioners={availableProvisioners}
|
||||
tags={templateVersion?.job.tags ?? {}}
|
||||
variant={AlertVariant.Inline}
|
||||
/>
|
||||
)}
|
||||
{isMissingVariables ? (
|
||||
<MissingVariablesBanner onFillVariables={onFillVariables} />
|
||||
) : (
|
||||
<>
|
||||
{(matchingProvisioners === 0 || !hasLogs) && (
|
||||
<ProvisionerStatusAlert
|
||||
matchingProvisioners={matchingProvisioners}
|
||||
availableProvisioners={availableProvisioners}
|
||||
tags={templateVersion?.job.tags ?? {}}
|
||||
variant={AlertVariant.Inline}
|
||||
/>
|
||||
)}
|
||||
|
||||
{hasLogs ? (
|
||||
<section className="flex-1 overflow-auto bg-surface-primary">
|
||||
<WorkspaceBuildLogs logs={logs} className="border-0" />
|
||||
</section>
|
||||
) : (
|
||||
<Loader />
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{hasLogs ? (
|
||||
<section className="flex-1 overflow-auto bg-surface-primary">
|
||||
<WorkspaceBuildLogs logs={logs} className="border-0" />
|
||||
</section>
|
||||
) : (
|
||||
<Loader />
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -20,10 +20,49 @@ const CreateTemplatePage: FC = () => {
|
||||
const [templateVersion, setTemplateVersion] = useState<TemplateVersion>();
|
||||
const createTemplateMutation = useMutation(createTemplate());
|
||||
const variablesSectionRef = useRef<HTMLDivElement>(null);
|
||||
// Remember what had focus when the drawer opened so it can be restored on
|
||||
// close. The drawer is opened from buttons outside its tree, so Radix has no
|
||||
// trigger to fall back to.
|
||||
const buildLogsOpenerRef = useRef<HTMLElement | null>(null);
|
||||
// Keeps focus on the variables input (rather than the opener) when the drawer
|
||||
// closes via the "Fill variables" action.
|
||||
const preserveVariablesFocusRef = useRef(false);
|
||||
|
||||
const openBuildLogs = () => {
|
||||
buildLogsOpenerRef.current =
|
||||
document.activeElement instanceof HTMLElement
|
||||
? document.activeElement
|
||||
: null;
|
||||
setIsBuildLogsOpen(true);
|
||||
};
|
||||
|
||||
const fillVariables = () => {
|
||||
variablesSectionRef.current?.scrollIntoView({ behavior: "smooth" });
|
||||
preserveVariablesFocusRef.current = true;
|
||||
setIsBuildLogsOpen(false);
|
||||
};
|
||||
|
||||
const restoreFocusOnDrawerClose = (event: Event) => {
|
||||
// Radix focuses a DrawerTrigger on close by default, but this drawer has
|
||||
// none. Return focus to the variables input for the "Fill variables" flow,
|
||||
// otherwise to whatever opened the drawer, so keyboard users are not
|
||||
// dropped onto the document body.
|
||||
if (preserveVariablesFocusRef.current) {
|
||||
preserveVariablesFocusRef.current = false;
|
||||
event.preventDefault();
|
||||
variablesSectionRef.current?.querySelector("input")?.focus();
|
||||
return;
|
||||
}
|
||||
const opener = buildLogsOpenerRef.current;
|
||||
if (opener?.isConnected) {
|
||||
event.preventDefault();
|
||||
opener.focus();
|
||||
}
|
||||
};
|
||||
|
||||
const pageViewProps: CreateTemplatePageViewProps = {
|
||||
onCreateTemplate: async (options) => {
|
||||
setIsBuildLogsOpen(true);
|
||||
openBuildLogs();
|
||||
const template = await createTemplateMutation.mutateAsync({
|
||||
...options,
|
||||
onCreateVersion: setTemplateVersion,
|
||||
@@ -36,7 +75,7 @@ const CreateTemplatePage: FC = () => {
|
||||
{ state: { justCreated: true } },
|
||||
);
|
||||
},
|
||||
onOpenBuildLogsDrawer: () => setIsBuildLogsOpen(true),
|
||||
onOpenBuildLogsDrawer: openBuildLogs,
|
||||
error: createTemplateMutation.error,
|
||||
isCreating: createTemplateMutation.isPending,
|
||||
variablesSectionRef,
|
||||
@@ -60,8 +99,9 @@ const CreateTemplatePage: FC = () => {
|
||||
error={createTemplateMutation.error}
|
||||
open={isBuildLogsOpen}
|
||||
onClose={() => setIsBuildLogsOpen(false)}
|
||||
onFillVariables={fillVariables}
|
||||
onCloseAutoFocus={restoreFocusOnDrawerClose}
|
||||
templateVersion={templateVersion}
|
||||
variablesSectionRef={variablesSectionRef}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user