mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
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:
@@ -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();
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user