From 96b0c098c620c51089838359f4cbcb7fb32363c9 Mon Sep 17 00:00:00 2001 From: Jaayden Halko Date: Fri, 10 Jan 2025 12:20:30 +0000 Subject: [PATCH 01/40] chore: remove stack component --- .../IdpSyncPage/IdpSyncPage.tsx | 13 +++---- .../IdpSyncPage/IdpSyncPageView.tsx | 39 ++++++++----------- 2 files changed, 22 insertions(+), 30 deletions(-) diff --git a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPage.tsx b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPage.tsx index fd1375405fda4..7be557c9f4b81 100644 --- a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPage.tsx +++ b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPage.tsx @@ -1,6 +1,7 @@ import LaunchOutlined from "@mui/icons-material/LaunchOutlined"; import Button from "@mui/material/Button"; import { groupsByOrganization } from "api/queries/groups"; +import { organizationRoles } from "api/queries/roles"; import { groupIdpSyncSettings, roleIdpSyncSettings, @@ -9,7 +10,6 @@ import { ChooseOne, Cond } from "components/Conditionals/ChooseOne"; import { EmptyState } from "components/EmptyState/EmptyState"; import { Paywall } from "components/Paywall/Paywall"; import { SettingsHeader } from "components/SettingsHeader/SettingsHeader"; -import { Stack } from "components/Stack/Stack"; import { useFeatureVisibility } from "modules/dashboard/useFeatureVisibility"; import { useOrganizationSettings } from "modules/management/OrganizationSettingsLayout"; import type { FC } from "react"; @@ -30,12 +30,13 @@ export const IdpSyncPage: FC = () => { const { organizations } = useOrganizationSettings(); const organization = organizations?.find((o) => o.name === organizationName); - const [groupIdpSyncSettingsQuery, roleIdpSyncSettingsQuery, groupsQuery] = + const [groupIdpSyncSettingsQuery, roleIdpSyncSettingsQuery, groupsQuery, rolesQuery] = useQueries({ queries: [ groupIdpSyncSettings(organizationName), roleIdpSyncSettings(organizationName), groupsByOrganization(organizationName), + organizationRoles(organizationName), ], }); @@ -61,10 +62,7 @@ export const IdpSyncPage: FC = () => { {pageTitle("IdP Sync")} - { > Setup IdP Sync - + { roleSyncSettings={roleIdpSyncSettingsQuery.data} groups={groupsQuery.data} groupsMap={groupsMap} + roles={rolesQuery.data} organization={organization} error={error} /> diff --git a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.tsx b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.tsx index c1e769af8f617..ef92d18673d16 100644 --- a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.tsx +++ b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.tsx @@ -13,6 +13,7 @@ import type { Group, GroupSyncSettings, Organization, + Role, RoleSyncSettings, } from "api/typesGenerated"; import { ErrorAlert } from "components/Alert/ErrorAlert"; @@ -26,7 +27,6 @@ import { HelpTooltipTrigger, } from "components/HelpTooltip/HelpTooltip"; import { Loader } from "components/Loader/Loader"; -import { Stack } from "components/Stack/Stack"; import { StatusIndicator } from "components/StatusIndicator/StatusIndicator"; import { TableLoaderSkeleton, @@ -45,6 +45,7 @@ interface IdpSyncPageViewProps { roleSyncSettings: RoleSyncSettings | undefined; groups: Group[] | undefined; groupsMap: Map; + roles: Role[] | undefined; organization: Organization; error?: unknown; } @@ -54,6 +55,7 @@ export const IdpSyncPageView: FC = ({ roleSyncSettings, groups, groupsMap, + roles, organization, error, }) => { @@ -85,7 +87,7 @@ export const IdpSyncPageView: FC = ({ return ( <> - +
@@ -99,7 +101,7 @@ export const IdpSyncPageView: FC = ({ {tab === "groups" ? ( <>
- +
= ({ : "n/a" } /> - +
- +
- - +
+
{groupSyncSettings?.mapping && Object.entries(groupSyncSettings.mapping) @@ -168,7 +165,7 @@ export const IdpSyncPageView: FC = ({ )} - +
) : ( <> @@ -179,19 +176,15 @@ export const IdpSyncPageView: FC = ({ showDisabled />
- +
- +
{roleSyncSettings?.mapping && Object.entries(roleSyncSettings.mapping) @@ -206,7 +199,7 @@ export const IdpSyncPageView: FC = ({ )} -
+ ); }; @@ -389,7 +382,7 @@ const LegacyGroupSyncHeader: FC = () => { fontWeight: 500, }} > - +
Legacy Group Sync Settings @@ -407,7 +400,7 @@ const LegacyGroupSyncHeader: FC = () => { - +
); }; From a88b72f6df4a09db6c50ed8bb8c5bafc1c648115 Mon Sep 17 00:00:00 2001 From: Jaayden Halko Date: Fri, 10 Jan 2025 14:01:52 +0000 Subject: [PATCH 02/40] chore: styling cleanup --- site/src/components/Tabs/Tabs.tsx | 57 ++-------- .../IdpSyncPage/IdpSyncHelpTooltip.tsx | 31 ------ .../IdpSyncPage/IdpSyncPage.tsx | 102 +++++++++--------- .../IdpSyncPage/IdpSyncPageView.tsx | 3 +- 4 files changed, 64 insertions(+), 129 deletions(-) delete mode 100644 site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncHelpTooltip.tsx diff --git a/site/src/components/Tabs/Tabs.tsx b/site/src/components/Tabs/Tabs.tsx index ebeaa762674ad..e351dbcca6911 100644 --- a/site/src/components/Tabs/Tabs.tsx +++ b/site/src/components/Tabs/Tabs.tsx @@ -1,6 +1,6 @@ -import { type Interpolation, type Theme, useTheme } from "@emotion/react"; import { type FC, type HTMLAttributes, createContext, useContext } from "react"; import { Link, type LinkProps } from "react-router-dom"; +import { cn } from "utils/cn"; export const TAB_PADDING_Y = 12; export const TAB_PADDING_X = 16; @@ -14,14 +14,10 @@ const TabsContext = createContext(undefined); type TabsProps = HTMLAttributes & TabsContextValue; export const Tabs: FC = ({ active, ...htmlProps }) => { - const theme = useTheme(); - return (
@@ -31,16 +27,7 @@ export const Tabs: FC = ({ active, ...htmlProps }) => { type TabsListProps = HTMLAttributes; export const TabsList: FC = (props) => { - return ( -
- ); + return
; }; type TabLinkProps = LinkProps & { @@ -59,37 +46,13 @@ export const TabLink: FC = ({ value, ...linkProps }) => { return ( ); }; - -const styles = { - tabLink: (theme) => ({ - textDecoration: "none", - color: theme.palette.text.secondary, - fontSize: 14, - display: "block", - padding: `${TAB_PADDING_Y}px ${TAB_PADDING_X}px`, - fontWeight: 500, - lineHeight: "1", - - "&:hover": { - color: theme.palette.text.primary, - }, - }), - activeTabLink: (theme) => ({ - color: theme.palette.text.primary, - position: "relative", - - "&:before": { - content: '""', - left: 0, - bottom: -1, - height: 1, - width: "100%", - background: theme.palette.primary.main, - position: "absolute", - }, - }), -} satisfies Record>; diff --git a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncHelpTooltip.tsx b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncHelpTooltip.tsx deleted file mode 100644 index b2484cf2349ce..0000000000000 --- a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncHelpTooltip.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import { - HelpTooltip, - HelpTooltipContent, - HelpTooltipLink, - HelpTooltipLinksGroup, - HelpTooltipText, - HelpTooltipTitle, - HelpTooltipTrigger, -} from "components/HelpTooltip/HelpTooltip"; -import type { FC } from "react"; -import { docs } from "utils/docs"; - -export const IdpSyncHelpTooltip: FC = () => { - return ( - - - - What is IdP Sync? - - View the current mappings between your external OIDC provider and - Coder. Use the Coder CLI to configure these mappings. - - - - Configure IdP Sync - - - - - ); -}; diff --git a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPage.tsx b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPage.tsx index 7be557c9f4b81..43a60f471d2d8 100644 --- a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPage.tsx +++ b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPage.tsx @@ -1,15 +1,13 @@ -import LaunchOutlined from "@mui/icons-material/LaunchOutlined"; -import Button from "@mui/material/Button"; import { groupsByOrganization } from "api/queries/groups"; -import { organizationRoles } from "api/queries/roles"; import { groupIdpSyncSettings, roleIdpSyncSettings, } from "api/queries/organizations"; +import { organizationRoles } from "api/queries/roles"; import { ChooseOne, Cond } from "components/Conditionals/ChooseOne"; import { EmptyState } from "components/EmptyState/EmptyState"; import { Paywall } from "components/Paywall/Paywall"; -import { SettingsHeader } from "components/SettingsHeader/SettingsHeader"; +import { SquareArrowOutUpRight } from "lucide-react"; import { useFeatureVisibility } from "modules/dashboard/useFeatureVisibility"; import { useOrganizationSettings } from "modules/management/OrganizationSettingsLayout"; import type { FC } from "react"; @@ -18,7 +16,6 @@ import { useQueries } from "react-query"; import { useParams } from "react-router-dom"; import { docs } from "utils/docs"; import { pageTitle } from "utils/page"; -import { IdpSyncHelpTooltip } from "./IdpSyncHelpTooltip"; import IdpSyncPageView from "./IdpSyncPageView"; export const IdpSyncPage: FC = () => { @@ -30,15 +27,19 @@ export const IdpSyncPage: FC = () => { const { organizations } = useOrganizationSettings(); const organization = organizations?.find((o) => o.name === organizationName); - const [groupIdpSyncSettingsQuery, roleIdpSyncSettingsQuery, groupsQuery, rolesQuery] = - useQueries({ - queries: [ - groupIdpSyncSettings(organizationName), - roleIdpSyncSettings(organizationName), - groupsByOrganization(organizationName), - organizationRoles(organizationName), - ], - }); + const [ + groupIdpSyncSettingsQuery, + roleIdpSyncSettingsQuery, + groupsQuery, + rolesQuery, + ] = useQueries({ + queries: [ + groupIdpSyncSettings(organizationName), + roleIdpSyncSettings(organizationName), + groupsByOrganization(organizationName), + organizationRoles(organizationName), + ], + }); if (!organization) { return ; @@ -62,42 +63,45 @@ export const IdpSyncPage: FC = () => { {pageTitle("IdP Sync")} -
- } - /> - +
+
+
+

IdP Sync

+

+ Automatically assign groups or roles to a user based on their IdP + claims. + + View docs + + +

+
+ {/* */} +
+ + + + + + + +
- - - - - - - - ); }; diff --git a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.tsx b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.tsx index ef92d18673d16..75d1ed752c33c 100644 --- a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.tsx +++ b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.tsx @@ -176,8 +176,7 @@ export const IdpSyncPageView: FC = ({ showDisabled />
-
+
Date: Fri, 10 Jan 2025 17:03:40 +0000 Subject: [PATCH 03/40] feat: add form and mutation queries --- site/src/api/api.ts | 30 ++ site/src/api/queries/organizations.ts | 28 ++ .../IdpSyncPage/IdpSyncPage.tsx | 69 +++- .../IdpSyncPage/IdpSyncPageView.tsx | 313 ++++++++++++------ 4 files changed, 329 insertions(+), 111 deletions(-) diff --git a/site/src/api/api.ts b/site/src/api/api.ts index ac4ef4a1ca340..5fbe8369fac83 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -733,6 +733,36 @@ class ApiMethods { return response.data; }; + /** + * @param data + * @param organization Can be the organization's ID or name + */ + patchGroupIdpSyncSettings = async ( + data: TypesGen.GroupSyncSettings, + organization: string, + ) => { + const response = await this.axios.patch( + `/api/v2/organizations/${organization}/settings/idpsync/groups"`, + data, + ); + return response.data; + }; + + /** + * @param data + * @param organization Can be the organization's ID or name + */ + patchRoleIdpSyncSettings = async ( + data: TypesGen.RoleSyncSettings, + organization: string, + ) => { + const response = await this.axios.patch( + `/api/v2/organizations/${organization}/settings/idpsync/roles"`, + data, + ); + return response.data; + }; + /** * @param organization Can be the organization's ID or name */ diff --git a/site/src/api/queries/organizations.ts b/site/src/api/queries/organizations.ts index c3f5a4ebd3ced..0cc8168243c16 100644 --- a/site/src/api/queries/organizations.ts +++ b/site/src/api/queries/organizations.ts @@ -2,6 +2,8 @@ import { API } from "api/api"; import type { AuthorizationResponse, CreateOrganizationRequest, + GroupSyncSettings, + RoleSyncSettings, UpdateOrganizationRequest, } from "api/typesGenerated"; import type { QueryClient } from "react-query"; @@ -156,6 +158,18 @@ export const groupIdpSyncSettings = (organization: string) => { }; }; +export const patchGroupSyncSettings = ( + organization: string, + queryClient: QueryClient, +) => { + return { + mutationFn: (request: GroupSyncSettings) => + API.patchGroupIdpSyncSettings(request, organization), + onSuccess: async () => + await queryClient.invalidateQueries(groupIdpSyncSettings(organization)), + }; +}; + export const getRoleIdpSyncSettingsKey = (organization: string) => [ "organizations", organization, @@ -169,6 +183,20 @@ export const roleIdpSyncSettings = (organization: string) => { }; }; +export const patchRoleSyncSettings = ( + organization: string, + queryClient: QueryClient, +) => { + return { + mutationFn: (request: RoleSyncSettings) => + API.patchRoleIdpSyncSettings(request, organization), + onSuccess: async () => + await queryClient.invalidateQueries( + getRoleIdpSyncSettingsKey(organization), + ), + }; +}; + /** * Fetch permissions for a single organization. * diff --git a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPage.tsx b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPage.tsx index 43a60f471d2d8..5cf27edf16968 100644 --- a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPage.tsx +++ b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPage.tsx @@ -1,24 +1,30 @@ +import { getErrorMessage } from "api/errors"; import { groupsByOrganization } from "api/queries/groups"; import { groupIdpSyncSettings, + patchGroupSyncSettings, + patchRoleSyncSettings, roleIdpSyncSettings, } from "api/queries/organizations"; import { organizationRoles } from "api/queries/roles"; import { ChooseOne, Cond } from "components/Conditionals/ChooseOne"; import { EmptyState } from "components/EmptyState/EmptyState"; +import { displayError } from "components/GlobalSnackbar/utils"; +import { displaySuccess } from "components/GlobalSnackbar/utils"; import { Paywall } from "components/Paywall/Paywall"; import { SquareArrowOutUpRight } from "lucide-react"; import { useFeatureVisibility } from "modules/dashboard/useFeatureVisibility"; import { useOrganizationSettings } from "modules/management/OrganizationSettingsLayout"; -import type { FC } from "react"; +import { type FC, useEffect } from "react"; import { Helmet } from "react-helmet-async"; -import { useQueries } from "react-query"; +import { useMutation, useQueries, useQueryClient } from "react-query"; import { useParams } from "react-router-dom"; import { docs } from "utils/docs"; import { pageTitle } from "utils/page"; import IdpSyncPageView from "./IdpSyncPageView"; export const IdpSyncPage: FC = () => { + const queryClient = useQueryClient(); const { organization: organizationName } = useParams() as { organization: string; }; @@ -45,10 +51,41 @@ export const IdpSyncPage: FC = () => { return ; } + const patchGroupSyncSettingsMutation = useMutation( + patchGroupSyncSettings(organizationName, queryClient), + ); + const patchRoleSyncSettingsMutation = useMutation( + patchRoleSyncSettings(organizationName, queryClient), + ); + + useEffect(() => { + if (patchGroupSyncSettingsMutation.error) { + displayError( + getErrorMessage( + patchGroupSyncSettingsMutation.error, + "Error updating IdP group sync settings.", + ), + ); + } + }, [patchGroupSyncSettingsMutation.error]); + + useEffect(() => { + if (patchRoleSyncSettingsMutation.error) { + displayError( + getErrorMessage( + patchRoleSyncSettingsMutation.error, + "Error updating IdP role sync settings.", + ), + ); + } + }, [patchRoleSyncSettingsMutation.error]); + const error = groupIdpSyncSettingsQuery.error || roleIdpSyncSettingsQuery.error || - groupsQuery.error; + groupsQuery.error || + patchGroupSyncSettingsMutation.error || + patchRoleSyncSettingsMutation.error; const groupsMap = new Map(); if (groupsQuery.data) { @@ -98,6 +135,32 @@ export const IdpSyncPage: FC = () => { roles={rolesQuery.data} organization={organization} error={error} + onSubmitGroupSyncSettings={async (data) => { + try { + await patchGroupSyncSettingsMutation.mutateAsync(data); + displaySuccess("IdP Group sync settings updated."); + } catch (error) { + displayError( + getErrorMessage( + error, + "Failed to update IdP group sync settings", + ), + ); + } + }} + onSubmitRoleSyncSettings={async (data) => { + try { + await patchRoleSyncSettingsMutation.mutateAsync(data); + displaySuccess("IdP Role sync settings updated."); + } catch (error) { + displayError( + getErrorMessage( + error, + "Failed to update IdP role sync settings", + ), + ); + } + }} /> diff --git a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.tsx b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.tsx index 75d1ed752c33c..92b6ce631a0b5 100644 --- a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.tsx +++ b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.tsx @@ -33,10 +33,12 @@ import { TableRowSkeleton, } from "components/TableLoader/TableLoader"; import { TabLink, Tabs, TabsList } from "components/Tabs/Tabs"; +import { useFormik } from "formik"; import type { FC } from "react"; import { useSearchParams } from "react-router-dom"; import { MONOSPACE_FONT_FAMILY } from "theme/constants"; import { docs } from "utils/docs"; +import * as Yup from "yup"; import { ExportPolicyButton } from "./ExportPolicyButton"; import { IdpPillList } from "./IdpPillList"; @@ -48,6 +50,8 @@ interface IdpSyncPageViewProps { roles: Role[] | undefined; organization: Organization; error?: unknown; + onSubmitGroupSyncSettings: (data: GroupSyncSettings) => void; + onSubmitRoleSyncSettings: (data: RoleSyncSettings) => void; } export const IdpSyncPageView: FC = ({ @@ -58,15 +62,11 @@ export const IdpSyncPageView: FC = ({ roles, organization, error, + onSubmitGroupSyncSettings, + onSubmitRoleSyncSettings, }) => { const [searchParams] = useSearchParams(); - - const getGroupNames = (groupIds: readonly string[]) => { - return groupIds.map((groupId) => groupsMap.get(groupId) || groupId); - }; - const tab = searchParams.get("tab") || "groups"; - const groupMappingCount = groupSyncSettings?.mapping ? Object.entries(groupSyncSettings.mapping).length : 0; @@ -99,104 +99,21 @@ export const IdpSyncPageView: FC = ({ {tab === "groups" ? ( - <> -
-
- - - -
-
-
- - -
-
- - {groupSyncSettings?.mapping && - Object.entries(groupSyncSettings.mapping) - .sort() - .map(([idpGroup, groups]) => ( - - ))} - - {groupSyncSettings?.legacy_group_name_mapping && ( -
- - - {Object.entries(groupSyncSettings.legacy_group_name_mapping) - .sort() - .map(([idpGroup, groupId]) => ( - - ))} - -
- )} -
- + ) : ( - <> -
- -
-
- - -
- - {roleSyncSettings?.mapping && - Object.entries(roleSyncSettings.mapping) - .sort() - .map(([idpRole, roles]) => ( - - ))} - - + )}
@@ -323,11 +240,6 @@ const IdpMappingTable: FC = ({ ); }; -interface GroupRowProps { - idpGroup: string; - coderGroup: readonly string[]; -} - const GroupRow: FC = ({ idpGroup, coderGroup }) => { return ( @@ -339,6 +251,191 @@ const GroupRow: FC = ({ idpGroup, coderGroup }) => { ); }; +interface IdpGroupSyncFormProps { + groupSyncSettings: GroupSyncSettings; + groupsMap: Map; + groupMappingCount: number; + legacyGroupMappingCount: number; + organization: Organization; + onSubmit: (data: GroupSyncSettings) => void; +} + +const groupSyncValidationSchema = Yup.object({ + field: Yup.string().trim(), + regex_filter: Yup.string().trim(), + auto_create_missing_groups: Yup.boolean(), + mapping: Yup.object().shape({ + [`${String}`]: Yup.array().of(Yup.string()), + }), +}); + +const IdpGroupSyncForm = ({ + groupSyncSettings, + groupMappingCount, + legacyGroupMappingCount, + groupsMap, + organization, + onSubmit, +}: IdpGroupSyncFormProps) => { + const form = useFormik({ + initialValues: { + field: groupSyncSettings?.field ?? "", + regex_filter: groupSyncSettings?.regex_filter ?? "", + auto_create_missing_groups: + groupSyncSettings?.auto_create_missing_groups ?? false, + mapping: groupSyncSettings?.mapping ?? {}, + }, + validationSchema: groupSyncValidationSchema, + onSubmit, + enableReinitialize: Boolean(groupSyncSettings), + }); + + const getGroupNames = (groupIds: readonly string[]) => { + return groupIds.map((groupId) => groupsMap.get(groupId) || groupId); + }; + + return ( +
+
+
+
+ + + +
+
+
+ + +
+
+ + {groupSyncSettings?.mapping && + Object.entries(groupSyncSettings.mapping) + .sort() + .map(([idpGroup, groups]) => ( + + ))} + + {groupSyncSettings?.legacy_group_name_mapping && ( +
+ + + {Object.entries(groupSyncSettings.legacy_group_name_mapping) + .sort() + .map(([idpGroup, groupId]) => ( + + ))} + +
+ )} +
+
+
+ ); +}; + +interface IdpRoleSyncFormProps { + roleSyncSettings: RoleSyncSettings; + roleMappingCount: number; + organization: Organization; + onSubmit: (data: RoleSyncSettings) => void; +} + +const roleyncValidationSchema = Yup.object({ + field: Yup.string().trim(), + regex_filter: Yup.string().trim(), + auto_create_missing_groups: Yup.boolean(), + mapping: Yup.object().shape({ + [`${String}`]: Yup.array().of(Yup.string()), + }), +}); + +const IdpRoleSyncForm = ({ + roleSyncSettings, + roleMappingCount, + organization, + onSubmit, +}: IdpRoleSyncFormProps) => { + const form = useFormik({ + initialValues: { + field: roleSyncSettings?.field ?? "", + mapping: roleSyncSettings?.mapping ?? {}, + }, + validationSchema: roleyncValidationSchema, + onSubmit, + enableReinitialize: Boolean(roleSyncSettings), + }); + + return ( +
+
+
+ +
+
+ + +
+ + {roleSyncSettings?.mapping && + Object.entries(roleSyncSettings.mapping) + .sort() + .map(([idpRole, roles]) => ( + + ))} + +
+
+ ); +}; + +interface GroupRowProps { + idpGroup: string; + coderGroup: readonly string[]; +} + interface RoleRowProps { idpRole: string; coderRoles: readonly string[]; From 4b15a2af80202b9ab97615fb26be270d1a914fee Mon Sep 17 00:00:00 2001 From: Jaayden Halko Date: Sun, 12 Jan 2025 19:01:32 +0000 Subject: [PATCH 04/40] feat: add form fields --- site/src/api/api.ts | 4 +- .../IdpSyncPage/IdpSyncPageView.tsx | 447 ++++++++++++------ 2 files changed, 313 insertions(+), 138 deletions(-) diff --git a/site/src/api/api.ts b/site/src/api/api.ts index 5fbe8369fac83..26491efb10565 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -742,7 +742,7 @@ class ApiMethods { organization: string, ) => { const response = await this.axios.patch( - `/api/v2/organizations/${organization}/settings/idpsync/groups"`, + `/api/v2/organizations/${organization}/settings/idpsync/groups`, data, ); return response.data; @@ -757,7 +757,7 @@ class ApiMethods { organization: string, ) => { const response = await this.axios.patch( - `/api/v2/organizations/${organization}/settings/idpsync/roles"`, + `/api/v2/organizations/${organization}/settings/idpsync/roles`, data, ); return response.data; diff --git a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.tsx b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.tsx index 92b6ce631a0b5..dcc86b91df0ae 100644 --- a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.tsx +++ b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.tsx @@ -1,6 +1,3 @@ -import type { Interpolation, Theme } from "@emotion/react"; -import LaunchOutlined from "@mui/icons-material/LaunchOutlined"; -import Button from "@mui/material/Button"; import Link from "@mui/material/Link"; import Skeleton from "@mui/material/Skeleton"; import Table from "@mui/material/Table"; @@ -17,6 +14,7 @@ import type { RoleSyncSettings, } from "api/typesGenerated"; import { ErrorAlert } from "components/Alert/ErrorAlert"; +import { Button } from "components/Button/Button"; import { ChooseOne, Cond } from "components/Conditionals/ChooseOne"; import { EmptyState } from "components/EmptyState/EmptyState"; import { @@ -26,17 +24,23 @@ import { HelpTooltipTitle, HelpTooltipTrigger, } from "components/HelpTooltip/HelpTooltip"; +import { Input } from "components/Input/Input"; +import { Label } from "components/Label/Label"; import { Loader } from "components/Loader/Loader"; -import { StatusIndicator } from "components/StatusIndicator/StatusIndicator"; +import { + MultiSelectCombobox, + type Option, +} from "components/MultiSelectCombobox/MultiSelectCombobox"; +import { Switch } from "components/Switch/Switch"; import { TableLoaderSkeleton, TableRowSkeleton, } from "components/TableLoader/TableLoader"; import { TabLink, Tabs, TabsList } from "components/Tabs/Tabs"; import { useFormik } from "formik"; -import type { FC } from "react"; +import { Plus, SquareArrowOutUpRight, Trash } from "lucide-react"; +import { type FC, useState } from "react"; import { useSearchParams } from "react-router-dom"; -import { MONOSPACE_FONT_FAMILY } from "theme/constants"; import { docs } from "utils/docs"; import * as Yup from "yup"; import { ExportPolicyButton } from "./ExportPolicyButton"; @@ -87,7 +91,7 @@ export const IdpSyncPageView: FC = ({ return ( <> -
+
@@ -103,6 +107,7 @@ export const IdpSyncPageView: FC = ({ groupSyncSettings={groupSyncSettings} groupMappingCount={groupMappingCount} legacyGroupMappingCount={legacyGroupMappingCount} + groups={groups} groupsMap={groupsMap} organization={organization} onSubmit={onSubmitGroupSyncSettings} @@ -111,6 +116,7 @@ export const IdpSyncPageView: FC = ({ @@ -120,47 +126,6 @@ export const IdpSyncPageView: FC = ({ ); }; -interface IdpFieldProps { - name: string; - fieldText: string | undefined; - showDisabled?: boolean; -} - -const IdpField: FC = ({ - name, - fieldText, - showDisabled = false, -}) => { - return ( - -

{name}

- {fieldText ? ( -

{fieldText}

- ) : ( - showDisabled && ( -
- -

disabled

-
- ) - )} -
- ); -}; - interface TableRowCountProps { count: number; type: string; @@ -218,13 +183,14 @@ const IdpMappingTable: FC = ({ message={`No ${type} Mappings`} isCompact cta={ - } /> @@ -254,6 +220,7 @@ const GroupRow: FC = ({ idpGroup, coderGroup }) => { interface IdpGroupSyncFormProps { groupSyncSettings: GroupSyncSettings; groupsMap: Map; + groups: Group[]; groupMappingCount: number; legacyGroupMappingCount: number; organization: Organization; @@ -273,6 +240,7 @@ const IdpGroupSyncForm = ({ groupSyncSettings, groupMappingCount, legacyGroupMappingCount, + groups, groupsMap, organization, onSubmit, @@ -289,60 +257,170 @@ const IdpGroupSyncForm = ({ onSubmit, enableReinitialize: Boolean(groupSyncSettings), }); + const [idpGroupName, setIdpGroupName] = useState(""); + const [coderGroups, setCoderGroups] = useState([]); const getGroupNames = (groupIds: readonly string[]) => { return groupIds.map((groupId) => groupsMap.get(groupId) || groupId); }; + const SYNC_FIELD_ID = "sync-field"; + const REGEX_FILTER_ID = "regex-filter"; + const AUTO_CREATE_MISSING_GROUPS_ID = "auto-create-missing-groups"; + const IDP_GROUP_NAME_ID = "idp-group-name"; + return (
-
-
-
- - - +
+
+
+ + + { + void form.setFieldValue("field", event.target.value); + }} + className="min-w-40" + /> +
+ { + void form.setFieldValue("regex_filter", event.target.value); + }} + className="min-w-40" + /> + +
+

+ If empty, group sync is deactivated +

+
+
+ +
+ { + void form.setFieldValue("organization_assign_default", checked); + form.handleSubmit(); + }} /> + + + +
-
- - +
+
+
+ + { + setIdpGroupName(event.target.value); + }} + /> +
+
+ + ({ + label: group.display_name || group.name, + value: group.name, + }))} + hidePlaceholderWhenSelected + placeholder="Select group" + emptyIndicator={ +

+ All groups selected +

+ } + /> +
+
+   + +
+
- - {groupSyncSettings?.mapping && - Object.entries(groupSyncSettings.mapping) - .sort() - .map(([idpGroup, groups]) => ( - - ))} - +
+ + {groupSyncSettings?.mapping && + Object.entries(groupSyncSettings.mapping) + .sort() + .map(([idpGroup, groups]) => ( + + ))} + +
+ + +
+
{groupSyncSettings?.legacy_group_name_mapping && (
@@ -372,6 +450,7 @@ interface IdpRoleSyncFormProps { roleSyncSettings: RoleSyncSettings; roleMappingCount: number; organization: Organization; + roles: Role[]; onSubmit: (data: RoleSyncSettings) => void; } @@ -388,6 +467,7 @@ const IdpRoleSyncForm = ({ roleSyncSettings, roleMappingCount, organization, + roles, onSubmit, }: IdpRoleSyncFormProps) => { const form = useFormik({ @@ -399,33 +479,132 @@ const IdpRoleSyncForm = ({ onSubmit, enableReinitialize: Boolean(roleSyncSettings), }); + const [idpRoleName, setIdpRoleName] = useState(""); + const [coderRoles, setCoderRoles] = useState([]); + + const SYNC_FIELD_ID = "sync-field"; + const IDP_ROLE_NAME_ID = "idp-role-name"; return ( -
-
- +
+
+ +
+
+ { + void form.setFieldValue("field", event.target.value); + }} + /> + +
+
+

+ If empty, role sync is deactivated +

-
- - +
+
+
+ + { + setIdpRoleName(event.target.value); + }} + /> +
+
+ + ({ + label: role.display_name || role.name, + value: role.name, + }))} + hidePlaceholderWhenSelected + placeholder="Select role" + emptyIndicator={ +

+ All roles selected +

+ } + /> +
+
+   + +
+
+
+ + {roleSyncSettings?.mapping && + Object.entries(roleSyncSettings.mapping) + .sort() + .map(([idpRole, roles]) => ( + + ))} + +
+ + +
+
- - {roleSyncSettings?.mapping && - Object.entries(roleSyncSettings.mapping) - .sort() - .map(([idpRole, roles]) => ( - - ))} -
); @@ -501,22 +680,18 @@ const LegacyGroupSyncHeader: FC = () => { ); }; -const styles = { - fieldText: { - fontFamily: MONOSPACE_FONT_FAMILY, - whiteSpace: "nowrap", - paddingBottom: ".02rem", - }, - fieldLabel: (theme) => ({ - color: theme.palette.text.secondary, - }), - fields: () => ({ - marginLeft: 16, - fontSize: 14, - }), - tableInfo: () => ({ - marginBottom: 16, - }), -} satisfies Record>; +export const AutoCreateMissingGroupsHelpTooltip: FC = () => { + return ( + + + + + Enabling auto create missing groups will automatically create groups + returned by the OIDC provider if they do not exist in Coder. + + + + ); +}; export default IdpSyncPageView; From 0befeca1cf60d8474492a27aa161f2897349b671 Mon Sep 17 00:00:00 2001 From: Jaayden Halko Date: Tue, 14 Jan 2025 15:08:08 +0000 Subject: [PATCH 05/40] chore: add delete button --- .../IdpSyncPage/IdpSyncPageView.tsx | 102 ++++++++++++++---- 1 file changed, 81 insertions(+), 21 deletions(-) diff --git a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.tsx b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.tsx index dcc86b91df0ae..4f3d9d2f11e6c 100644 --- a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.tsx +++ b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.tsx @@ -95,10 +95,10 @@ export const IdpSyncPageView: FC = ({ - Group Sync Settings + Group sync settings - Role Sync Settings + Role sync settings @@ -168,6 +168,7 @@ const IdpMappingTable: FC = ({ IdP {type} Coder {type} + @@ -206,13 +207,30 @@ const IdpMappingTable: FC = ({ ); }; -const GroupRow: FC = ({ idpGroup, coderGroup }) => { +interface GroupRowProps { + idpGroup: string; + coderGroup: readonly string[]; + onDelete: (idpOrg: string) => void; +} + +const GroupRow: FC = ({ idpGroup, coderGroup, onDelete }) => { return ( {idpGroup} + + + ); }; @@ -264,6 +282,20 @@ const IdpGroupSyncForm = ({ return groupIds.map((groupId) => groupsMap.get(groupId) || groupId); }; + const handleDelete = async (idpOrg: string) => { + const newMapping = Object.fromEntries( + Object.entries(form.values.mapping || {}).filter( + ([key]) => key !== idpOrg, + ), + ); + const newSyncSettings = { + ...form.values, + mapping: newMapping, + }; + void form.setFieldValue("mapping", newSyncSettings.mapping); + form.handleSubmit(); + }; + const SYNC_FIELD_ID = "sync-field"; const REGEX_FILTER_ID = "regex-filter"; const AUTO_CREATE_MISSING_GROUPS_ID = "auto-create-missing-groups"; @@ -361,7 +393,7 @@ const IdpGroupSyncForm = ({ onChange={setCoderGroups} defaultOptions={groups.map((group) => ({ label: group.display_name || group.name, - value: group.name, + value: group.id, }))} hidePlaceholderWhenSelected placeholder="Select group" @@ -399,7 +431,7 @@ const IdpGroupSyncForm = ({
-
+
{groupSyncSettings?.mapping && Object.entries(groupSyncSettings.mapping) @@ -409,15 +441,18 @@ const IdpGroupSyncForm = ({ key={idpGroup} idpGroup={idpGroup} coderGroup={getGroupNames(groups)} + onDelete={handleDelete} /> ))}
- + + +
@@ -435,6 +470,7 @@ const IdpGroupSyncForm = ({ key={idpGroup} idpGroup={idpGroup} coderGroup={getGroupNames([groupId])} + onDelete={handleDelete} /> ))} @@ -482,6 +518,20 @@ const IdpRoleSyncForm = ({ const [idpRoleName, setIdpRoleName] = useState(""); const [coderRoles, setCoderRoles] = useState([]); + const handleDelete = async (idpOrg: string) => { + const newMapping = Object.fromEntries( + Object.entries(form.values.mapping || {}).filter( + ([key]) => key !== idpOrg, + ), + ); + const newSyncSettings = { + ...form.values, + mapping: newMapping, + }; + void form.setFieldValue("mapping", newSyncSettings.mapping); + form.handleSubmit(); + }; + const SYNC_FIELD_ID = "sync-field"; const IDP_ROLE_NAME_ID = "idp-role-name"; @@ -592,15 +642,18 @@ const IdpRoleSyncForm = ({ key={idpRole} idpRole={idpRole} coderRoles={roles} + onDelete={handleDelete} /> ))}
- + + +
@@ -610,23 +663,30 @@ const IdpRoleSyncForm = ({ ); }; -interface GroupRowProps { - idpGroup: string; - coderGroup: readonly string[]; -} - interface RoleRowProps { idpRole: string; coderRoles: readonly string[]; + onDelete: (idpOrg: string) => void; } -const RoleRow: FC = ({ idpRole, coderRoles }) => { +const RoleRow: FC = ({ idpRole, coderRoles, onDelete }) => { return ( {idpRole} + + + ); }; From f8349de91124686db0617c85b0bc6d91bcccfa07 Mon Sep 17 00:00:00 2001 From: Jaayden Halko Date: Tue, 14 Jan 2025 15:08:45 +0000 Subject: [PATCH 06/40] fix: update input component to 40px height --- site/src/components/Input/Input.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/src/components/Input/Input.tsx b/site/src/components/Input/Input.tsx index f1abd6ecc949e..b50d6415a8983 100644 --- a/site/src/components/Input/Input.tsx +++ b/site/src/components/Input/Input.tsx @@ -13,7 +13,7 @@ export const Input = forwardRef< Date: Tue, 14 Jan 2025 15:43:01 +0000 Subject: [PATCH 07/40] fix: fix styles for MultiSelectCombobox --- .../components/MultiSelectCombobox/MultiSelectCombobox.tsx | 4 ++-- .../IdpOrgSyncPage/IdpOrgSyncPageView.tsx | 2 +- .../ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.tsx | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/site/src/components/MultiSelectCombobox/MultiSelectCombobox.tsx b/site/src/components/MultiSelectCombobox/MultiSelectCombobox.tsx index 9afe1f40ce8e9..f84ebaf3f93ca 100644 --- a/site/src/components/MultiSelectCombobox/MultiSelectCombobox.tsx +++ b/site/src/components/MultiSelectCombobox/MultiSelectCombobox.tsx @@ -454,7 +454,7 @@ export const MultiSelectCombobox = forwardRef< {/* biome-ignore lint/a11y/useKeyWithClickEvents: onKeyDown is not needed here */}
- +
diff --git a/site/src/pages/DeploymentSettingsPage/IdpOrgSyncPage/IdpOrgSyncPageView.tsx b/site/src/pages/DeploymentSettingsPage/IdpOrgSyncPage/IdpOrgSyncPageView.tsx index ae90e26c3aa7c..c3881376236fa 100644 --- a/site/src/pages/DeploymentSettingsPage/IdpOrgSyncPage/IdpOrgSyncPageView.tsx +++ b/site/src/pages/DeploymentSettingsPage/IdpOrgSyncPage/IdpOrgSyncPageView.tsx @@ -201,7 +201,7 @@ export const IdpOrgSyncPageView: FC = ({ } />
-
+
 
-
+
 
-
+
  ); }; From a7c954b79c8fa939814fd07f0132314bdbc7f348 Mon Sep 17 00:00:00 2001 From: Jaayden Halko Date: Tue, 14 Jan 2025 16:37:39 +0000 Subject: [PATCH 09/40] fix: update copy and spacing --- .../IdpSyncPage/IdpSyncPageView.tsx | 347 +++++++++--------- 1 file changed, 171 insertions(+), 176 deletions(-) diff --git a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.tsx b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.tsx index fc4626b5568ee..147e3c5272427 100644 --- a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.tsx +++ b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.tsx @@ -166,8 +166,8 @@ const IdpMappingTable: FC = ({ - IdP {type} - Coder {type} + IdP {type.toLocaleLowerCase()} + Coder {type.toLocaleLowerCase()} @@ -176,7 +176,6 @@ const IdpMappingTable: FC = ({ - @@ -198,7 +197,6 @@ const IdpMappingTable: FC = ({ - {children} @@ -305,11 +303,18 @@ const IdpGroupSyncForm = ({
+
+ +
-
+
@@ -322,7 +327,7 @@ const IdpGroupSyncForm = ({ onChange={async (event) => { void form.setFieldValue("field", event.target.value); }} - className="min-w-40" + className="min-w-72 w-72" />
- -
- { - void form.setFieldValue("organization_assign_default", checked); - form.handleSubmit(); +
+
+ { + void form.setFieldValue("organization_assign_default", checked); + form.handleSubmit(); + }} + /> + + + + +
+
+
+ + { + setIdpGroupName(event.target.value); }} /> - - - -
-
-
-
-
- - { - setIdpGroupName(event.target.value); - }} - /> -
-
- - ({ - label: group.display_name || group.name, - value: group.id, - }))} - hidePlaceholderWhenSelected - placeholder="Select group" - emptyIndicator={ -

- All groups selected -

- } - /> -
-
-   - -
+
+ + ({ + label: group.display_name || group.name, + value: group.id, + }))} + hidePlaceholderWhenSelected + placeholder="Select group" + emptyIndicator={ +

+ All groups selected +

+ } + /> +
+
+   +
+
@@ -445,14 +448,7 @@ const IdpGroupSyncForm = ({ /> ))} -
- - - +
@@ -539,8 +535,15 @@ const IdpRoleSyncForm = ({
+
+ +
-
-
-
- - { - setIdpRoleName(event.target.value); - }} - /> -
-
- - ({ - label: role.display_name || role.name, - value: role.name, - }))} - hidePlaceholderWhenSelected - placeholder="Select role" - emptyIndicator={ -

- All roles selected -

- } - /> -
-
-   - -
+
+
+ + { + setIdpRoleName(event.target.value); + }} + />
-
- - {roleSyncSettings?.mapping && - Object.entries(roleSyncSettings.mapping) - .sort() - .map(([idpRole, roles]) => ( - - ))} - -
- - - - -
+
+ + ({ + label: role.display_name || role.name, + value: role.name, + }))} + hidePlaceholderWhenSelected + placeholder="Select role" + emptyIndicator={ +

+ All roles selected +

+ } + /> +
+
+   + +
+
+
+ + {roleSyncSettings?.mapping && + Object.entries(roleSyncSettings.mapping) + .sort() + .map(([idpRole, roles]) => ( + + ))} + +
+
From c09a3f32f7407d43db0b9724cba13ffc1cffaa51 Mon Sep 17 00:00:00 2001 From: Jaayden Halko Date: Tue, 14 Jan 2025 19:45:34 +0000 Subject: [PATCH 10/40] chore: update styles --- .../IdpSyncPage/IdpSyncPageView.tsx | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.tsx b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.tsx index 147e3c5272427..b5fbfcb708b4f 100644 --- a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.tsx +++ b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.tsx @@ -134,16 +134,9 @@ interface TableRowCountProps { const TableRowCount: FC = ({ count, type }) => { return (
({ - margin: 0, - fontSize: 13, - color: theme.palette.text.secondary, - "& strong": { - color: theme.palette.text.primary, - }, - })} + className="text-content-secondary text-xs" > - Showing {count} {type} + Showing {count} {type}
); }; @@ -434,7 +427,7 @@ const IdpGroupSyncForm = ({
-
+
{groupSyncSettings?.mapping && Object.entries(groupSyncSettings.mapping) @@ -635,7 +628,7 @@ const IdpRoleSyncForm = ({
-
+
{roleSyncSettings?.mapping && Object.entries(roleSyncSettings.mapping) From 8714f5ec6a21864a79b405f7fb7a1f4833d2982c Mon Sep 17 00:00:00 2001 From: Jaayden Halko Date: Tue, 14 Jan 2025 19:45:42 +0000 Subject: [PATCH 11/40] fix: fix storybook tests --- .../IdpSyncPage/ExportPolicyButton.stories.tsx | 4 ++-- .../IdpSyncPage/IdpSyncPageView.stories.tsx | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/site/src/pages/ManagementSettingsPage/IdpSyncPage/ExportPolicyButton.stories.tsx b/site/src/pages/ManagementSettingsPage/IdpSyncPage/ExportPolicyButton.stories.tsx index af9a6b2fa4073..6c25f170d629e 100644 --- a/site/src/pages/ManagementSettingsPage/IdpSyncPage/ExportPolicyButton.stories.tsx +++ b/site/src/pages/ManagementSettingsPage/IdpSyncPage/ExportPolicyButton.stories.tsx @@ -32,7 +32,7 @@ export const ClickExportGroupPolicy: Story = { play: async ({ canvasElement, args }) => { const canvas = within(canvasElement); await userEvent.click( - canvas.getByRole("button", { name: "Export Policy" }), + canvas.getByRole("button", { name: "Export policy" }), ); await waitFor(() => expect(args.download).toHaveBeenCalledWith( @@ -58,7 +58,7 @@ export const ClickExportRolePolicy: Story = { play: async ({ canvasElement, args }) => { const canvas = within(canvasElement); await userEvent.click( - canvas.getByRole("button", { name: "Export Policy" }), + canvas.getByRole("button", { name: "Export policy" }), ); await waitFor(() => expect(args.download).toHaveBeenCalledWith( diff --git a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.stories.tsx b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.stories.tsx index d2ff522fbbfd9..ec5cc299c645c 100644 --- a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.stories.tsx +++ b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.stories.tsx @@ -83,8 +83,8 @@ export const RolesTab: Story = { play: async ({ canvasElement }) => { const user = userEvent.setup(); const canvas = within(canvasElement); - const rolesTab = await canvas.findByText("Role Sync Settings"); + const rolesTab = await canvas.findByText("Role sync settings"); await user.click(rolesTab); - await expect(canvas.findByText("IdP Role")).resolves.toBeVisible(); + await expect(canvas.findByText("IdP role")).resolves.toBeVisible(); }, }; From b558f358b34d753dc32aa32743b805b010a6651f Mon Sep 17 00:00:00 2001 From: Jaayden Halko Date: Tue, 14 Jan 2025 19:55:31 +0000 Subject: [PATCH 12/40] fix: fix legacy group mappings styles --- .../IdpSyncPage/IdpSyncPageView.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.tsx b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.tsx index b5fbfcb708b4f..41a4eb83d35d0 100644 --- a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.tsx +++ b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.tsx @@ -426,7 +426,7 @@ const IdpGroupSyncForm = ({
-
+
{groupSyncSettings?.mapping && @@ -446,7 +446,7 @@ const IdpGroupSyncForm = ({
{groupSyncSettings?.legacy_group_name_mapping && ( -
+
))} -
+
)}
@@ -706,15 +706,15 @@ const LegacyGroupSyncHeader: FC = () => { }} >
- Legacy Group Sync Settings + Legacy group sync settings - Legacy Group Sync Settings + Legacy group sync settings These settings were configured using environment variables, and only apply to the default organization. It is now recommended to - configure IdP sync via the CLI, which enables sync to be + configure IdP sync via the CLI or the UI, which enables sync to be configured for any organization, and for those settings to be persisted without manually setting environment variables.{" "} From aa0e53c457c602da0bb872e8989c6bd43d17440e Mon Sep 17 00:00:00 2001 From: Jaayden Halko Date: Tue, 14 Jan 2025 20:27:53 +0000 Subject: [PATCH 13/40] fix: format --- .../ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.tsx b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.tsx index 41a4eb83d35d0..701cc5ab426e6 100644 --- a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.tsx +++ b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.tsx @@ -133,9 +133,7 @@ interface TableRowCountProps { const TableRowCount: FC = ({ count, type }) => { return ( -
+
Showing {count} {type}
); From 76358569486101700a744887eca79d218fe9a23a Mon Sep 17 00:00:00 2001 From: Jaayden Halko Date: Tue, 14 Jan 2025 22:24:36 +0000 Subject: [PATCH 14/40] feat: create link component --- .../IdpSyncPage/IdpSyncPage.tsx | 11 ++--------- .../IdpSyncPage/IdpSyncPageView.tsx | 7 +------ 2 files changed, 3 insertions(+), 15 deletions(-) diff --git a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPage.tsx b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPage.tsx index 5cf27edf16968..ab0077f758dfa 100644 --- a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPage.tsx +++ b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPage.tsx @@ -11,8 +11,8 @@ import { ChooseOne, Cond } from "components/Conditionals/ChooseOne"; import { EmptyState } from "components/EmptyState/EmptyState"; import { displayError } from "components/GlobalSnackbar/utils"; import { displaySuccess } from "components/GlobalSnackbar/utils"; +import { Link } from "components/Link/Link"; import { Paywall } from "components/Paywall/Paywall"; -import { SquareArrowOutUpRight } from "lucide-react"; import { useFeatureVisibility } from "modules/dashboard/useFeatureVisibility"; import { useOrganizationSettings } from "modules/management/OrganizationSettingsLayout"; import { type FC, useEffect } from "react"; @@ -107,16 +107,9 @@ export const IdpSyncPage: FC = () => {

Automatically assign groups or roles to a user based on their IdP claims. - - View docs - - +

- {/* */} diff --git a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.tsx b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.tsx index 701cc5ab426e6..53f415cdaea7f 100644 --- a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.tsx +++ b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.tsx @@ -697,12 +697,7 @@ const TableLoader = () => { const LegacyGroupSyncHeader: FC = () => { return ( -

+

Legacy group sync settings From 91ca380961da79b2311dfec3131b3a7156d01459 Mon Sep 17 00:00:00 2001 From: Jaayden Halko Date: Wed, 15 Jan 2025 11:46:47 +0000 Subject: [PATCH 15/40] fix: use new Link component --- .../IdpSyncPage/IdpSyncPageView.tsx | 24 ++++++++----------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.tsx b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.tsx index 53f415cdaea7f..9410e875ac8cd 100644 --- a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.tsx +++ b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.tsx @@ -1,4 +1,3 @@ -import Link from "@mui/material/Link"; import Skeleton from "@mui/material/Skeleton"; import Table from "@mui/material/Table"; import TableBody from "@mui/material/TableBody"; @@ -26,6 +25,7 @@ import { } from "components/HelpTooltip/HelpTooltip"; import { Input } from "components/Input/Input"; import { Label } from "components/Label/Label"; +import { Link } from "components/Link/Link"; import { Loader } from "components/Loader/Loader"; import { MultiSelectCombobox, @@ -38,7 +38,7 @@ import { } from "components/TableLoader/TableLoader"; import { TabLink, Tabs, TabsList } from "components/Tabs/Tabs"; import { useFormik } from "formik"; -import { Plus, SquareArrowOutUpRight, Trash } from "lucide-react"; +import { Plus, Trash } from "lucide-react"; import { type FC, useState } from "react"; import { useSearchParams } from "react-router-dom"; import { docs } from "utils/docs"; @@ -174,15 +174,10 @@ const IdpMappingTable: FC = ({ message={`No ${type} Mappings`} isCompact cta={ - + } /> @@ -710,9 +705,10 @@ const LegacyGroupSyncHeader: FC = () => { configure IdP sync via the CLI or the UI, which enables sync to be configured for any organization, and for those settings to be persisted without manually setting environment variables.{" "} - - Learn more… - + From 328ce2f1aa1128df51f85f9cfd2774dfa8be7082 Mon Sep 17 00:00:00 2001 From: Jaayden Halko Date: Wed, 15 Jan 2025 12:00:16 +0000 Subject: [PATCH 16/40] fix: use correct field value --- .../ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.tsx b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.tsx index 9410e875ac8cd..4c221d4146500 100644 --- a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.tsx +++ b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.tsx @@ -347,7 +347,7 @@ const IdpGroupSyncForm = ({ id={AUTO_CREATE_MISSING_GROUPS_ID} checked={form.values.auto_create_missing_groups} onCheckedChange={async (checked) => { - void form.setFieldValue("organization_assign_default", checked); + void form.setFieldValue("auto_create_missing_groups", checked); form.handleSubmit(); }} /> From 52d563ca8723870fc44c06f7cf9b7fa1bbd77232 Mon Sep 17 00:00:00 2001 From: Jaayden Halko Date: Wed, 15 Jan 2025 15:39:26 +0000 Subject: [PATCH 17/40] fix: update error handling --- .../IdpSyncPage/IdpSyncPage.tsx | 22 ------ .../IdpSyncPage/IdpSyncPageView.tsx | 69 +++++++++---------- 2 files changed, 32 insertions(+), 59 deletions(-) diff --git a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPage.tsx b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPage.tsx index ab0077f758dfa..484d616f9fa6c 100644 --- a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPage.tsx +++ b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPage.tsx @@ -58,28 +58,6 @@ export const IdpSyncPage: FC = () => { patchRoleSyncSettings(organizationName, queryClient), ); - useEffect(() => { - if (patchGroupSyncSettingsMutation.error) { - displayError( - getErrorMessage( - patchGroupSyncSettingsMutation.error, - "Error updating IdP group sync settings.", - ), - ); - } - }, [patchGroupSyncSettingsMutation.error]); - - useEffect(() => { - if (patchRoleSyncSettingsMutation.error) { - displayError( - getErrorMessage( - patchRoleSyncSettingsMutation.error, - "Error updating IdP role sync settings.", - ), - ); - } - }, [patchRoleSyncSettingsMutation.error]); - const error = groupIdpSyncSettingsQuery.error || roleIdpSyncSettingsQuery.error || diff --git a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.tsx b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.tsx index 4c221d4146500..2bdf3a5598b8f 100644 --- a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.tsx +++ b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.tsx @@ -81,48 +81,43 @@ export const IdpSyncPageView: FC = ({ ? Object.entries(roleSyncSettings.mapping).length : 0; - if (error) { - return ; - } - if (!groupSyncSettings || !roleSyncSettings || !groups) { return ; } return ( - <> -
- - - - Group sync settings - - - Role sync settings - - - - {tab === "groups" ? ( - - ) : ( - - )} -
- +
+ {Boolean(error) && } + + + + Group sync settings + + + Role sync settings + + + + {tab === "groups" ? ( + + ) : ( + + )} +
); }; From b1d764914740357d2361004a301e07843f4f6c5a Mon Sep 17 00:00:00 2001 From: Jaayden Halko Date: Wed, 15 Jan 2025 22:34:06 +0000 Subject: [PATCH 18/40] feat: add e2e tests for group and role sync --- site/e2e/api.ts | 36 ++++ .../tests/organizations/idpGroupSync.spec.ts | 163 ++++++++++++++++++ .../tests/organizations/idpRoleSync.spec.ts | 145 ++++++++++++++++ 3 files changed, 344 insertions(+) create mode 100644 site/e2e/tests/organizations/idpGroupSync.spec.ts create mode 100644 site/e2e/tests/organizations/idpRoleSync.spec.ts diff --git a/site/e2e/api.ts b/site/e2e/api.ts index 96a2e37260767..302471bbe38f9 100644 --- a/site/e2e/api.ts +++ b/site/e2e/api.ts @@ -88,6 +88,42 @@ export const createOrganizationSyncSettings = async () => { return settings; }; +export const createGroupSyncSettings = async (orgId: string) => { + const settings = await API.patchGroupIdpSyncSettings( + { + field: "group-field-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: true, + }, + orgId, + ); + return settings; +}; + +export const createRoleSyncSettings = async (orgId: string) => { + const settings = await API.patchRoleIdpSyncSettings( + { + field: "role-field-test", + mapping: { + "idp-role-1": [ + "fbd2116a-8961-4954-87ae-e4575bd29ce0", + "13de3eb4-9b4f-49e7-b0f8-0c3728a0d2e2", + ], + "idp-role-2": ["fbd2116a-8961-4954-87ae-e4575bd29ce0"], + }, + }, + orgId, + ); + return settings; +}; + export const createCustomRole = async ( orgId: string, name: string, diff --git a/site/e2e/tests/organizations/idpGroupSync.spec.ts b/site/e2e/tests/organizations/idpGroupSync.spec.ts new file mode 100644 index 0000000000000..ad8355113df3e --- /dev/null +++ b/site/e2e/tests/organizations/idpGroupSync.spec.ts @@ -0,0 +1,163 @@ +import { expect, test } from "@playwright/test"; +import { + createGroupSyncSettings, + createOrganizationWithName, + deleteOrganization, + setupApiCalls, +} from "../../api"; +import { randomName, requiresLicense } from "../../helpers"; +import { login } from "../../helpers"; +import { beforeCoderTest } from "../../hooks"; + +test.beforeEach(async ({ page }) => { + beforeCoderTest(page); + await login(page); + await setupApiCalls(page); +}); + +test.describe("IdpGroupSyncPage", () => { + test("add new IdP group mapping with API", async ({ page }) => { + requiresLicense(); + const org = await createOrganizationWithName(randomName()); + await createGroupSyncSettings(org.id); + + await page.goto(`/organizations/${org.name}/idp-sync?tab=groups`, { + waitUntil: "domcontentloaded", + }); + + await expect( + page.getByRole("switch", { name: "Auto create missing groups" }), + ).toBeChecked(); + + await expect(page.getByText("idp-group-1")).toBeVisible(); + await expect( + page.getByText("fbd2116a-8961-4954-87ae-e4575bd29ce0").first(), + ).toBeVisible(); + + await expect(page.getByText("idp-group-2")).toBeVisible(); + await expect( + page.getByText("fbd2116a-8961-4954-87ae-e4575bd29ce0").last(), + ).toBeVisible(); + + await deleteOrganization(org.name); + }); + + test("delete a IdP group to coder group mapping row", async ({ page }) => { + requiresLicense(); + const org = await createOrganizationWithName(randomName()); + await createGroupSyncSettings(org.id); + + await page.goto(`/organizations/${org.name}/idp-sync?tab=groups`, { + waitUntil: "domcontentloaded", + }); + + await expect(page.getByText("idp-group-1")).toBeVisible(); + await page + .getByRole("button", { name: /delete/i }) + .first() + .click(); + await expect(page.getByText("idp-group-1")).not.toBeVisible(); + await expect( + page.getByText("IdP Group sync settings updated."), + ).toBeVisible(); + }); + + test("update sync field", async ({ page }) => { + requiresLicense(); + const org = await createOrganizationWithName(randomName()); + await page.goto(`/organizations/${org.name}/idp-sync?tab=groups`, { + waitUntil: "domcontentloaded", + }); + + const syncField = page.getByRole("textbox", { + name: "Group sync field", + }); + const saveButton = page.getByRole("button", { name: /save/i }).first(); + + await expect(saveButton).toBeDisabled(); + + await syncField.fill("test-field"); + await expect(saveButton).toBeEnabled(); + + await page.getByRole("button", { name: /save/i }).click(); + + await expect( + page.getByText("IdP Group sync settings updated."), + ).toBeVisible(); + }); + + test("toggle off auto create missing groups", async ({ page }) => { + requiresLicense(); + const org = await createOrganizationWithName(randomName()); + await page.goto(`/organizations/${org.name}/idp-sync?tab=groups`, { + waitUntil: "domcontentloaded", + }); + + const toggle = page.getByRole("switch", { + name: "Auto create missing groups", + }); + await toggle.click(); + + await expect( + page.getByText("IdP Group sync settings updated."), + ).toBeVisible(); + + await expect(toggle).toBeChecked(); + }); + + test("export policy button is enabled when sync settings are present", async ({ + page, + }) => { + requiresLicense(); + const org = await createOrganizationWithName(randomName()); + await createGroupSyncSettings(org.id); + await page.goto(`/organizations/${org.name}/idp-sync?tab=groups`, { + waitUntil: "domcontentloaded", + }); + + const exportButton = page.getByRole("button", { name: /Export Policy/i }); + await expect(exportButton).toBeEnabled(); + await exportButton.click(); + }); + + test("add new IdP group mapping with UI", async ({ page }) => { + requiresLicense(); + const orgName = randomName(); + await createOrganizationWithName(orgName); + + await page.goto(`/organizations/${orgName}/idp-sync?tab=groups`, { + waitUntil: "domcontentloaded", + }); + + const idpOrgInput = page.getByLabel("IdP group name"); + const orgSelector = page.getByPlaceholder("Select group"); + const addButton = page.getByRole("button", { + name: /Add IdP group/i, + }); + + await expect(addButton).toBeDisabled(); + + await idpOrgInput.fill("new-idp-group"); + + // Select Coder organization from combobox + await orgSelector.click(); + await page.getByRole("option", { name: /Everyone/i }).click(); + + // Add button should now be enabled + await expect(addButton).toBeEnabled(); + + await addButton.click(); + + // Verify new mapping appears in table + const newRow = page.getByTestId("group-new-idp-group"); + await expect(newRow).toBeVisible(); + await expect(newRow.getByText("new-idp-group")).toBeVisible(); + await expect(newRow.getByText("Everyone")).toBeVisible(); + + await expect( + page.getByText("IdP Group sync settings updated."), + ).toBeVisible(); + + await deleteOrganization(orgName); + }); +}); diff --git a/site/e2e/tests/organizations/idpRoleSync.spec.ts b/site/e2e/tests/organizations/idpRoleSync.spec.ts new file mode 100644 index 0000000000000..78f602b9ba9eb --- /dev/null +++ b/site/e2e/tests/organizations/idpRoleSync.spec.ts @@ -0,0 +1,145 @@ +import { expect, test } from "@playwright/test"; +import { + createOrganizationWithName, + createRoleSyncSettings, + deleteOrganization, + setupApiCalls, +} from "../../api"; +import { randomName, requiresLicense } from "../../helpers"; +import { login } from "../../helpers"; +import { beforeCoderTest } from "../../hooks"; + +test.beforeEach(async ({ page }) => { + beforeCoderTest(page); + await login(page); + await setupApiCalls(page); +}); + +test.describe("IdpRoleSyncPage", () => { + test("add new IdP role mapping with API", async ({ page }) => { + requiresLicense(); + const org = await createOrganizationWithName(randomName()); + await createRoleSyncSettings(org.id); + + await page.goto(`/organizations/${org.name}/idp-sync?tab=roles`, { + waitUntil: "domcontentloaded", + }); + + await expect(page.getByText("idp-role-1")).toBeVisible(); + await expect( + page.getByText("fbd2116a-8961-4954-87ae-e4575bd29ce0").first(), + ).toBeVisible(); + + await expect(page.getByText("idp-role-2")).toBeVisible(); + await expect( + page.getByText("fbd2116a-8961-4954-87ae-e4575bd29ce0").last(), + ).toBeVisible(); + + await deleteOrganization(org.name); + }); + + test("delete a IdP role to coder role mapping row", async ({ page }) => { + requiresLicense(); + const org = await createOrganizationWithName(randomName()); + await createRoleSyncSettings(org.id); + + await page.goto(`/organizations/${org.name}/idp-sync?tab=roles`, { + waitUntil: "domcontentloaded", + }); + + await expect(page.getByText("idp-role-1")).toBeVisible(); + await page + .getByRole("button", { name: /delete/i }) + .first() + .click(); + await expect(page.getByText("idp-role-1")).not.toBeVisible(); + await expect( + page.getByText("IdP Role sync settings updated."), + ).toBeVisible(); + + await deleteOrganization(org.name); + }); + + test("update sync field", async ({ page }) => { + requiresLicense(); + const org = await createOrganizationWithName(randomName()); + await page.goto(`/organizations/${org.name}/idp-sync?tab=roles`, { + waitUntil: "domcontentloaded", + }); + + const syncField = page.getByRole("textbox", { + name: "Role sync field", + }); + const saveButton = page.getByRole("button", { name: /save/i }).first(); + + await expect(saveButton).toBeDisabled(); + + await syncField.fill("test-field"); + await expect(saveButton).toBeEnabled(); + + await page.getByRole("button", { name: /save/i }).click(); + + await expect( + page.getByText("IdP Role sync settings updated."), + ).toBeVisible(); + + await deleteOrganization(org.name); + }); + + test("export policy button is enabled when sync settings are present", async ({ + page, + }) => { + requiresLicense(); + const org = await createOrganizationWithName(randomName()); + await page.goto(`/organizations/${org.name}/idp-sync?tab=roles`, { + waitUntil: "domcontentloaded", + }); + + const exportButton = page.getByRole("button", { name: /Export Policy/i }); + await createRoleSyncSettings(org.id); + + await expect(exportButton).toBeEnabled(); + await exportButton.click(); + }); + + test("add new IdP role mapping with UI", async ({ page }) => { + requiresLicense(); + const orgName = randomName(); + await createOrganizationWithName(orgName); + + await page.goto(`/organizations/${orgName}/idp-sync?tab=roles`, { + waitUntil: "domcontentloaded", + }); + + const idpOrgInput = page.getByLabel("IdP role name"); + const roleSelector = page.getByPlaceholder("Select role"); + const addButton = page.getByRole("button", { + name: /Add IdP role/i, + }); + + await expect(addButton).toBeDisabled(); + + await idpOrgInput.fill("new-idp-role"); + + // Select Coder role from combobox + await roleSelector.click(); + await page.getByRole("option", { name: /Organization Admin/i }).click(); + + // Add button should now be enabled + await expect(addButton).toBeEnabled(); + + await addButton.click(); + + // Verify new mapping appears in table + const newRow = page.getByTestId("role-new-idp-role"); + await expect(newRow).toBeVisible(); + await expect(newRow.getByText("new-idp-role")).toBeVisible(); + await expect(newRow.getByText("organization-admin")).toBeVisible(); + + await expect( + page.getByText("IdP Role sync settings updated."), + ).toBeVisible(); + + await deleteOrganization(orgName); + }); +}); From b3812f1104dca36f3452cda193a105d00abad008 Mon Sep 17 00:00:00 2001 From: Jaayden Halko Date: Wed, 15 Jan 2025 23:27:06 +0000 Subject: [PATCH 19/40] chore: update link components --- .../IdpSyncPage/IdpSyncPage.tsx | 2 +- .../IdpSyncPage/IdpSyncPageView.tsx | 20 +++++++++---------- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPage.tsx b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPage.tsx index 484d616f9fa6c..a1d0abaae2f1c 100644 --- a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPage.tsx +++ b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPage.tsx @@ -85,7 +85,7 @@ export const IdpSyncPage: FC = () => {

Automatically assign groups or roles to a user based on their IdP claims. - + View docs

diff --git a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.tsx b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.tsx index 2bdf3a5598b8f..d6c5f6189e0cc 100644 --- a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.tsx +++ b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.tsx @@ -166,13 +166,12 @@ const IdpMappingTable: FC = ({ + + How to setup IdP {type.toLocaleLowerCase()} sync + } /> @@ -202,7 +201,7 @@ const GroupRow: FC = ({ idpGroup, coderGroup, onDelete }) => { +

+

+ If empty, group sync is deactivated +

+ + + +
+ { + void form.setFieldValue("auto_create_missing_groups", checked); + form.handleSubmit(); + }} + /> + + + + +
+
+
+ + { + setIdpGroupName(event.target.value); + }} + /> +
+
+ + ({ + label: group.display_name || group.name, + value: group.id, + }))} + hidePlaceholderWhenSelected + placeholder="Select group" + emptyIndicator={ +

+ All groups selected +

+ } + /> +
+
+   + +
+
+ +
+ + {groupSyncSettings?.mapping && + Object.entries(groupSyncSettings.mapping) + .sort() + .map(([idpGroup, groups]) => ( + + ))} + + + {groupSyncSettings?.legacy_group_name_mapping && ( +
+ + + {Object.entries(groupSyncSettings.legacy_group_name_mapping) + .sort() + .map(([idpGroup, groupId]) => ( + + ))} + +
+ )} +
+ + + ); +}; + +interface GroupRowProps { + idpGroup: string; + coderGroup: readonly string[]; + onDelete: (idpOrg: string) => void; +} + +const GroupRow: FC = ({ idpGroup, coderGroup, onDelete }) => { + return ( + + {idpGroup} + + + + + + + + ); +}; + +const AutoCreateMissingGroupsHelpTooltip: FC = () => { + return ( + + + + + Enabling auto create missing groups will automatically create groups + returned by the OIDC provider if they do not exist in Coder. + + + + ); +}; + +const LegacyGroupSyncHeader: FC = () => { + return ( +

+
+ Legacy group sync settings + + + + Legacy group sync settings + + These settings were configured using environment variables, and + only apply to the default organization. It is now recommended to + configure IdP sync via the CLI or the UI, which enables sync to be + configured for any organization, and for those settings to be + persisted without manually setting environment variables.{" "} + + Learn more… + + + + +
+

+ ); +}; diff --git a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpMappingTable.tsx b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpMappingTable.tsx new file mode 100644 index 0000000000000..0c6ea4149828d --- /dev/null +++ b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpMappingTable.tsx @@ -0,0 +1,95 @@ +import Skeleton from "@mui/material/Skeleton"; +import Table from "@mui/material/Table"; +import TableBody from "@mui/material/TableBody"; +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 { ChooseOne, Cond } from "components/Conditionals/ChooseOne"; +import { EmptyState } from "components/EmptyState/EmptyState"; +import { Link } from "components/Link/Link"; +import { + TableLoaderSkeleton, + TableRowSkeleton, +} from "components/TableLoader/TableLoader"; +import type { FC } from "react"; +import { docs } from "utils/docs"; + +interface IdpMappingTableProps { + type: "Role" | "Group"; + rowCount: number; + children: React.ReactNode; +} + +export const IdpMappingTable: FC = ({ + type, + rowCount, + children, +}) => { + const isLoading = false; + + return ( +
+ +
+ + + IdP {type.toLocaleLowerCase()} + + Coder {type.toLocaleLowerCase()} + + + + + + + + + + + + + + How to setup IdP {type.toLocaleLowerCase()} sync + + } + /> + + + + {children} + + +
+ +
+
+ Showing {rowCount}{" "} + groups +
+
+
+ ); +}; + +const TableLoader = () => { + return ( + + + + + + + + + + + + + + ); +}; diff --git a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpRoleSyncForm.tsx b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpRoleSyncForm.tsx new file mode 100644 index 0000000000000..63eaa55fa8a53 --- /dev/null +++ b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpRoleSyncForm.tsx @@ -0,0 +1,220 @@ +import TableCell from "@mui/material/TableCell"; +import TableRow from "@mui/material/TableRow"; +import type { Organization, Role, RoleSyncSettings } from "api/typesGenerated"; +import { Button } from "components/Button/Button"; +import { Input } from "components/Input/Input"; +import { Label } from "components/Label/Label"; +import { + MultiSelectCombobox, + type Option, +} from "components/MultiSelectCombobox/MultiSelectCombobox"; +import { useFormik } from "formik"; +import { Plus, Trash } from "lucide-react"; +import { type FC, useState } from "react"; +import * as Yup from "yup"; +import { ExportPolicyButton } from "./ExportPolicyButton"; +import { IdpMappingTable } from "./IdpMappingTable"; +import { IdpPillList } from "./IdpPillList"; + +interface IdpRoleSyncFormProps { + roleSyncSettings: RoleSyncSettings; + roleMappingCount: number; + organization: Organization; + roles: Role[]; + onSubmit: (data: RoleSyncSettings) => void; +} + +const roleyncValidationSchema = Yup.object({ + field: Yup.string().trim(), + regex_filter: Yup.string().trim(), + auto_create_missing_groups: Yup.boolean(), + mapping: Yup.object().shape({ + [`${String}`]: Yup.array().of(Yup.string()), + }), +}); + +export const IdpRoleSyncForm = ({ + roleSyncSettings, + roleMappingCount, + organization, + roles, + onSubmit, +}: IdpRoleSyncFormProps) => { + const form = useFormik({ + initialValues: { + field: roleSyncSettings?.field ?? "", + mapping: roleSyncSettings?.mapping ?? {}, + }, + validationSchema: roleyncValidationSchema, + onSubmit, + enableReinitialize: Boolean(roleSyncSettings), + }); + const [idpRoleName, setIdpRoleName] = useState(""); + const [coderRoles, setCoderRoles] = useState([]); + + const handleDelete = async (idpOrg: string) => { + const newMapping = Object.fromEntries( + Object.entries(form.values.mapping || {}).filter( + ([key]) => key !== idpOrg, + ), + ); + const newSyncSettings = { + ...form.values, + mapping: newMapping, + }; + void form.setFieldValue("mapping", newSyncSettings.mapping); + form.handleSubmit(); + }; + + const SYNC_FIELD_ID = "sync-field"; + const IDP_ROLE_NAME_ID = "idp-role-name"; + + return ( +
+
+
+ +
+
+ +
+
+ { + void form.setFieldValue("field", event.target.value); + }} + className="min-w-72 w-72" + /> + +
+
+

+ If empty, role sync is deactivated +

+
+
+
+ + { + setIdpRoleName(event.target.value); + }} + /> +
+
+ + ({ + label: role.display_name || role.name, + value: role.name, + }))} + hidePlaceholderWhenSelected + placeholder="Select role" + emptyIndicator={ +

+ All roles selected +

+ } + /> +
+
+   + +
+
+ + {roleSyncSettings?.mapping && + Object.entries(roleSyncSettings.mapping) + .sort() + .map(([idpRole, roles]) => ( + + ))} + +
+
+ ); +}; + +interface RoleRowProps { + idpRole: string; + coderRoles: readonly string[]; + onDelete: (idpOrg: string) => void; +} + +const RoleRow: FC = ({ idpRole, coderRoles, onDelete }) => { + return ( + + {idpRole} + + + + + + + + ); +}; diff --git a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.tsx b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.tsx index d6c5f6189e0cc..18e1fd96fbdff 100644 --- a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.tsx +++ b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.tsx @@ -1,10 +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"; -import TableContainer from "@mui/material/TableContainer"; -import TableHead from "@mui/material/TableHead"; -import TableRow from "@mui/material/TableRow"; import type { Group, GroupSyncSettings, @@ -13,38 +6,12 @@ import type { RoleSyncSettings, } from "api/typesGenerated"; import { ErrorAlert } from "components/Alert/ErrorAlert"; -import { Button } from "components/Button/Button"; -import { ChooseOne, Cond } from "components/Conditionals/ChooseOne"; -import { EmptyState } from "components/EmptyState/EmptyState"; -import { - HelpTooltip, - HelpTooltipContent, - HelpTooltipText, - HelpTooltipTitle, - HelpTooltipTrigger, -} from "components/HelpTooltip/HelpTooltip"; -import { Input } from "components/Input/Input"; -import { Label } from "components/Label/Label"; -import { Link } from "components/Link/Link"; import { Loader } from "components/Loader/Loader"; -import { - MultiSelectCombobox, - type Option, -} from "components/MultiSelectCombobox/MultiSelectCombobox"; -import { Switch } from "components/Switch/Switch"; -import { - TableLoaderSkeleton, - TableRowSkeleton, -} from "components/TableLoader/TableLoader"; import { TabLink, Tabs, TabsList } from "components/Tabs/Tabs"; -import { useFormik } from "formik"; -import { Plus, Trash } from "lucide-react"; -import { type FC, useState } from "react"; +import type { FC } from "react"; import { useSearchParams } from "react-router-dom"; -import { docs } from "utils/docs"; -import * as Yup from "yup"; -import { ExportPolicyButton } from "./ExportPolicyButton"; -import { IdpPillList } from "./IdpPillList"; +import { IdpGroupSyncForm } from "./IdpGroupSyncForm"; +import { IdpRoleSyncForm } from "./IdpRoleSyncForm"; interface IdpSyncPageViewProps { groupSyncSettings: GroupSyncSettings | undefined; @@ -121,607 +88,4 @@ export const IdpSyncPageView: FC = ({ ); }; -interface TableRowCountProps { - count: number; - type: string; -} - -const TableRowCount: FC = ({ count, type }) => { - return ( -
- Showing {count} {type} -
- ); -}; - -interface IdpMappingTableProps { - type: "Role" | "Group"; - isEmpty: boolean; - children: React.ReactNode; -} - -const IdpMappingTable: FC = ({ - type, - isEmpty, - children, -}) => { - const isLoading = false; - - return ( - - - - - IdP {type.toLocaleLowerCase()} - Coder {type.toLocaleLowerCase()} - - - - - - - - - - - - - How to setup IdP {type.toLocaleLowerCase()} sync - - } - /> - - - - {children} - - -
-
- ); -}; - -interface GroupRowProps { - idpGroup: string; - coderGroup: readonly string[]; - onDelete: (idpOrg: string) => void; -} - -const GroupRow: FC = ({ idpGroup, coderGroup, onDelete }) => { - return ( - - {idpGroup} - - - - - - - - ); -}; - -interface IdpGroupSyncFormProps { - groupSyncSettings: GroupSyncSettings; - groupsMap: Map; - groups: Group[]; - groupMappingCount: number; - legacyGroupMappingCount: number; - organization: Organization; - onSubmit: (data: GroupSyncSettings) => void; -} - -const groupSyncValidationSchema = Yup.object({ - field: Yup.string().trim(), - regex_filter: Yup.string().trim(), - auto_create_missing_groups: Yup.boolean(), - mapping: Yup.object().shape({ - [`${String}`]: Yup.array().of(Yup.string()), - }), -}); - -const IdpGroupSyncForm = ({ - groupSyncSettings, - groupMappingCount, - legacyGroupMappingCount, - groups, - groupsMap, - organization, - onSubmit, -}: IdpGroupSyncFormProps) => { - const form = useFormik({ - initialValues: { - field: groupSyncSettings?.field ?? "", - regex_filter: groupSyncSettings?.regex_filter ?? "", - auto_create_missing_groups: - groupSyncSettings?.auto_create_missing_groups ?? false, - mapping: groupSyncSettings?.mapping ?? {}, - }, - validationSchema: groupSyncValidationSchema, - onSubmit, - enableReinitialize: Boolean(groupSyncSettings), - }); - const [idpGroupName, setIdpGroupName] = useState(""); - const [coderGroups, setCoderGroups] = useState([]); - - const getGroupNames = (groupIds: readonly string[]) => { - return groupIds.map((groupId) => groupsMap.get(groupId) || groupId); - }; - - const handleDelete = async (idpOrg: string) => { - const newMapping = Object.fromEntries( - Object.entries(form.values.mapping || {}).filter( - ([key]) => key !== idpOrg, - ), - ); - const newSyncSettings = { - ...form.values, - mapping: newMapping, - }; - void form.setFieldValue("mapping", newSyncSettings.mapping); - form.handleSubmit(); - }; - - const SYNC_FIELD_ID = "sync-field"; - const REGEX_FILTER_ID = "regex-filter"; - const AUTO_CREATE_MISSING_GROUPS_ID = "auto-create-missing-groups"; - const IDP_GROUP_NAME_ID = "idp-group-name"; - - return ( -
-
-
- -
-
-
-
- - - { - void form.setFieldValue("field", event.target.value); - }} - className="min-w-72 w-72" - /> -
- { - void form.setFieldValue("regex_filter", event.target.value); - }} - className="min-w-40" - /> - -
-

- If empty, group sync is deactivated -

-
-
-
-
- { - void form.setFieldValue("auto_create_missing_groups", checked); - form.handleSubmit(); - }} - /> - - - - -
-
-
- - { - setIdpGroupName(event.target.value); - }} - /> -
-
- - ({ - label: group.display_name || group.name, - value: group.id, - }))} - hidePlaceholderWhenSelected - placeholder="Select group" - emptyIndicator={ -

- All groups selected -

- } - /> -
-
-   - -
-
- -
-
- - {groupSyncSettings?.mapping && - Object.entries(groupSyncSettings.mapping) - .sort() - .map(([idpGroup, groups]) => ( - - ))} - -
- -
-
- {groupSyncSettings?.legacy_group_name_mapping && ( -
- - - {Object.entries(groupSyncSettings.legacy_group_name_mapping) - .sort() - .map(([idpGroup, groupId]) => ( - - ))} - -
- )} -
-
-
- ); -}; - -interface IdpRoleSyncFormProps { - roleSyncSettings: RoleSyncSettings; - roleMappingCount: number; - organization: Organization; - roles: Role[]; - onSubmit: (data: RoleSyncSettings) => void; -} - -const roleyncValidationSchema = Yup.object({ - field: Yup.string().trim(), - regex_filter: Yup.string().trim(), - auto_create_missing_groups: Yup.boolean(), - mapping: Yup.object().shape({ - [`${String}`]: Yup.array().of(Yup.string()), - }), -}); - -const IdpRoleSyncForm = ({ - roleSyncSettings, - roleMappingCount, - organization, - roles, - onSubmit, -}: IdpRoleSyncFormProps) => { - const form = useFormik({ - initialValues: { - field: roleSyncSettings?.field ?? "", - mapping: roleSyncSettings?.mapping ?? {}, - }, - validationSchema: roleyncValidationSchema, - onSubmit, - enableReinitialize: Boolean(roleSyncSettings), - }); - const [idpRoleName, setIdpRoleName] = useState(""); - const [coderRoles, setCoderRoles] = useState([]); - - const handleDelete = async (idpOrg: string) => { - const newMapping = Object.fromEntries( - Object.entries(form.values.mapping || {}).filter( - ([key]) => key !== idpOrg, - ), - ); - const newSyncSettings = { - ...form.values, - mapping: newMapping, - }; - void form.setFieldValue("mapping", newSyncSettings.mapping); - form.handleSubmit(); - }; - - const SYNC_FIELD_ID = "sync-field"; - const IDP_ROLE_NAME_ID = "idp-role-name"; - - return ( -
-
-
- -
-
- -
-
- { - void form.setFieldValue("field", event.target.value); - }} - className="min-w-72 w-72" - /> - -
-
-

- If empty, role sync is deactivated -

-
-
-
- - { - setIdpRoleName(event.target.value); - }} - /> -
-
- - ({ - label: role.display_name || role.name, - value: role.name, - }))} - hidePlaceholderWhenSelected - placeholder="Select role" - emptyIndicator={ -

- All roles selected -

- } - /> -
-
-   - -
-
-
- - {roleSyncSettings?.mapping && - Object.entries(roleSyncSettings.mapping) - .sort() - .map(([idpRole, roles]) => ( - - ))} - -
- -
-
-
-
- ); -}; - -interface RoleRowProps { - idpRole: string; - coderRoles: readonly string[]; - onDelete: (idpOrg: string) => void; -} - -const RoleRow: FC = ({ idpRole, coderRoles, onDelete }) => { - return ( - - {idpRole} - - - - - - - - ); -}; - -const TableLoader = () => { - return ( - - - - - - - - - - - - - - ); -}; - -const LegacyGroupSyncHeader: FC = () => { - return ( -

-
- Legacy group sync settings - - - - Legacy group sync settings - - These settings were configured using environment variables, and - only apply to the default organization. It is now recommended to - configure IdP sync via the CLI or the UI, which enables sync to be - configured for any organization, and for those settings to be - persisted without manually setting environment variables.{" "} - - Learn more… - - - - -
-

- ); -}; - -export const AutoCreateMissingGroupsHelpTooltip: FC = () => { - return ( - - - - - Enabling auto create missing groups will automatically create groups - returned by the OIDC provider if they do not exist in Coder. - - - - ); -}; - export default IdpSyncPageView; From 528f2d87b150e32f0fdeca7173fa747468837dc1 Mon Sep 17 00:00:00 2001 From: Jaayden Halko Date: Thu, 16 Jan 2025 19:56:01 +0000 Subject: [PATCH 22/40] fix: adding loading spinners --- .../IdpOrgSyncPage/IdpOrgSyncPageView.tsx | 9 +++++-- .../CustomRolesPage/CustomRolesPageView.tsx | 1 - .../IdpSyncPage/IdpGroupSyncForm.tsx | 24 ++++++++++++------- .../IdpSyncPage/IdpRoleSyncForm.tsx | 6 ++++- 4 files changed, 27 insertions(+), 13 deletions(-) diff --git a/site/src/pages/DeploymentSettingsPage/IdpOrgSyncPage/IdpOrgSyncPageView.tsx b/site/src/pages/DeploymentSettingsPage/IdpOrgSyncPage/IdpOrgSyncPageView.tsx index c3881376236fa..d124b422b41be 100644 --- a/site/src/pages/DeploymentSettingsPage/IdpOrgSyncPage/IdpOrgSyncPageView.tsx +++ b/site/src/pages/DeploymentSettingsPage/IdpOrgSyncPage/IdpOrgSyncPageView.tsx @@ -32,6 +32,7 @@ import { MultiSelectCombobox, type Option, } from "components/MultiSelectCombobox/MultiSelectCombobox"; +import { Spinner } from "components/Spinner/Spinner"; import { Switch } from "components/Switch/Switch"; import { useFormik } from "formik"; import { Plus, SquareArrowOutUpRight, Trash } from "lucide-react"; @@ -132,6 +133,7 @@ export const IdpOrgSyncPageView: FC = ({ form.handleSubmit(); }} > + Save
@@ -221,7 +223,9 @@ export const IdpOrgSyncPageView: FC = ({ setCoderOrgs([]); }} > - + + + Add IdP organization
@@ -267,6 +271,7 @@ export const IdpOrgSyncPageView: FC = ({ }} type="submit" > + Confirm @@ -344,7 +349,7 @@ const OrganizationRow: FC = ({
@@ -154,14 +156,16 @@ export const IdpGroupSyncForm = ({
- { - void form.setFieldValue("auto_create_missing_groups", checked); - form.handleSubmit(); - }} - /> + + { + void form.setFieldValue("auto_create_missing_groups", checked); + form.handleSubmit(); + }} + /> +
diff --git a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpRoleSyncForm.tsx b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpRoleSyncForm.tsx index 63eaa55fa8a53..dc35af135f8ba 100644 --- a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpRoleSyncForm.tsx +++ b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpRoleSyncForm.tsx @@ -8,6 +8,7 @@ import { MultiSelectCombobox, type Option, } from "components/MultiSelectCombobox/MultiSelectCombobox"; +import { Spinner } from "components/Spinner/Spinner"; import { useFormik } from "formik"; import { Plus, Trash } from "lucide-react"; import { type FC, useState } from "react"; @@ -105,6 +106,7 @@ export const IdpRoleSyncForm = ({ form.handleSubmit(); }} > + Save
@@ -168,7 +170,9 @@ export const IdpRoleSyncForm = ({ setCoderRoles([]); }} > - + + + Add IdP role
From d283fa1266bd7a178fa74ed12bb5377644b71e19 Mon Sep 17 00:00:00 2001 From: Jaayden Halko Date: Fri, 17 Jan 2025 19:13:45 +0000 Subject: [PATCH 23/40] fix: update icon button sizing to match design --- site/src/components/Button/Button.tsx | 5 +++-- .../IdpOrgSyncPage/IdpOrgSyncPageView.tsx | 3 ++- .../ManagementSettingsPage/IdpSyncPage/IdpGroupSyncForm.tsx | 3 ++- .../ManagementSettingsPage/IdpSyncPage/IdpRoleSyncForm.tsx | 3 ++- 4 files changed, 9 insertions(+), 5 deletions(-) diff --git a/site/src/components/Button/Button.tsx b/site/src/components/Button/Button.tsx index bc25b0c077017..93e1a479aa6cc 100644 --- a/site/src/components/Button/Button.tsx +++ b/site/src/components/Button/Button.tsx @@ -10,10 +10,10 @@ import { cn } from "utils/cn"; export const buttonVariants = cva( `inline-flex items-center justify-center gap-1 whitespace-nowrap border-solid rounded-md transition-colors min-w-20 - text-sm font-semibold font-medium cursor-pointer no-underline + text-sm font-semibold font-medium cursor-pointer no-underline 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]:shrink-0 [&_svg]:p-[2px]`, + [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg]:p-0.5`, { variants: { variant: { @@ -30,6 +30,7 @@ export const buttonVariants = cva( size: { lg: "h-10 px-3 py-2 [&_svg]:size-icon-lg", sm: "h-[30px] px-2 py-1.5 text-xs [&_svg]:size-icon-sm", + icon: "h-[30px] min-w-[30px] px-1 py-1.5 [&_svg]:size-icon-sm", }, }, defaultVariants: { diff --git a/site/src/pages/DeploymentSettingsPage/IdpOrgSyncPage/IdpOrgSyncPageView.tsx b/site/src/pages/DeploymentSettingsPage/IdpOrgSyncPage/IdpOrgSyncPageView.tsx index d124b422b41be..2c28ceba3264f 100644 --- a/site/src/pages/DeploymentSettingsPage/IdpOrgSyncPage/IdpOrgSyncPageView.tsx +++ b/site/src/pages/DeploymentSettingsPage/IdpOrgSyncPage/IdpOrgSyncPageView.tsx @@ -349,7 +349,8 @@ const OrganizationRow: FC = ({
-
-   +
-
-   +
diff --git a/site/src/pages/DeploymentSettingsPage/IdpOrgSyncPage/IdpOrgSyncPageView.tsx b/site/src/pages/DeploymentSettingsPage/IdpOrgSyncPage/IdpOrgSyncPageView.tsx index 1d9c8afd2667f..4a599d9b5709f 100644 --- a/site/src/pages/DeploymentSettingsPage/IdpOrgSyncPage/IdpOrgSyncPageView.tsx +++ b/site/src/pages/DeploymentSettingsPage/IdpOrgSyncPage/IdpOrgSyncPageView.tsx @@ -28,6 +28,7 @@ import { } from "components/HelpTooltip/HelpTooltip"; import { Input } from "components/Input/Input"; import { Label } from "components/Label/Label"; +import { Link } from "components/Link/Link"; import { MultiSelectCombobox, type Option, @@ -35,7 +36,7 @@ import { import { Spinner } from "components/Spinner/Spinner"; import { Switch } from "components/Switch/Switch"; import { useFormik } from "formik"; -import { Plus, SquareArrowOutUpRight, Trash } from "lucide-react"; +import { Plus, Trash } from "lucide-react"; import { type FC, useId, useState } from "react"; import { docs } from "utils/docs"; import * as Yup from "yup"; @@ -207,6 +208,7 @@ export const IdpOrgSyncPageView: FC = ({
+ + How to set up IdP organization sync + } /> diff --git a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpGroupSyncForm.tsx b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpGroupSyncForm.tsx index afae4d77bccbe..fde75ef9bc697 100644 --- a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpGroupSyncForm.tsx +++ b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpGroupSyncForm.tsx @@ -210,6 +210,7 @@ export const IdpGroupSyncForm = ({
- + {form.errors && ( +

+ {form?.errors?.field} +

+ )}
@@ -231,6 +250,11 @@ export const IdpOrgSyncPageView: FC = ({
+ {form.errors && ( +

+ {Object.values(form?.errors?.mapping || {})} +

+ )} {form.values.mapping && Object.entries(form.values.mapping) diff --git a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpGroupSyncForm.tsx b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpGroupSyncForm.tsx index fde75ef9bc697..f992f04fdf323 100644 --- a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpGroupSyncForm.tsx +++ b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpGroupSyncForm.tsx @@ -26,6 +26,7 @@ import { useFormik } from "formik"; import { Plus, Trash } from "lucide-react"; import { type FC, useId, useState } from "react"; import { docs } from "utils/docs"; +import { isUUID } from "utils/uuid"; import * as Yup from "yup"; import { ExportPolicyButton } from "./ExportPolicyButton"; import { IdpMappingTable } from "./IdpMappingTable"; @@ -45,9 +46,23 @@ const groupSyncValidationSchema = Yup.object({ field: Yup.string().trim(), regex_filter: Yup.string().trim(), auto_create_missing_groups: Yup.boolean(), - mapping: Yup.object().shape({ - [`${String}`]: Yup.array().of(Yup.string()), - }), + mapping: Yup.object() + .test( + "valid-mapping", + "Invalid group sync settings mapping structure", + (value) => { + if (!value) return true; + return Object.entries(value).every( + ([key, arr]) => + typeof key === "string" && + Array.isArray(arr) && + arr.every((item) => { + return typeof item === "string" && isUUID(item); + }), + ); + }, + ) + .default({}), }); export const IdpGroupSyncForm = ({ @@ -149,6 +164,11 @@ export const IdpGroupSyncForm = ({

+ {form.errors && ( +

+ {form?.errors?.field || form?.errors?.regex_filter} +

+ )}
@@ -233,7 +253,11 @@ export const IdpGroupSyncForm = ({
- + {form.errors && ( +

+ {Object.values(form?.errors?.mapping || {})} +

+ )}
{groupSyncSettings?.mapping && diff --git a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpRoleSyncForm.tsx b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpRoleSyncForm.tsx index f213578c49867..eb58622de7f76 100644 --- a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpRoleSyncForm.tsx +++ b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpRoleSyncForm.tsx @@ -12,6 +12,7 @@ import { Spinner } from "components/Spinner/Spinner"; import { useFormik } from "formik"; import { Plus, Trash } from "lucide-react"; import { type FC, useId, useState } from "react"; +import { isUUID } from "utils/uuid"; import * as Yup from "yup"; import { ExportPolicyButton } from "./ExportPolicyButton"; import { IdpMappingTable } from "./IdpMappingTable"; @@ -25,13 +26,27 @@ interface IdpRoleSyncFormProps { onSubmit: (data: RoleSyncSettings) => void; } -const roleyncValidationSchema = Yup.object({ +const roleSyncValidationSchema = Yup.object({ field: Yup.string().trim(), regex_filter: Yup.string().trim(), auto_create_missing_groups: Yup.boolean(), - mapping: Yup.object().shape({ - [`${String}`]: Yup.array().of(Yup.string()), - }), + mapping: Yup.object() + .test( + "valid-mapping", + "Invalid role sync settings mapping structure", + (value) => { + if (!value) return true; + return Object.entries(value).every( + ([key, arr]) => + typeof key === "string" && + Array.isArray(arr) && + arr.every((item) => { + return typeof item === "string"; + }), + ); + }, + ) + .default({}), }); export const IdpRoleSyncForm = ({ @@ -46,7 +61,7 @@ export const IdpRoleSyncForm = ({ field: roleSyncSettings?.field ?? "", mapping: roleSyncSettings?.mapping ?? {}, }, - validationSchema: roleyncValidationSchema, + validationSchema: roleSyncValidationSchema, onSubmit, enableReinitialize: Boolean(roleSyncSettings), }); @@ -113,6 +128,11 @@ export const IdpRoleSyncForm = ({ If empty, role sync is deactivated

+ {form.errors && ( +

+ {form?.errors?.field} +

+ )}
+ {form.errors && ( +

+ {Object.values(form?.errors?.mapping || {})} +

+ )} {roleSyncSettings?.mapping && Object.entries(roleSyncSettings.mapping) From 9b709ae4b2b7a546674b9c16ccf60751f013bd04 Mon Sep 17 00:00:00 2001 From: Jaayden Halko Date: Mon, 27 Jan 2025 16:20:00 +0000 Subject: [PATCH 37/40] fix: remove unnecessary async --- .../IdpOrgSyncPage/IdpOrgSyncPageView.tsx | 4 ++-- .../ManagementSettingsPage/IdpSyncPage/IdpGroupSyncForm.tsx | 6 +++--- .../ManagementSettingsPage/IdpSyncPage/IdpRoleSyncForm.tsx | 3 +-- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/site/src/pages/DeploymentSettingsPage/IdpOrgSyncPage/IdpOrgSyncPageView.tsx b/site/src/pages/DeploymentSettingsPage/IdpOrgSyncPage/IdpOrgSyncPageView.tsx index 3f7e3cc342443..57bb993231232 100644 --- a/site/src/pages/DeploymentSettingsPage/IdpOrgSyncPage/IdpOrgSyncPageView.tsx +++ b/site/src/pages/DeploymentSettingsPage/IdpOrgSyncPage/IdpOrgSyncPageView.tsx @@ -133,7 +133,7 @@ export const IdpOrgSyncPageView: FC = ({ { + onChange={(event) => { void form.setFieldValue("field", event.target.value); }} /> @@ -154,7 +154,7 @@ export const IdpOrgSyncPageView: FC = ({ { + onCheckedChange={(checked) => { if (!checked) { setIsDialogOpen(true); } else { diff --git a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpGroupSyncForm.tsx b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpGroupSyncForm.tsx index f992f04fdf323..21d483a6c87c8 100644 --- a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpGroupSyncForm.tsx +++ b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpGroupSyncForm.tsx @@ -133,7 +133,7 @@ export const IdpGroupSyncForm = ({ { + onChange={(event) => { void form.setFieldValue("field", event.target.value); }} className="w-72" @@ -142,7 +142,7 @@ export const IdpGroupSyncForm = ({ { + onChange={(event) => { void form.setFieldValue("regex_filter", event.target.value); }} className="min-w-40" @@ -175,7 +175,7 @@ export const IdpGroupSyncForm = ({ { + onCheckedChange={(checked) => { void form.setFieldValue("auto_create_missing_groups", checked); form.handleSubmit(); }} diff --git a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpRoleSyncForm.tsx b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpRoleSyncForm.tsx index eb58622de7f76..63a2450669afd 100644 --- a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpRoleSyncForm.tsx +++ b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpRoleSyncForm.tsx @@ -12,7 +12,6 @@ import { Spinner } from "components/Spinner/Spinner"; import { useFormik } from "formik"; import { Plus, Trash } from "lucide-react"; import { type FC, useId, useState } from "react"; -import { isUUID } from "utils/uuid"; import * as Yup from "yup"; import { ExportPolicyButton } from "./ExportPolicyButton"; import { IdpMappingTable } from "./IdpMappingTable"; @@ -105,7 +104,7 @@ export const IdpRoleSyncForm = ({ { + onChange={(event) => { void form.setFieldValue("field", event.target.value); }} className="w-72" From bee57839ddb6ed03765d2cffd021b47b34c80acc Mon Sep 17 00:00:00 2001 From: Jaayden Halko Date: Mon, 27 Jan 2025 16:54:46 +0000 Subject: [PATCH 38/40] fix: remove useEffect --- .../MultiSelectCombobox/MultiSelectCombobox.tsx | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/site/src/components/MultiSelectCombobox/MultiSelectCombobox.tsx b/site/src/components/MultiSelectCombobox/MultiSelectCombobox.tsx index 66509c5a2b887..702be6a64d582 100644 --- a/site/src/components/MultiSelectCombobox/MultiSelectCombobox.tsx +++ b/site/src/components/MultiSelectCombobox/MultiSelectCombobox.tsx @@ -430,11 +430,9 @@ export const MultiSelectCombobox = forwardRef< return undefined; }; - useEffect(() => { - if (inputRef.current && inputProps?.id) { - inputRef.current.id = inputProps?.id; - } - }, [inputProps?.id]); + if (inputRef.current && inputProps?.id) { + inputRef.current.id = inputProps?.id; + } const fixedOptions = selected.filter((s) => s.fixed); From d3b64b2f70d298a19a7403f8da86d14dae628d56 Mon Sep 17 00:00:00 2001 From: Jaayden Halko Date: Mon, 27 Jan 2025 17:03:14 +0000 Subject: [PATCH 39/40] fix: error display --- .../IdpSyncPage/IdpGroupSyncForm.tsx | 8 ++++---- .../IdpSyncPage/IdpRoleSyncForm.tsx | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpGroupSyncForm.tsx b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpGroupSyncForm.tsx index 21d483a6c87c8..70650250e3f17 100644 --- a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpGroupSyncForm.tsx +++ b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpGroupSyncForm.tsx @@ -164,9 +164,9 @@ export const IdpGroupSyncForm = ({

- {form.errors && ( + {form.errors.field || form.errors.regex_filter && (

- {form?.errors?.field || form?.errors?.regex_filter} + {form.errors.field || form.errors.regex_filter}

)} @@ -253,9 +253,9 @@ export const IdpGroupSyncForm = ({ - {form.errors && ( + {form.errors.mapping && (

- {Object.values(form?.errors?.mapping || {})} + {Object.values(form.errors.mapping || {})}

)}
diff --git a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpRoleSyncForm.tsx b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpRoleSyncForm.tsx index 63a2450669afd..9e945cf3a4654 100644 --- a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpRoleSyncForm.tsx +++ b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpRoleSyncForm.tsx @@ -127,9 +127,9 @@ export const IdpRoleSyncForm = ({ If empty, role sync is deactivated

- {form.errors && ( + {form.errors.field && (

- {form?.errors?.field} + {form.errors.field}

)}
@@ -197,9 +197,9 @@ export const IdpRoleSyncForm = ({
- {form.errors && ( + {form.errors.mapping && (

- {Object.values(form?.errors?.mapping || {})} + {Object.values(form.errors.mapping || {})}

)} From 0362c16cd7089f757761d616e05c6639793e01c8 Mon Sep 17 00:00:00 2001 From: Jaayden Halko Date: Mon, 27 Jan 2025 17:13:48 +0000 Subject: [PATCH 40/40] fix: format --- .../IdpOrgSyncPage/IdpOrgSyncPageView.tsx | 8 ++++---- .../IdpSyncPage/IdpGroupSyncForm.tsx | 11 ++++++----- .../IdpSyncPage/IdpRoleSyncForm.tsx | 4 +--- 3 files changed, 11 insertions(+), 12 deletions(-) diff --git a/site/src/pages/DeploymentSettingsPage/IdpOrgSyncPage/IdpOrgSyncPageView.tsx b/site/src/pages/DeploymentSettingsPage/IdpOrgSyncPage/IdpOrgSyncPageView.tsx index 57bb993231232..7ed1b85e8c9dd 100644 --- a/site/src/pages/DeploymentSettingsPage/IdpOrgSyncPage/IdpOrgSyncPageView.tsx +++ b/site/src/pages/DeploymentSettingsPage/IdpOrgSyncPage/IdpOrgSyncPageView.tsx @@ -179,9 +179,9 @@ export const IdpOrgSyncPageView: FC = ({

- {form.errors && ( + {form.errors.field && (

- {form?.errors?.field} + {form.errors.field}

)}
@@ -250,9 +250,9 @@ export const IdpOrgSyncPageView: FC = ({
- {form.errors && ( + {form.errors.mapping && (

- {Object.values(form?.errors?.mapping || {})} + {Object.values(form.errors.mapping || {})}

)} diff --git a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpGroupSyncForm.tsx b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpGroupSyncForm.tsx index 70650250e3f17..2f1c0be7fa602 100644 --- a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpGroupSyncForm.tsx +++ b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpGroupSyncForm.tsx @@ -164,11 +164,12 @@ export const IdpGroupSyncForm = ({

- {form.errors.field || form.errors.regex_filter && ( -

- {form.errors.field || form.errors.regex_filter} -

- )} + {form.errors.field || + (form.errors.regex_filter && ( +

+ {form.errors.field || form.errors.regex_filter} +

+ ))}
diff --git a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpRoleSyncForm.tsx b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpRoleSyncForm.tsx index 9e945cf3a4654..ffe68e33e2ecc 100644 --- a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpRoleSyncForm.tsx +++ b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpRoleSyncForm.tsx @@ -128,9 +128,7 @@ export const IdpRoleSyncForm = ({

{form.errors.field && ( -

- {form.errors.field} -

+

{form.errors.field}

)}