From a40ba7d3de288bc71101ef03c81aa02afe7179a8 Mon Sep 17 00:00:00 2001 From: Jaayden Halko Date: Tue, 17 Sep 2024 21:22:10 +0000 Subject: [PATCH 01/13] feat: idp sync initial commit --- site/src/api/api.ts | 21 ++++++++ site/src/api/queries/organizations.ts | 26 ++++++++++ .../AppearanceSettingsPageView.tsx | 2 +- .../IdpSyncPage/IdpSyncPage.tsx | 49 ++++++++++++++++--- .../IdpSyncPage/IdpSyncPageView.tsx | 33 ++++++++----- 5 files changed, 111 insertions(+), 20 deletions(-) diff --git a/site/src/api/api.ts b/site/src/api/api.ts index e0781846ff4fe..b44b03c1a5456 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -704,6 +704,27 @@ class ApiMethods { return response.data; }; + getGroupIdpSyncSettingsByOrganization = async ( + organization: string, + ): Promise => { + const response = await this.axios.get( + `/api/v2/organizations/${organization}/settings/idpsync/groups`, + ); + return response.data; + }; + + /** + * @param organization Can be the organization's ID or name + */ + getRoleIdpSyncSettingsByOrganization = async ( + organization: string, + ): Promise => { + const response = await this.axios.get( + `/api/v2/organizations/${organization}/settings/idpsync/roles`, + ); + return response.data; + }; + getTemplate = async (templateId: string): Promise => { const response = await this.axios.get( `/api/v2/templates/${templateId}`, diff --git a/site/src/api/queries/organizations.ts b/site/src/api/queries/organizations.ts index 8e1143800b869..f0a5cca33cb53 100644 --- a/site/src/api/queries/organizations.ts +++ b/site/src/api/queries/organizations.ts @@ -141,6 +141,32 @@ export const provisionerDaemonGroups = (organization: string) => { }; }; +export const getGroupIdpSyncSettingsKey = (organization: string) => [ + "organization", + organization, + "groupIdpSyncSettings", +]; + +export const groupIdpSyncSettings = (organization: string) => { + return { + queryKey: getGroupIdpSyncSettingsKey(organization), + queryFn: () => API.getGroupIdpSyncSettingsByOrganization(organization), + }; +}; + +export const getRoleIdpSyncSettingsKey = (organization: string) => [ + "organization", + organization, + "roleIdpSyncSettings", +]; + +export const roleIdpSyncSettings = (organization: string) => { + return { + queryKey: getRoleIdpSyncSettingsKey(organization), + queryFn: () => API.getRoleIdpSyncSettingsByOrganization(organization), + }; +}; + /** * Fetch permissions for a single organization. * diff --git a/site/src/pages/DeploySettingsPage/AppearanceSettingsPage/AppearanceSettingsPageView.tsx b/site/src/pages/DeploySettingsPage/AppearanceSettingsPage/AppearanceSettingsPageView.tsx index 0c5d470c027fe..2060c1b5f5660 100644 --- a/site/src/pages/DeploySettingsPage/AppearanceSettingsPage/AppearanceSettingsPageView.tsx +++ b/site/src/pages/DeploySettingsPage/AppearanceSettingsPage/AppearanceSettingsPageView.tsx @@ -74,7 +74,7 @@ export const AppearanceSettingsPageView: FC< diff --git a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPage.tsx b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPage.tsx index 93b4e59455409..1fe628a11b2e0 100644 --- a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPage.tsx +++ b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPage.tsx @@ -6,11 +6,20 @@ import { SettingsHeader } from "components/SettingsHeader/SettingsHeader"; import { Stack } from "components/Stack/Stack"; import type { FC } from "react"; import { Helmet } from "react-helmet-async"; -import { Link as RouterLink } from "react-router-dom"; +import { Link as RouterLink, useParams } from "react-router-dom"; import { docs } from "utils/docs"; import { pageTitle } from "utils/page"; import { IdpSyncHelpTooltip } from "./IdpSyncHelpTooltip"; import IdpSyncPageView from "./IdpSyncPageView"; +import { + organizationsPermissions, + groupIdpSyncSettings, + roleIdpSyncSettings, +} from "api/queries/organizations"; +import { useQuery } from "react-query"; +import { useOrganizationSettings } from "../ManagementSettingsLayout"; +import { Loader } from "components/Loader/Loader"; +import { EmptyState } from "components/EmptyState/EmptyState"; const mockOIDCConfig = { allow_signups: true, @@ -45,19 +54,39 @@ const mockOIDCConfig = { }; export const IdpSyncPage: FC = () => { + const { organization: organizationName } = useParams() as { + organization: string; + }; + // feature visibility and permissions to be implemented when integrating with backend // const feats = useFeatureVisibility(); // const { organization: organizationName } = useParams() as { // organization: string; // }; - // const { organizations } = useOrganizationSettings(); - // const organization = organizations?.find((o) => o.name === organizationName); - // const permissionsQuery = useQuery(organizationPermissions(organization?.id)); + const { organizations } = useOrganizationSettings(); + const organization = organizations?.find((o) => o.name === organizationName); + const permissionsQuery = useQuery( + organizationsPermissions(organizations?.map((o) => o.id)), + ); + const groupIdpSyncSettingsQuery = useQuery( + groupIdpSyncSettings(organizationName), + ); + const roleIdpSyncSettingsQuery = useQuery( + roleIdpSyncSettings(organizationName), + ); // const permissions = permissionsQuery.data; - // if (!permissions) { - // return ; - // } + if (!organization) { + return ; + } + + if ( + permissionsQuery.isLoading || + groupIdpSyncSettingsQuery.isLoading || + roleIdpSyncSettingsQuery.isLoading + ) { + return ; + } return ( <> @@ -91,7 +120,11 @@ export const IdpSyncPage: FC = () => { - + ); }; diff --git a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.tsx b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.tsx index 08c165d6c7e91..7c7225d0373e8 100644 --- a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.tsx +++ b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.tsx @@ -9,7 +9,11 @@ import TableCell from "@mui/material/TableCell"; import TableContainer from "@mui/material/TableContainer"; import TableHead from "@mui/material/TableHead"; import TableRow from "@mui/material/TableRow"; -import type { OIDCConfig } from "api/typesGenerated"; +import type { + OIDCConfig, + GroupSyncSettings, + RoleSyncSettings, +} from "api/typesGenerated"; import { ChooseOne, Cond } from "components/Conditionals/ChooseOne"; import { EmptyState } from "components/EmptyState/EmptyState"; import { Paywall } from "components/Paywall/Paywall"; @@ -25,16 +29,17 @@ import { docs } from "utils/docs"; export type IdpSyncPageViewProps = { oidcConfig: OIDCConfig | undefined; + groupSyncSettings: GroupSyncSettings | undefined; + roleSyncSettings: RoleSyncSettings | undefined; }; -export const IdpSyncPageView: FC = ({ oidcConfig }) => { +export const IdpSyncPageView: FC = ({ + oidcConfig, + groupSyncSettings, + roleSyncSettings, +}) => { const theme = useTheme(); - const { - groups_field, - user_role_field, - group_regex_filter, - group_auto_create, - } = oidcConfig || {}; + const { user_role_field } = oidcConfig || {}; return ( <> @@ -54,16 +59,22 @@ export const IdpSyncPageView: FC = ({ oidcConfig }) => { From 7ff9a07c17edd0dfd8b41a0ac9479444b7cbf5bf Mon Sep 17 00:00:00 2001 From: Jaayden Halko Date: Fri, 20 Sep 2024 21:02:07 +0000 Subject: [PATCH 02/13] fix: hookup backend data for groups and roles --- .../IdpSyncPage/IdpSyncPage.tsx | 58 ++++-------- .../IdpSyncPage/IdpSyncPageView.stories.tsx | 19 +++- .../IdpSyncPage/IdpSyncPageView.tsx | 80 +++++++++------- .../IdpSyncPage/PillList.tsx | 91 +++++++++++++++++++ site/src/testHelpers/entities.ts | 67 +++++++------- 5 files changed, 206 insertions(+), 109 deletions(-) create mode 100644 site/src/pages/ManagementSettingsPage/IdpSyncPage/PillList.tsx diff --git a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPage.tsx b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPage.tsx index 1fe628a11b2e0..a647ff80c66d0 100644 --- a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPage.tsx +++ b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPage.tsx @@ -1,57 +1,27 @@ import AddIcon from "@mui/icons-material/AddOutlined"; import LaunchOutlined from "@mui/icons-material/LaunchOutlined"; import Button from "@mui/material/Button"; +import { groupsByOrganization } from "api/queries/groups"; +import { + groupIdpSyncSettings, + organizationsPermissions, + roleIdpSyncSettings, +} from "api/queries/organizations"; +import { EmptyState } from "components/EmptyState/EmptyState"; import { FeatureStageBadge } from "components/FeatureStageBadge/FeatureStageBadge"; +import { Loader } from "components/Loader/Loader"; import { SettingsHeader } from "components/SettingsHeader/SettingsHeader"; import { Stack } from "components/Stack/Stack"; +import { useDashboard } from "modules/dashboard/useDashboard"; import type { FC } from "react"; import { Helmet } from "react-helmet-async"; +import { useQuery } from "react-query"; import { Link as RouterLink, useParams } from "react-router-dom"; import { docs } from "utils/docs"; import { pageTitle } from "utils/page"; +import { useOrganizationSettings } from "../ManagementSettingsLayout"; import { IdpSyncHelpTooltip } from "./IdpSyncHelpTooltip"; import IdpSyncPageView from "./IdpSyncPageView"; -import { - organizationsPermissions, - groupIdpSyncSettings, - roleIdpSyncSettings, -} from "api/queries/organizations"; -import { useQuery } from "react-query"; -import { useOrganizationSettings } from "../ManagementSettingsLayout"; -import { Loader } from "components/Loader/Loader"; -import { EmptyState } from "components/EmptyState/EmptyState"; - -const mockOIDCConfig = { - allow_signups: true, - client_id: "test", - client_secret: "test", - client_key_file: "test", - client_cert_file: "test", - email_domain: [], - issuer_url: "test", - scopes: [], - ignore_email_verified: true, - username_field: "", - name_field: "", - email_field: "", - auth_url_params: {}, - ignore_user_info: true, - organization_field: "", - organization_mapping: {}, - organization_assign_default: true, - group_auto_create: false, - group_regex_filter: "^Coder-.*$", - group_allow_list: [], - groups_field: "groups", - group_mapping: { group1: "developers", group2: "admin", group3: "auditors" }, - user_role_field: "roles", - user_role_mapping: { role1: ["role1", "role2"] }, - user_roles_default: [], - sign_in_text: "", - icon_url: "", - signups_disabled_text: "string", - skip_issuer_checks: true, -}; export const IdpSyncPage: FC = () => { const { organization: organizationName } = useParams() as { @@ -64,6 +34,7 @@ export const IdpSyncPage: FC = () => { // organization: string; // }; const { organizations } = useOrganizationSettings(); + const organization = organizations?.find((o) => o.name === organizationName); const permissionsQuery = useQuery( organizationsPermissions(organizations?.map((o) => o.id)), @@ -71,9 +42,12 @@ export const IdpSyncPage: FC = () => { const groupIdpSyncSettingsQuery = useQuery( groupIdpSyncSettings(organizationName), ); + + const groupsQuery = useQuery(groupsByOrganization(organizationName)); const roleIdpSyncSettingsQuery = useQuery( roleIdpSyncSettings(organizationName), ); + // const permissions = permissionsQuery.data; if (!organization) { @@ -121,9 +95,9 @@ export const IdpSyncPage: FC = () => { ); diff --git a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.stories.tsx b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.stories.tsx index 47952edc61c95..a27dc85e48d79 100644 --- a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.stories.tsx +++ b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.stories.tsx @@ -1,5 +1,10 @@ import type { Meta, StoryObj } from "@storybook/react"; -import { MockOIDCConfig } from "testHelpers/entities"; +import { + MockGroup, + MockGroup2, + MockGroupSyncSettings, + MockRoleSyncSettings, +} from "testHelpers/entities"; import { IdpSyncPageView } from "./IdpSyncPageView"; const meta: Meta = { @@ -11,9 +16,17 @@ export default meta; type Story = StoryObj; export const Empty: Story = { - args: { oidcConfig: undefined }, + args: { + groupSyncSettings: undefined, + roleSyncSettings: undefined, + groups: [MockGroup, MockGroup2], + }, }; export const Default: Story = { - args: { oidcConfig: MockOIDCConfig }, + args: { + groupSyncSettings: MockGroupSyncSettings, + roleSyncSettings: MockRoleSyncSettings, + groups: [MockGroup, MockGroup2], + }, }; diff --git a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.tsx b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.tsx index 7c7225d0373e8..424837a356fc2 100644 --- a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.tsx +++ b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.tsx @@ -10,7 +10,7 @@ import TableContainer from "@mui/material/TableContainer"; import TableHead from "@mui/material/TableHead"; import TableRow from "@mui/material/TableRow"; import type { - OIDCConfig, + Group, GroupSyncSettings, RoleSyncSettings, } from "api/typesGenerated"; @@ -26,20 +26,32 @@ import { import type { FC } from "react"; import { MONOSPACE_FONT_FAMILY } from "theme/constants"; import { docs } from "utils/docs"; +import { PillList } from "./PillList"; export type IdpSyncPageViewProps = { - oidcConfig: OIDCConfig | undefined; groupSyncSettings: GroupSyncSettings | undefined; roleSyncSettings: RoleSyncSettings | undefined; + groups: Group[] | undefined; }; export const IdpSyncPageView: FC = ({ - oidcConfig, groupSyncSettings, roleSyncSettings, + groups, }) => { - const theme = useTheme(); - const { user_role_field } = oidcConfig || {}; + // const theme = useTheme(); + + const groupsMap = new Map(); + if (groups) { + for (const group of groups) { + groupsMap.set(group.id, group.display_name || group.name); + } + } + + const getGroupNames = (groupIds: readonly string[]) => { + return groupIds.map((groupId) => groupsMap.get(groupId) || groupId); + }; + return ( <> @@ -67,13 +79,13 @@ export const IdpSyncPageView: FC = ({ fieldText={ typeof groupSyncSettings?.regex_filter === "string" ? groupSyncSettings?.regex_filter - : "" + : "none" } /> @@ -83,7 +95,7 @@ export const IdpSyncPageView: FC = ({ @@ -91,38 +103,38 @@ export const IdpSyncPageView: FC = ({ - {oidcConfig?.user_role_mapping && - Object.entries(oidcConfig.user_role_mapping) + {groupSyncSettings?.mapping && + Object.entries(groupSyncSettings.mapping) .sort() - .map(([idpRole, roles]) => ( - ( + ))} - {oidcConfig?.user_role_mapping && - Object.entries(oidcConfig.group_mapping) + {roleSyncSettings?.mapping && + Object.entries(roleSyncSettings.mapping) .sort() - .map(([idpGroup, group]) => ( - ( + ))} @@ -226,28 +238,32 @@ const IdpMappingTable: FC = ({ interface GroupRowProps { idpGroup: string; - coderGroup: string; + coderGroup: readonly string[]; } const GroupRow: FC = ({ idpGroup, coderGroup }) => { return ( {idpGroup} - {coderGroup} + + + ); }; interface RoleRowProps { idpRole: string; - coderRoles: ReadonlyArray; + coderRoles: readonly string[]; } const RoleRow: FC = ({ idpRole, coderRoles }) => { return ( {idpRole} - coderRoles Placeholder + + + ); }; diff --git a/site/src/pages/ManagementSettingsPage/IdpSyncPage/PillList.tsx b/site/src/pages/ManagementSettingsPage/IdpSyncPage/PillList.tsx new file mode 100644 index 0000000000000..02b64d1c1a8ab --- /dev/null +++ b/site/src/pages/ManagementSettingsPage/IdpSyncPage/PillList.tsx @@ -0,0 +1,91 @@ +import { type Interpolation, type Theme, useTheme } from "@emotion/react"; +import Stack from "@mui/material/Stack"; +import { Pill } from "components/Pill/Pill"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "components/Popover/Popover"; +import type { FC } from "react"; + +interface PillListProps { + roles: readonly string[]; +} + +export const PillList: FC = ({ roles }) => { + return ( + + {roles.length > 0 ? ( + {roles[0]} + ) : ( +

None

+ )} + + {roles.length > 1 && } +
+ ); +}; + +type OverflowPillProps = { + roles: string[]; +}; + +const OverflowPill: FC = ({ roles }) => { + const theme = useTheme(); + + return ( + + + + +{roles.length} more + + + + + {roles.map((role) => ( + + {role} + + ))} + + + ); +}; + +const styles = { + pill: (theme) => ({ + backgroundColor: theme.experimental.pillDefault.background, + borderColor: theme.experimental.pillDefault.outline, + color: theme.experimental.pillDefault.text, + width: "fit-content", + }), +} satisfies Record>; diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index 9d9f3192fd9c6..d134f84203405 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -451,38 +451,6 @@ export const MockAssignableSiteRoles = [ assignableRole(MockAuditorRole, true), ]; -export const MockOIDCConfig: TypesGen.OIDCConfig = { - allow_signups: true, - client_id: "test", - client_secret: "test", - client_key_file: "test", - client_cert_file: "test", - email_domain: [], - issuer_url: "test", - scopes: [], - ignore_email_verified: true, - username_field: "", - name_field: "", - email_field: "", - auth_url_params: {}, - ignore_user_info: true, - organization_field: "", - organization_mapping: {}, - organization_assign_default: true, - group_auto_create: false, - group_regex_filter: "^Coder-.*$", - group_allow_list: [], - groups_field: "groups", - group_mapping: { group1: "developers", group2: "admin", group3: "auditors" }, - user_role_field: "roles", - user_role_mapping: { role1: ["role1", "role2"] }, - user_roles_default: [], - sign_in_text: "", - icon_url: "", - signups_disabled_text: "string", - skip_issuer_checks: true, -}; - export const MockMemberPermissions = { viewAuditLog: false, }; @@ -2632,6 +2600,27 @@ export const MockWorkspaceQuota: TypesGen.WorkspaceQuota = { budget: 100, }; +export const MockGroupSyncSettings: TypesGen.GroupSyncSettings = { + field: "group-test", + mapping: { + "idp-group-1": [ + "fbd2116a-8961-4954-87ae-e4575bd29ce0", + "13de3eb4-9b4f-49e7-b0f8-0c3728a0d2e2", + ], + "idp-group-2": ["fbd2116a-8961-4954-87ae-e4575bd29ce0"], + }, + regex_filter: "@[a-zA-Z0-9_]+", + auto_create_missing_groups: false, +}; + +export const MockRoleSyncSettings: TypesGen.RoleSyncSettings = { + field: "role-test", + mapping: { + "idp-role-1": ["admin", "developer"], + "idp-role-2": ["auditor"], + }, +}; + export const MockGroup: TypesGen.Group = { id: "fbd2116a-8961-4954-87ae-e4575bd29ce0", name: "Front-End", @@ -2646,6 +2635,20 @@ export const MockGroup: TypesGen.Group = { total_member_count: 2, }; +export const MockGroup2: TypesGen.Group = { + id: "13de3eb4-9b4f-49e7-b0f8-0c3728a0d2e2", + name: "developer", + display_name: "", + avatar_url: "https://example.com", + organization_id: MockOrganization.id, + organization_name: MockOrganization.name, + organization_display_name: MockOrganization.display_name, + members: [MockUser, MockUser2], + quota_allowance: 5, + source: "user", + total_member_count: 2, +}; + const MockEveryoneGroup: TypesGen.Group = { // The "Everyone" group must have the same ID as a the organization it belongs // to. From 6e303f42df0b1a25e03bc06df49697a85236eb89 Mon Sep 17 00:00:00 2001 From: Jaayden Halko Date: Fri, 20 Sep 2024 22:28:52 +0000 Subject: [PATCH 03/13] chore: cleanup --- site/src/api/queries/organizations.ts | 7 +++++ .../IdpSyncPage/IdpSyncPage.tsx | 22 ++++++++++------ .../IdpSyncPage/IdpSyncPageView.tsx | 26 ++++++++++++++++--- .../ManagementSettingsPage/SidebarView.tsx | 2 +- 4 files changed, 45 insertions(+), 12 deletions(-) diff --git a/site/src/api/queries/organizations.ts b/site/src/api/queries/organizations.ts index f0a5cca33cb53..6bd8358022aff 100644 --- a/site/src/api/queries/organizations.ts +++ b/site/src/api/queries/organizations.ts @@ -269,6 +269,13 @@ export const organizationsPermissions = ( }, action: "read", }, + viewIdpSyncSettings: { + object: { + resource_type: "idpsync_settings", + organization_id: organizationId, + }, + action: "read", + }, }); // The endpoint takes a flat array, so to avoid collisions prepend each diff --git a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPage.tsx b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPage.tsx index a647ff80c66d0..dcca449cf9043 100644 --- a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPage.tsx +++ b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPage.tsx @@ -7,6 +7,7 @@ import { organizationsPermissions, roleIdpSyncSettings, } from "api/queries/organizations"; +import { ErrorAlert } from "components/Alert/ErrorAlert"; import { EmptyState } from "components/EmptyState/EmptyState"; import { FeatureStageBadge } from "components/FeatureStageBadge/FeatureStageBadge"; import { Loader } from "components/Loader/Loader"; @@ -27,12 +28,6 @@ export const IdpSyncPage: FC = () => { const { organization: organizationName } = useParams() as { organization: string; }; - - // feature visibility and permissions to be implemented when integrating with backend - // const feats = useFeatureVisibility(); - // const { organization: organizationName } = useParams() as { - // organization: string; - // }; const { organizations } = useOrganizationSettings(); const organization = organizations?.find((o) => o.name === organizationName); @@ -48,8 +43,6 @@ export const IdpSyncPage: FC = () => { roleIdpSyncSettings(organizationName), ); - // const permissions = permissionsQuery.data; - if (!organization) { return ; } @@ -62,6 +55,19 @@ export const IdpSyncPage: FC = () => { return ; } + const error = + groupIdpSyncSettingsQuery.error || + roleIdpSyncSettingsQuery.error || + groupsQuery.error; + if ( + error || + !groupIdpSyncSettingsQuery.data || + !roleIdpSyncSettingsQuery.data || + !groupsQuery.data + ) { + return ; + } + return ( <> diff --git a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.tsx b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.tsx index 424837a356fc2..ff983529ea126 100644 --- a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.tsx +++ b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.tsx @@ -1,5 +1,4 @@ import type { Interpolation, Theme } from "@emotion/react"; -import { useTheme } from "@emotion/react"; import LaunchOutlined from "@mui/icons-material/LaunchOutlined"; import Button from "@mui/material/Button"; import Skeleton from "@mui/material/Skeleton"; @@ -39,8 +38,6 @@ export const IdpSyncPageView: FC = ({ roleSyncSettings, groups, }) => { - // const theme = useTheme(); - const groupsMap = new Map(); if (groups) { for (const group of groups) { @@ -101,6 +98,29 @@ export const IdpSyncPageView: FC = ({
+ {groupSyncSettings?.mapping && roleSyncSettings?.mapping && ( +
({ + margin: 0, + fontSize: 13, + paddingBottom: 14, + color: theme.palette.text.secondary, + "& strong": { + color: theme.palette.text.primary, + }, + })} + > + Showing{" "} + + {Object.entries(groupSyncSettings?.mapping).length} + {" "} + groups and{" "} + + {Object.entries(roleSyncSettings?.mapping).length} + {" "} + provisioners +
+ )} )} - {organization.permissions.editMembers && ( + {organization.permissions.viewIdpSyncSettings && ( From 310726b62b30bb7d3e8220da499057feb8c3291e Mon Sep 17 00:00:00 2001 From: Jaayden Halko Date: Sat, 21 Sep 2024 19:24:33 +0000 Subject: [PATCH 04/13] feat: separate groups and roles into tabs --- .../IdpSyncPage/IdpSyncPage.tsx | 35 +- .../IdpSyncPage/IdpSyncPageView.tsx | 301 ++++++++++-------- 2 files changed, 189 insertions(+), 147 deletions(-) diff --git a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPage.tsx b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPage.tsx index dcca449cf9043..f3ed2c973cf56 100644 --- a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPage.tsx +++ b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPage.tsx @@ -1,10 +1,8 @@ -import AddIcon from "@mui/icons-material/AddOutlined"; import LaunchOutlined from "@mui/icons-material/LaunchOutlined"; import Button from "@mui/material/Button"; import { groupsByOrganization } from "api/queries/groups"; import { groupIdpSyncSettings, - organizationsPermissions, roleIdpSyncSettings, } from "api/queries/organizations"; import { ErrorAlert } from "components/Alert/ErrorAlert"; @@ -13,11 +11,10 @@ import { FeatureStageBadge } from "components/FeatureStageBadge/FeatureStageBadg import { Loader } from "components/Loader/Loader"; import { SettingsHeader } from "components/SettingsHeader/SettingsHeader"; import { Stack } from "components/Stack/Stack"; -import { useDashboard } from "modules/dashboard/useDashboard"; import type { FC } from "react"; import { Helmet } from "react-helmet-async"; -import { useQuery } from "react-query"; -import { Link as RouterLink, useParams } from "react-router-dom"; +import { useQueries } from "react-query"; +import { useParams } from "react-router-dom"; import { docs } from "utils/docs"; import { pageTitle } from "utils/page"; import { useOrganizationSettings } from "../ManagementSettingsLayout"; @@ -29,26 +26,23 @@ export const IdpSyncPage: FC = () => { organization: string; }; const { organizations } = useOrganizationSettings(); - const organization = organizations?.find((o) => o.name === organizationName); - const permissionsQuery = useQuery( - organizationsPermissions(organizations?.map((o) => o.id)), - ); - const groupIdpSyncSettingsQuery = useQuery( - groupIdpSyncSettings(organizationName), - ); - - const groupsQuery = useQuery(groupsByOrganization(organizationName)); - const roleIdpSyncSettingsQuery = useQuery( - roleIdpSyncSettings(organizationName), - ); if (!organization) { return ; } + const [groupIdpSyncSettingsQuery, roleIdpSyncSettingsQuery, groupsQuery] = + useQueries({ + queries: [ + groupIdpSyncSettings(organizationName), + roleIdpSyncSettings(organizationName), + groupsByOrganization(organizationName), + ], + }); + if ( - permissionsQuery.isLoading || + groupsQuery.isLoading || groupIdpSyncSettingsQuery.isLoading || roleIdpSyncSettingsQuery.isLoading ) { @@ -81,7 +75,7 @@ export const IdpSyncPage: FC = () => { > } badges={} /> @@ -94,9 +88,6 @@ export const IdpSyncPage: FC = () => { > Setup IdP Sync - diff --git a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.tsx b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.tsx index ff983529ea126..ae7cc9ce4804b 100644 --- a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.tsx +++ b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.tsx @@ -1,4 +1,5 @@ import type { Interpolation, Theme } from "@emotion/react"; +import AddIcon from "@mui/icons-material/AddOutlined"; import LaunchOutlined from "@mui/icons-material/LaunchOutlined"; import Button from "@mui/material/Button"; import Skeleton from "@mui/material/Skeleton"; @@ -22,7 +23,9 @@ import { TableLoaderSkeleton, TableRowSkeleton, } from "components/TableLoader/TableLoader"; +import { TabLink, Tabs, TabsList } from "components/Tabs/Tabs"; import type { FC } from "react"; +import { useSearchParams } from "react-router-dom"; import { MONOSPACE_FONT_FAMILY } from "theme/constants"; import { docs } from "utils/docs"; import { PillList } from "./PillList"; @@ -38,6 +41,7 @@ export const IdpSyncPageView: FC = ({ roleSyncSettings, groups, }) => { + const [searchParams] = useSearchParams(); const groupsMap = new Map(); if (groups) { for (const group of groups) { @@ -49,6 +53,15 @@ export const IdpSyncPageView: FC = ({ return groupIds.map((groupId) => groupsMap.get(groupId) || groupId); }; + const tab = searchParams.get("tab") || "groups"; + + const groupMappingCount = groupSyncSettings?.mapping + ? Object.entries(groupSyncSettings.mapping).length + : 0; + const roleMappingCount = roleSyncSettings?.mapping + ? Object.entries(roleSyncSettings.mapping).length + : 0; + return ( <> @@ -60,105 +73,124 @@ export const IdpSyncPageView: FC = ({ /> - - {/* Semantically fieldset is used for forms. In the future this screen will allow - updates to these fields in a form */} -
- Groups - - - - - -
-
- Roles - - - -
-
- {groupSyncSettings?.mapping && roleSyncSettings?.mapping && ( -
({ - margin: 0, - fontSize: 13, - paddingBottom: 14, - color: theme.palette.text.secondary, - "& strong": { - color: theme.palette.text.primary, - }, - })} - > - Showing{" "} - - {Object.entries(groupSyncSettings?.mapping).length} - {" "} - groups and{" "} - - {Object.entries(roleSyncSettings?.mapping).length} - {" "} - provisioners -
- )} - - + - {groupSyncSettings?.mapping && - Object.entries(groupSyncSettings.mapping) - .sort() - .map(([idpGroup, groups]) => ( - + + Group Sync Settings + + + Role Sync Settings + + + + {tab === "groups" ? ( + <> +
+ + - ))} - - - {roleSyncSettings?.mapping && - Object.entries(roleSyncSettings.mapping) - .sort() - .map(([idpRole, roles]) => ( - + - ))} - - + +
+ + + + + + + {groupSyncSettings?.mapping && + Object.entries(groupSyncSettings.mapping) + .sort() + .map(([idpGroup, groups]) => ( + + ))} + + + + ) : ( + <> +
+ +
+ + + + + + {roleSyncSettings?.mapping && + Object.entries(roleSyncSettings.mapping) + .sort() + .map(([idpRole, roles]) => ( + + ))} + + + )} +
@@ -178,27 +210,50 @@ const IdpField: FC = ({ }) => { return ( -

{name}

-

- {fieldText || - (showStatusIndicator && ( -

- -

disabled

-
- ))} -

+

{name}

+ {fieldText ? ( +

{fieldText}

+ ) : ( + showStatusIndicator && ( +
+ +

disabled

+
+ ) + )}
); }; +interface TableRowCountProps { + count: number; + type: string; +} + +const TableRowCount: FC = ({ count, type }) => { + return ( +
({ + margin: 0, + fontSize: 13, + color: theme.palette.text.secondary, + "& strong": { + color: theme.palette.text.primary, + }, + })} + > + Showing {count} {type} +
+ ); +}; + interface IdpMappingTableProps { type: "Role" | "Group"; isEmpty: boolean; @@ -237,7 +292,9 @@ const IdpMappingTable: FC = ({ + ); +}; diff --git a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPage.tsx b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPage.tsx index f3ed2c973cf56..a41395be33cb0 100644 --- a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPage.tsx +++ b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPage.tsx @@ -95,6 +95,7 @@ export const IdpSyncPage: FC = () => { groupSyncSettings={groupIdpSyncSettingsQuery.data} roleSyncSettings={roleIdpSyncSettingsQuery.data} groups={groupsQuery.data} + organization={organization} /> ); diff --git a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.tsx b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.tsx index ae7cc9ce4804b..c5c6aceb64aaf 100644 --- a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.tsx +++ b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.tsx @@ -1,5 +1,4 @@ import type { Interpolation, Theme } from "@emotion/react"; -import AddIcon from "@mui/icons-material/AddOutlined"; import LaunchOutlined from "@mui/icons-material/LaunchOutlined"; import Button from "@mui/material/Button"; import Skeleton from "@mui/material/Skeleton"; @@ -12,6 +11,7 @@ import TableRow from "@mui/material/TableRow"; import type { Group, GroupSyncSettings, + Organization, RoleSyncSettings, } from "api/typesGenerated"; import { ChooseOne, Cond } from "components/Conditionals/ChooseOne"; @@ -28,18 +28,21 @@ import type { FC } from "react"; import { useSearchParams } from "react-router-dom"; import { MONOSPACE_FONT_FAMILY } from "theme/constants"; import { docs } from "utils/docs"; +import { ExportPolicyButton } from "./ExportPolicyButton"; import { PillList } from "./PillList"; -export type IdpSyncPageViewProps = { +interface IdpSyncPageViewProps { groupSyncSettings: GroupSyncSettings | undefined; roleSyncSettings: RoleSyncSettings | undefined; groups: Group[] | undefined; -}; + organization: Organization; +} export const IdpSyncPageView: FC = ({ groupSyncSettings, roleSyncSettings, groups, + organization, }) => { const [searchParams] = useSearchParams(); const groupsMap = new Map(); @@ -62,6 +65,15 @@ export const IdpSyncPageView: FC = ({ ? Object.entries(roleSyncSettings.mapping).length : 0; + const rolePolicy = + roleSyncSettings?.field && roleSyncSettings.mapping + ? JSON.stringify(roleSyncSettings, null, 2) + : null; + const groupPolicy = + groupSyncSettings?.field && groupSyncSettings.mapping + ? JSON.stringify(groupSyncSettings, null, 2) + : null; + return ( <> @@ -77,7 +89,7 @@ export const IdpSyncPageView: FC = ({ @@ -121,14 +133,11 @@ export const IdpSyncPageView: FC = ({ css={styles.tableInfo} > - + = ({ css={styles.tableInfo} > - + = ({ }) => { return ( -

{name}

+

{name}

{fieldText ? (

{fieldText}

) : ( @@ -365,12 +371,16 @@ const TableLoader = () => { const styles = { fieldText: (theme) => ({ - color: theme.palette.text.secondary, fontFamily: MONOSPACE_FONT_FAMILY, + whiteSpace: "nowrap", + }), + fieldLabel: (theme) => ({ + color: theme.palette.text.secondary, }), fields: () => ({ - marginBottom: 20, + marginBottom: 16, marginLeft: 16, + fontSize: 14, }), tableInfo: () => ({ marginBottom: 16, From 45f4d1365c2e45e4f829e3a4f00e27eb179716b9 Mon Sep 17 00:00:00 2001 From: Jaayden Halko Date: Sun, 22 Sep 2024 21:29:57 +0000 Subject: [PATCH 06/13] feat: handle missing groups --- .../{PillList.tsx => IdpPillList.tsx} | 20 ++++++++++++++++--- .../IdpSyncPage/IdpSyncPageView.tsx | 6 +++--- 2 files changed, 20 insertions(+), 6 deletions(-) rename site/src/pages/ManagementSettingsPage/IdpSyncPage/{PillList.tsx => IdpPillList.tsx} (77%) diff --git a/site/src/pages/ManagementSettingsPage/IdpSyncPage/PillList.tsx b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpPillList.tsx similarity index 77% rename from site/src/pages/ManagementSettingsPage/IdpSyncPage/PillList.tsx rename to site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpPillList.tsx index 02b64d1c1a8ab..170611786bc5f 100644 --- a/site/src/pages/ManagementSettingsPage/IdpSyncPage/PillList.tsx +++ b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpPillList.tsx @@ -12,11 +12,16 @@ interface PillListProps { roles: readonly string[]; } -export const PillList: FC = ({ roles }) => { +const UUID = + /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; + +export const IdpPillList: FC = ({ roles }) => { return ( {roles.length > 0 ? ( - {roles[0]} + + {roles[0]} + ) : (

None

)} @@ -72,7 +77,10 @@ const OverflowPill: FC = ({ roles }) => { }} > {roles.map((role) => ( - + {role} ))} @@ -88,4 +96,10 @@ const styles = { color: theme.experimental.pillDefault.text, width: "fit-content", }), + errorPill: (theme) => ({ + backgroundColor: theme.roles.error.background, + borderColor: theme.roles.error.outline, + color: theme.roles.error.text, + width: "fit-content", + }), } satisfies Record>; diff --git a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.tsx b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.tsx index c5c6aceb64aaf..e7a37be6e2610 100644 --- a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.tsx +++ b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.tsx @@ -29,7 +29,7 @@ import { useSearchParams } from "react-router-dom"; import { MONOSPACE_FONT_FAMILY } from "theme/constants"; import { docs } from "utils/docs"; import { ExportPolicyButton } from "./ExportPolicyButton"; -import { PillList } from "./PillList"; +import { IdpPillList } from "./IdpPillList"; interface IdpSyncPageViewProps { groupSyncSettings: GroupSyncSettings | undefined; @@ -329,7 +329,7 @@ const GroupRow: FC = ({ idpGroup, coderGroup }) => { {idpGroup} - + ); @@ -345,7 +345,7 @@ const RoleRow: FC = ({ idpRole, coderRoles }) => { {idpRole} - + ); From 080b0329a0390511dc4e2ea96c3d4c71a70cee2c Mon Sep 17 00:00:00 2001 From: Jaayden Halko Date: Sun, 22 Sep 2024 22:23:17 +0000 Subject: [PATCH 07/13] chore: add story for missing groups --- .../IdpSyncPage/IdpSyncPageView.stories.tsx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.stories.tsx b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.stories.tsx index a27dc85e48d79..bd01494daff21 100644 --- a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.stories.tsx +++ b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.stories.tsx @@ -30,3 +30,11 @@ export const Default: Story = { groups: [MockGroup, MockGroup2], }, }; + +export const MissingGroups: Story = { + args: { + groupSyncSettings: MockGroupSyncSettings, + roleSyncSettings: MockRoleSyncSettings, + groups: [], + }, +}; From 8e1e021c2ab7634d146e6c0535ae234350f3068e Mon Sep 17 00:00:00 2001 From: Jaayden Halko Date: Sun, 22 Sep 2024 22:24:04 +0000 Subject: [PATCH 08/13] chore: add stories for export policy button --- .../ExportPolicyButton.stories.tsx | 69 +++++++++++++++++++ .../IdpSyncPage/ExportPolicyButton.tsx | 4 +- 2 files changed, 72 insertions(+), 1 deletion(-) create mode 100644 site/src/pages/ManagementSettingsPage/IdpSyncPage/ExportPolicyButton.stories.tsx diff --git a/site/src/pages/ManagementSettingsPage/IdpSyncPage/ExportPolicyButton.stories.tsx b/site/src/pages/ManagementSettingsPage/IdpSyncPage/ExportPolicyButton.stories.tsx new file mode 100644 index 0000000000000..e3bbd66ea02aa --- /dev/null +++ b/site/src/pages/ManagementSettingsPage/IdpSyncPage/ExportPolicyButton.stories.tsx @@ -0,0 +1,69 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { expect, fn, userEvent, waitFor, within } from "@storybook/test"; +import { + MockGroupSyncSettings, + MockOrganization, + MockRoleSyncSettings, +} from "testHelpers/entities"; +import { ExportPolicyButton } from "./ExportPolicyButton"; + +const meta: Meta = { + title: "modules/resources/ExportPolicyButton", + component: ExportPolicyButton, + args: { + policy: JSON.stringify(MockGroupSyncSettings, null, 2), + type: "groups", + organization: MockOrganization, + }, +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = {}; + +export const ClickExportGroupPolicy: Story = { + args: { + policy: JSON.stringify(MockGroupSyncSettings, null, 2), + type: "groups", + organization: MockOrganization, + download: fn(), + }, + play: async ({ canvasElement, args }) => { + const canvas = within(canvasElement); + await userEvent.click( + canvas.getByRole("button", { name: "Export Policy" }), + ); + await waitFor(() => + expect(args.download).toHaveBeenCalledWith( + expect.anything(), + `${MockOrganization.name}_groups-policy.json`, + ), + ); + const blob: Blob = (args.download as jest.Mock).mock.calls[0][0]; + await expect(blob.type).toEqual("application/json"); + }, +}; + +export const ClickExportRolePolicy: Story = { + args: { + policy: JSON.stringify(MockRoleSyncSettings, null, 2), + type: "roles", + organization: MockOrganization, + download: fn(), + }, + play: async ({ canvasElement, args }) => { + const canvas = within(canvasElement); + await userEvent.click( + canvas.getByRole("button", { name: "Export Policy" }), + ); + await waitFor(() => + expect(args.download).toHaveBeenCalledWith( + expect.anything(), + `${MockOrganization.name}_roles-policy.json`, + ), + ); + const blob: Blob = (args.download as jest.Mock).mock.calls[0][0]; + await expect(blob.type).toEqual("application/json"); + }, +}; diff --git a/site/src/pages/ManagementSettingsPage/IdpSyncPage/ExportPolicyButton.tsx b/site/src/pages/ManagementSettingsPage/IdpSyncPage/ExportPolicyButton.tsx index a6eb4fa25b36f..6bdc74c5d1323 100644 --- a/site/src/pages/ManagementSettingsPage/IdpSyncPage/ExportPolicyButton.tsx +++ b/site/src/pages/ManagementSettingsPage/IdpSyncPage/ExportPolicyButton.tsx @@ -9,12 +9,14 @@ interface DownloadPolicyButtonProps { policy: string | null; type: "groups" | "roles"; organization: Organization; + download?: (file: Blob, filename: string) => void; } export const ExportPolicyButton: FC = ({ policy, type, organization, + download = saveAs, }) => { const [isDownloading, setIsDownloading] = useState(false); @@ -29,7 +31,7 @@ export const ExportPolicyButton: FC = ({ const file = new Blob([policy], { type: "application/json", }); - saveAs(file, `${organization.name}_${type}-policy.json`); + download(file, `${organization.name}_${type}-policy.json`); } catch (e) { console.error(e); displayError("Failed to export policy json"); From c8636195cb3716f81444e219dba1947e0c8af4c0 Mon Sep 17 00:00:00 2001 From: Jaayden Halko Date: Mon, 23 Sep 2024 20:30:44 +0000 Subject: [PATCH 09/13] fix: updates for PR review --- site/src/api/api.ts | 3 ++ site/src/api/queries/organizations.ts | 4 +- .../ExportPolicyButton.stories.tsx | 6 +-- .../IdpSyncPage/ExportPolicyButton.tsx | 24 ++++++--- .../IdpSyncPage/IdpPillList.tsx | 4 +- .../IdpSyncPage/IdpSyncPage.tsx | 16 ++++-- .../IdpSyncPage/IdpSyncPageView.tsx | 54 ++++++++----------- 7 files changed, 60 insertions(+), 51 deletions(-) diff --git a/site/src/api/api.ts b/site/src/api/api.ts index b44b03c1a5456..103a3c50e7900 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -704,6 +704,9 @@ class ApiMethods { return response.data; }; + /** + * @param organization Can be the organization's ID or name + */ getGroupIdpSyncSettingsByOrganization = async ( organization: string, ): Promise => { diff --git a/site/src/api/queries/organizations.ts b/site/src/api/queries/organizations.ts index 6bd8358022aff..d1df8f409dcdf 100644 --- a/site/src/api/queries/organizations.ts +++ b/site/src/api/queries/organizations.ts @@ -142,7 +142,7 @@ export const provisionerDaemonGroups = (organization: string) => { }; export const getGroupIdpSyncSettingsKey = (organization: string) => [ - "organization", + "organizations", organization, "groupIdpSyncSettings", ]; @@ -155,7 +155,7 @@ export const groupIdpSyncSettings = (organization: string) => { }; export const getRoleIdpSyncSettingsKey = (organization: string) => [ - "organization", + "organizations", organization, "roleIdpSyncSettings", ]; diff --git a/site/src/pages/ManagementSettingsPage/IdpSyncPage/ExportPolicyButton.stories.tsx b/site/src/pages/ManagementSettingsPage/IdpSyncPage/ExportPolicyButton.stories.tsx index e3bbd66ea02aa..0da470de6a580 100644 --- a/site/src/pages/ManagementSettingsPage/IdpSyncPage/ExportPolicyButton.stories.tsx +++ b/site/src/pages/ManagementSettingsPage/IdpSyncPage/ExportPolicyButton.stories.tsx @@ -11,7 +11,7 @@ const meta: Meta = { title: "modules/resources/ExportPolicyButton", component: ExportPolicyButton, args: { - policy: JSON.stringify(MockGroupSyncSettings, null, 2), + syncSettings: MockGroupSyncSettings, type: "groups", organization: MockOrganization, }, @@ -24,7 +24,7 @@ export const Default: Story = {}; export const ClickExportGroupPolicy: Story = { args: { - policy: JSON.stringify(MockGroupSyncSettings, null, 2), + syncSettings: MockGroupSyncSettings, type: "groups", organization: MockOrganization, download: fn(), @@ -47,7 +47,7 @@ export const ClickExportGroupPolicy: Story = { export const ClickExportRolePolicy: Story = { args: { - policy: JSON.stringify(MockRoleSyncSettings, null, 2), + syncSettings: MockRoleSyncSettings, type: "roles", organization: MockOrganization, download: fn(), diff --git a/site/src/pages/ManagementSettingsPage/IdpSyncPage/ExportPolicyButton.tsx b/site/src/pages/ManagementSettingsPage/IdpSyncPage/ExportPolicyButton.tsx index 6bdc74c5d1323..9cb4cb06e7385 100644 --- a/site/src/pages/ManagementSettingsPage/IdpSyncPage/ExportPolicyButton.tsx +++ b/site/src/pages/ManagementSettingsPage/IdpSyncPage/ExportPolicyButton.tsx @@ -1,34 +1,44 @@ import DownloadOutlined from "@mui/icons-material/DownloadOutlined"; import Button from "@mui/material/Button"; -import type { Organization } from "api/typesGenerated"; +import type { + GroupSyncSettings, + Organization, + RoleSyncSettings, +} from "api/typesGenerated"; import { displayError } from "components/GlobalSnackbar/utils"; import { saveAs } from "file-saver"; -import { type FC, useState } from "react"; +import { type FC, useMemo, useState } from "react"; interface DownloadPolicyButtonProps { - policy: string | null; + syncSettings: RoleSyncSettings | GroupSyncSettings | undefined; type: "groups" | "roles"; organization: Organization; download?: (file: Blob, filename: string) => void; } export const ExportPolicyButton: FC = ({ - policy, + syncSettings, type, organization, download = saveAs, }) => { const [isDownloading, setIsDownloading] = useState(false); + const policyJSON = useMemo(() => { + return syncSettings?.field && syncSettings.mapping + ? JSON.stringify(syncSettings, null, 2) + : null; + }, [syncSettings]); + return (