Thanks to visit codestin.com
Credit goes to github.com

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
2 changes: 1 addition & 1 deletion docs/admin/users/idp-sync.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ group sync for each organization.
1. As an Owner or Organization Admin, go to **Admin settings**, select
**Organizations**, then **IdP Sync**:

![IdP Sync - Group sync settings](../../images/admin/users/organizations/group-sync-empty.png)
![IdP Sync - Group sync](../../images/admin/users/organizations/group-sync-empty.png)

1. Enter the **Group sync field** and an optional **Regex filter**, then select
**Save**.
Expand Down
18 changes: 13 additions & 5 deletions site/e2e/tests/organizations/idpGroupSync.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,12 @@ test.describe("IdpGroupSyncPage", () => {

const row = page.getByTestId("group-idp-group-1");
await expect(row.getByRole("cell", { name: "idp-group-1" })).toBeVisible();
await row.getByRole("button", { name: /delete/i }).click();
await row.getByRole("button", { name: /delete mapping/i }).click();
const dialog = page.getByRole("dialog", { name: "Delete group mapping" });
await dialog
.getByLabel("Name of the group mapping to delete")
.fill("idp-group-1");
await dialog.getByRole("button", { name: /^delete$/i }).click();
await expect(
row.getByRole("cell", { name: "idp-group-1" }),
).not.toBeVisible();
Expand All @@ -89,17 +94,18 @@ test.describe("IdpGroupSyncPage", () => {
waitUntil: "domcontentloaded",
});

const syncField = page.getByRole("textbox", {
const groupForm = page.getByRole("form", { name: "Group sync" });
const syncField = groupForm.getByRole("textbox", {
name: "Group sync field",
});
const saveButton = page.getByRole("button", { name: /save/i });
const saveButton = groupForm.getByRole("button", { name: /^save$/i });

await expect(saveButton).toBeDisabled();

await syncField.fill("test-field");
await expect(saveButton).toBeEnabled();

await page.getByRole("button", { name: /save/i }).click();
await saveButton.click();

await expect(
page.getByText("IdP group sync settings updated."),
Expand Down Expand Up @@ -135,7 +141,9 @@ test.describe("IdpGroupSyncPage", () => {
waitUntil: "domcontentloaded",
});

const exportButton = page.getByRole("button", { name: /Export Policy/i });
const exportButton = page.getByRole("button", {
name: /export policy/i,
});
await expect(exportButton).toBeEnabled();
await exportButton.click();
});
Expand Down
17 changes: 10 additions & 7 deletions site/e2e/tests/organizations/idpRoleSync.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ test.describe("IdpRoleSyncPage", () => {
row.getByRole("cell", { name: "idp-role-1" }),
).not.toBeVisible();
await expect(
page.getByText("IdP Role sync settings updated."),
page.getByText("IdP role sync settings updated."),
).toBeVisible();

await deleteOrganization(org.name);
Expand All @@ -83,20 +83,21 @@ test.describe("IdpRoleSyncPage", () => {
waitUntil: "domcontentloaded",
});

const syncField = page.getByRole("textbox", {
const roleForm = page.getByRole("form", { name: "Role sync" });
const syncField = roleForm.getByRole("textbox", {
name: "Role sync field",
});
const saveButton = page.getByRole("button", { name: /save/i });
const saveButton = roleForm.getByRole("button", { name: /^save$/i });

await expect(saveButton).toBeDisabled();

await syncField.fill("test-field");
await expect(saveButton).toBeEnabled();

await page.getByRole("button", { name: /save/i }).click();
await saveButton.click();

await expect(
page.getByText("IdP Role sync settings updated."),
page.getByText("IdP role sync settings updated."),
).toBeVisible();

await deleteOrganization(org.name);
Expand All @@ -110,7 +111,9 @@ test.describe("IdpRoleSyncPage", () => {
waitUntil: "domcontentloaded",
});

const exportButton = page.getByRole("button", { name: /Export Policy/i });
const exportButton = page.getByRole("button", {
name: /export policy/i,
});
await createRoleSyncSettings(org.id);

await expect(exportButton).toBeEnabled();
Expand Down Expand Up @@ -166,7 +169,7 @@ test.describe("IdpRoleSyncPage", () => {
).toBeVisible();

await expect(
page.getByText("IdP Role sync settings updated."),
page.getByText("IdP role sync settings updated."),
).toBeVisible();

await deleteOrganization(orgName);
Expand Down
15 changes: 15 additions & 0 deletions site/src/components/SettingsHeader/SettingsHeader.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,3 +81,18 @@ export const SecondaryHeaderWithDescriptionAndDocsLink: Story = {
actions: <SettingsHeaderDocsLink href={docs("/admin/external-auth")} />,
},
};

export const TertiaryHeaderWithDescription: Story = {
args: {
children: (
<>
<SettingsHeaderTitle level="h3" hierarchy="tertiary">
This is a tertiary header.
</SettingsHeaderTitle>
<SettingsHeaderDescription>
Use tertiary styling for subsection titles under a secondary header.
</SettingsHeaderDescription>
</>
),
},
};
12 changes: 8 additions & 4 deletions site/src/components/SettingsHeader/SettingsHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,13 @@ export const SettingsHeader: FC<SettingsHeaderProps> = ({
className,
}) => {
return (
<hgroup className="flex flex-col justify-between items-start gap-2 pb-6 sm:flex-row">
<div className={cn("text-sm flex flex-col gap-2 flex-1", className)}>
{children}
</div>
<hgroup
className={cn(
"flex flex-col justify-between items-start gap-2 pb-6 sm:flex-row",
className,
)}
>
<div className="text-sm flex flex-col gap-2 flex-1">{children}</div>
{actions}
</hgroup>
);
Expand Down Expand Up @@ -49,6 +52,7 @@ const titleVariants = cva("m-0 flex items-center gap-2 leading-tight", {
hierarchy: {
primary: "text-3xl font-semibold",
secondary: "text-2xl font-medium",
tertiary: "text-xl font-medium",
},
},
defaultVariants: {
Expand Down
99 changes: 99 additions & 0 deletions site/src/modules/idpSync/ExportPolicyButton.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { expect, fn, userEvent, waitFor, within } from "storybook/test";
import {
MockGroupSyncSettings,
MockOrganization,
MockOrganizationSyncSettings,
MockRoleSyncSettings,
} from "#/testHelpers/entities";
import { ExportPolicyButton } from "./ExportPolicyButton";

const meta: Meta<typeof ExportPolicyButton> = {
title: "modules/idpSync/ExportPolicyButton",
component: ExportPolicyButton,
args: {
syncSettings: MockGroupSyncSettings,
filename: `${MockOrganization.name}_groups-policy.json`,
size: "sm",
},
};

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

export const Default: Story = {};

const downloadGroupPolicy = fn<(file: Blob, filename: string) => void>();
const downloadRolePolicy = fn<(file: Blob, filename: string) => void>();
const downloadOrganizationPolicy = fn<(file: Blob, filename: string) => void>();

const expectExportedPolicy = async (
canvasElement: HTMLElement,
download: typeof downloadGroupPolicy,
filename: string,
settings: unknown,
) => {
const canvas = within(canvasElement);

await userEvent.click(canvas.getByRole("button", { name: "Export policy" }));
await waitFor(() =>
expect(download).toHaveBeenCalledWith(expect.anything(), filename),
);
const [blob] = download.mock.lastCall ?? [];
await expect(blob).toBeInstanceOf(Blob);
if (!(blob instanceof Blob)) {
return;
}
await expect(blob.type).toEqual("application/json");
await expect(await blob.text()).toEqual(JSON.stringify(settings, null, 2));
};

export const ClickExportGroupPolicy: Story = {
args: {
syncSettings: MockGroupSyncSettings,
filename: `${MockOrganization.name}_groups-policy.json`,
size: "sm",
download: downloadGroupPolicy,
},
play: async ({ canvasElement }) => {
await expectExportedPolicy(
canvasElement,
downloadGroupPolicy,
`${MockOrganization.name}_groups-policy.json`,
MockGroupSyncSettings,
);
},
};

export const ClickExportRolePolicy: Story = {
args: {
syncSettings: MockRoleSyncSettings,
filename: `${MockOrganization.name}_roles-policy.json`,
size: "sm",
download: downloadRolePolicy,
},
play: async ({ canvasElement }) => {
await expectExportedPolicy(
canvasElement,
downloadRolePolicy,
`${MockOrganization.name}_roles-policy.json`,
MockRoleSyncSettings,
);
},
};

export const ClickExportOrganizationPolicy: Story = {
args: {
syncSettings: MockOrganizationSyncSettings,
filename: "organizations_policy.json",
download: downloadOrganizationPolicy,
},
play: async ({ canvasElement }) => {
await expectExportedPolicy(
canvasElement,
downloadOrganizationPolicy,
"organizations_policy.json",
MockOrganizationSyncSettings,
);
},
};
66 changes: 66 additions & 0 deletions site/src/modules/idpSync/ExportPolicyButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { saveAs } from "file-saver";
import { DownloadIcon } from "lucide-react";
import { type FC, useState } from "react";
import { toast } from "sonner";
import { getErrorDetail } from "#/api/errors";
import type {
GroupSyncSettings,
OrganizationSyncSettings,
RoleSyncSettings,
} from "#/api/typesGenerated";
import { Button, type ButtonProps } from "#/components/Button/Button";

type ExportableSyncSettings =
| GroupSyncSettings
| RoleSyncSettings
| OrganizationSyncSettings;

interface ExportPolicyButtonProps {
syncSettings: ExportableSyncSettings | undefined;
filename: string;
size?: ButtonProps["size"];
download?: (file: Blob, filename: string) => void;
}

export const ExportPolicyButton: FC<ExportPolicyButtonProps> = ({
syncSettings,
filename,
size = "sm",
download = saveAs,
}) => {
const [isDownloading, setIsDownloading] = useState(false);

const canCreatePolicyJson =
Boolean(syncSettings?.field) &&
Object.keys(syncSettings?.mapping ?? {}).length > 0;

return (
<Button
size={size}
variant="outline"
disabled={!canCreatePolicyJson || isDownloading}
onClick={async () => {
if (!canCreatePolicyJson) {
return;
}

try {
setIsDownloading(true);
const file = new Blob([JSON.stringify(syncSettings, null, 2)], {
type: "application/json",
});
download(file, filename);
} catch (error) {
toast.error("Failed to export policy JSON.", {
description: getErrorDetail(error),
});
} finally {
setIsDownloading(false);
}
}}
>
<DownloadIcon />
Export policy
</Button>
);
};
23 changes: 23 additions & 0 deletions site/src/modules/idpSync/IdpUnseenClaimWarning.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { screen, userEvent, within } from "storybook/test";
import { IdpUnseenClaimWarning } from "./IdpUnseenClaimWarning";

const meta: Meta<typeof IdpUnseenClaimWarning> = {
title: "modules/idpSync/IdpUnseenClaimWarning",
component: IdpUnseenClaimWarning,
};

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

export const Default: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await userEvent.hover(
canvas.getByRole("button", { name: "Unknown claim value" }),
);
await screen.findByRole("tooltip", {
name: /has not be seen in the specified claim field/i,
});
},
};
33 changes: 33 additions & 0 deletions site/src/modules/idpSync/IdpUnseenClaimWarning.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { TriangleAlertIcon } from "lucide-react";
import type { FC } from "react";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "#/components/Tooltip/Tooltip";

export const IdpUnseenClaimWarning: FC = () => {
return (
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
aria-label="Unknown claim value"
className="inline-flex items-center justify-center border-0 bg-transparent p-0 text-content-warning cursor-pointer"
>
<TriangleAlertIcon className="size-icon-xs" />
</button>
</TooltipTrigger>
<TooltipContent
align="start"
alignOffset={-8}
sideOffset={8}
className="p-2 text-xs text-content-secondary max-w-sm"
>
This value has not be seen in the specified claim field before. You
might want to check your IdP configuration and ensure that this value is
not misspelled.
</TooltipContent>
</Tooltip>
);
};
Loading
Loading