feat: show warning dialog if user switches off assign default org (#15848)

resolves coder/internal#240

Switching off the setting to automatically assign all users to the
default organization is potentially disruptive. This adds a warning
dialog before the change is commited.

<img width="1168" alt="Screenshot 2024-12-13 at 11 20 16"
src="https://github.com/user-attachments/assets/e6bf8c97-3cad-4501-9f28-073fca118668"
/>
This commit is contained in:
Jaayden Halko
2024-12-13 06:24:40 -05:00
committed by GitHub
parent b5ba3e3da8
commit 6116776905
5 changed files with 89 additions and 52 deletions
+7 -1
View File
@@ -78,7 +78,7 @@ test.describe("IdpOrgSyncPage", () => {
).toBeVisible();
});
test("toggle default organization assignment", async ({ page }) => {
test("toggle off default organization assignment", async ({ page }) => {
requiresLicense();
await page.goto("/deployment/idp-org-sync", {
waitUntil: "domcontentloaded",
@@ -89,6 +89,12 @@ test.describe("IdpOrgSyncPage", () => {
});
await toggle.click();
const dialog = page.getByRole("dialog");
await expect(dialog).toBeVisible();
await dialog.getByRole("button", { name: "Confirm" }).click();
await expect(dialog).not.toBeVisible();
await expect(
page.getByText("Organization sync settings updated."),
).toBeVisible();
+6 -1
View File
@@ -8,7 +8,12 @@ import { type FC, forwardRef } from "react";
import { cn } from "utils/cn";
export const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-content-link disabled:pointer-events-none disabled:text-content-disabled [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 font-semibold border-solid cursor-pointer",
`inline-flex items-center justify-center gap-2 whitespace-nowrap
border-solid rounded-md transition-colors
text-sm font-semibold font-medium cursor-pointer
focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-content-link
disabled:pointer-events-none disabled:text-content-disabled
[&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0`,
{
variants: {
variant: {
+10 -21
View File
@@ -3,7 +3,6 @@
* @see {@link https://ui.shadcn.com/docs/components/dialog}
*/
import * as DialogPrimitive from "@radix-ui/react-dialog";
import { X } from "lucide-react";
import {
type ComponentPropsWithoutRef,
type ElementRef,
@@ -46,29 +45,19 @@ export const DialogContent = forwardRef<
<DialogPrimitive.Content
ref={ref}
className={cn(
`fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg
gap-4 border bg-background p-6 shadow-lg duration-200 sm:rounded-lg
translate-x-[-50%] translate-y-[-50%]
data-[state=open]:animate-in data-[state=closed]:animate-out
data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0
data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95
data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%]
data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%]`,
`fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg gap-4
border border-solid border-border bg-surface-primary p-8 shadow-lg duration-200 sm:rounded-lg
translate-x-[-50%] translate-y-[-50%]
data-[state=open]:animate-in data-[state=closed]:animate-out
data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0
data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95
data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%]
data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%]`,
className,
)}
{...props}
>
{children}
<DialogPrimitive.Close
className={`absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity
hover:opacity-100
focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2
disabled:pointer-events-none
data-[state=open]:bg-surface-secondary data-[state=open]:text-content-disabled`}
>
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
));
@@ -106,7 +95,7 @@ export const DialogTitle = forwardRef<
<DialogPrimitive.Title
ref={ref}
className={cn(
"text-lg font-semibold leading-none tracking-tight",
"text-xl m-0 text-content-primary font-semibold leading-none tracking-tight",
className,
)}
{...props}
@@ -119,7 +108,7 @@ export const DialogDescription = forwardRef<
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
className={cn("text-sm text-content-disabled", className)}
className={cn("text-sm text-content-secondary", className)}
{...props}
/>
));
@@ -1,4 +1,5 @@
import type { Meta, StoryObj } from "@storybook/react";
import { userEvent, within } from "@storybook/test";
import {
MockOrganization,
MockOrganization2,
@@ -48,3 +49,18 @@ export const MissingGroups: Story = {
organizationSyncSettings: MockOrganizationSyncSettings,
},
};
export const AssignDefaultOrgWarningDialog: Story = {
args: {
organizationSyncSettings: MockOrganizationSyncSettings,
organizations: [MockOrganization, MockOrganization2],
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await userEvent.click(
canvas.getByRole("switch", {
name: "Assign Default Organization",
}),
);
},
};
@@ -1,4 +1,3 @@
import Skeleton from "@mui/material/Skeleton";
import Table from "@mui/material/Table";
import TableBody from "@mui/material/TableBody";
import TableCell from "@mui/material/TableCell";
@@ -12,6 +11,14 @@ import type {
import { ErrorAlert } from "components/Alert/ErrorAlert";
import { Button } from "components/Button/Button";
import { ChooseOne, Cond } from "components/Conditionals/ChooseOne";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "components/Dialog/Dialog";
import { EmptyState } from "components/EmptyState/EmptyState";
import {
HelpTooltip,
@@ -26,10 +33,6 @@ import {
type Option,
} from "components/MultiSelectCombobox/MultiSelectCombobox";
import { Switch } from "components/Switch/Switch";
import {
TableLoaderSkeleton,
TableRowSkeleton,
} from "components/TableLoader/TableLoader";
import { useFormik } from "formik";
import { Plus, SquareArrowOutUpRight, Trash } from "lucide-react";
import { type FC, useState } from "react";
@@ -74,6 +77,7 @@ export const IdpOrgSyncPageView: FC<IdpSyncPageViewProps> = ({
const organizationMappingCount = form.values.mapping
? Object.entries(form.values.mapping).length
: 0;
const [isDialogOpen, setIsDialogOpen] = useState(false);
const getOrgNames = (orgIds: readonly string[]) => {
return orgIds.map(
@@ -136,11 +140,15 @@ export const IdpOrgSyncPageView: FC<IdpSyncPageViewProps> = ({
id={ORGANIZATION_ASSIGN_DEFAULT_ID}
checked={form.values.organization_assign_default}
onCheckedChange={async (checked) => {
void form.setFieldValue(
"organization_assign_default",
checked,
);
form.handleSubmit();
if (!checked) {
setIsDialogOpen(true);
} else {
void form.setFieldValue(
"organization_assign_default",
checked,
);
form.handleSubmit();
}
}}
/>
<span className="flex flex-row items-center gap-1">
@@ -234,6 +242,36 @@ export const IdpOrgSyncPageView: FC<IdpSyncPageViewProps> = ({
</div>
</fieldset>
</form>
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
<DialogContent className="flex flex-col gap-12 max-w-lg">
<DialogHeader className="flex flex-col gap-4">
<DialogTitle>
Switch off default organization assignment
</DialogTitle>
<DialogDescription>
Warning: This will remove all users from the default organization
unless otherwise specified in an organization mapping defined
below.
</DialogDescription>
</DialogHeader>
<DialogFooter className="flex flex-row">
<Button variant="outline" onClick={() => setIsDialogOpen(false)}>
Cancel
</Button>
<Button
onClick={() => {
void form.setFieldValue("organization_assign_default", false);
setIsDialogOpen(false);
form.handleSubmit();
}}
type="submit"
>
Confirm
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
};
@@ -318,31 +356,14 @@ const OrganizationRow: FC<OrganizationRowProps> = ({
);
};
const TableLoader = () => {
return (
<TableLoaderSkeleton>
<TableRowSkeleton>
<TableCell>
<Skeleton variant="text" width="25%" />
</TableCell>
<TableCell>
<Skeleton variant="text" width="25%" />
</TableCell>
<TableCell>
<Skeleton variant="text" width="10%" />
</TableCell>
</TableRowSkeleton>
</TableLoaderSkeleton>
);
};
export const AssignDefaultOrgHelpTooltip: FC = () => {
return (
<HelpTooltip>
<HelpTooltipTrigger />
<HelpTooltipContent>
<HelpTooltipText>
Disabling will remove all users from the default organization.
Disabling will remove all users from the default organization if a
mapping for the default organization is not defined.
</HelpTooltipText>
</HelpTooltipContent>
</HelpTooltip>