From a40ba7d3de288bc71101ef03c81aa02afe7179a8 Mon Sep 17 00:00:00 2001
From: Jaayden Halko
Date: Tue, 17 Sep 2024 21:22:10 +0000
Subject: [PATCH 01/13] feat: idp sync initial commit
---
site/src/api/api.ts | 21 ++++++++
site/src/api/queries/organizations.ts | 26 ++++++++++
.../AppearanceSettingsPageView.tsx | 2 +-
.../IdpSyncPage/IdpSyncPage.tsx | 49 ++++++++++++++++---
.../IdpSyncPage/IdpSyncPageView.tsx | 33 ++++++++-----
5 files changed, 111 insertions(+), 20 deletions(-)
diff --git a/site/src/api/api.ts b/site/src/api/api.ts
index e0781846ff4fe..b44b03c1a5456 100644
--- a/site/src/api/api.ts
+++ b/site/src/api/api.ts
@@ -704,6 +704,27 @@ class ApiMethods {
return response.data;
};
+ getGroupIdpSyncSettingsByOrganization = async (
+ organization: string,
+ ): Promise => {
+ const response = await this.axios.get(
+ `/api/v2/organizations/${organization}/settings/idpsync/groups`,
+ );
+ return response.data;
+ };
+
+ /**
+ * @param organization Can be the organization's ID or name
+ */
+ getRoleIdpSyncSettingsByOrganization = async (
+ organization: string,
+ ): Promise => {
+ const response = await this.axios.get(
+ `/api/v2/organizations/${organization}/settings/idpsync/roles`,
+ );
+ return response.data;
+ };
+
getTemplate = async (templateId: string): Promise => {
const response = await this.axios.get(
`/api/v2/templates/${templateId}`,
diff --git a/site/src/api/queries/organizations.ts b/site/src/api/queries/organizations.ts
index 8e1143800b869..f0a5cca33cb53 100644
--- a/site/src/api/queries/organizations.ts
+++ b/site/src/api/queries/organizations.ts
@@ -141,6 +141,32 @@ export const provisionerDaemonGroups = (organization: string) => {
};
};
+export const getGroupIdpSyncSettingsKey = (organization: string) => [
+ "organization",
+ organization,
+ "groupIdpSyncSettings",
+];
+
+export const groupIdpSyncSettings = (organization: string) => {
+ return {
+ queryKey: getGroupIdpSyncSettingsKey(organization),
+ queryFn: () => API.getGroupIdpSyncSettingsByOrganization(organization),
+ };
+};
+
+export const getRoleIdpSyncSettingsKey = (organization: string) => [
+ "organization",
+ organization,
+ "roleIdpSyncSettings",
+];
+
+export const roleIdpSyncSettings = (organization: string) => {
+ return {
+ queryKey: getRoleIdpSyncSettingsKey(organization),
+ queryFn: () => API.getRoleIdpSyncSettingsByOrganization(organization),
+ };
+};
+
/**
* Fetch permissions for a single organization.
*
diff --git a/site/src/pages/DeploySettingsPage/AppearanceSettingsPage/AppearanceSettingsPageView.tsx b/site/src/pages/DeploySettingsPage/AppearanceSettingsPage/AppearanceSettingsPageView.tsx
index 0c5d470c027fe..2060c1b5f5660 100644
--- a/site/src/pages/DeploySettingsPage/AppearanceSettingsPage/AppearanceSettingsPageView.tsx
+++ b/site/src/pages/DeploySettingsPage/AppearanceSettingsPage/AppearanceSettingsPageView.tsx
@@ -74,7 +74,7 @@ export const AppearanceSettingsPageView: FC<
diff --git a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPage.tsx b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPage.tsx
index 93b4e59455409..1fe628a11b2e0 100644
--- a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPage.tsx
+++ b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPage.tsx
@@ -6,11 +6,20 @@ import { SettingsHeader } from "components/SettingsHeader/SettingsHeader";
import { Stack } from "components/Stack/Stack";
import type { FC } from "react";
import { Helmet } from "react-helmet-async";
-import { Link as RouterLink } from "react-router-dom";
+import { Link as RouterLink, useParams } from "react-router-dom";
import { docs } from "utils/docs";
import { pageTitle } from "utils/page";
import { IdpSyncHelpTooltip } from "./IdpSyncHelpTooltip";
import IdpSyncPageView from "./IdpSyncPageView";
+import {
+ organizationsPermissions,
+ groupIdpSyncSettings,
+ roleIdpSyncSettings,
+} from "api/queries/organizations";
+import { useQuery } from "react-query";
+import { useOrganizationSettings } from "../ManagementSettingsLayout";
+import { Loader } from "components/Loader/Loader";
+import { EmptyState } from "components/EmptyState/EmptyState";
const mockOIDCConfig = {
allow_signups: true,
@@ -45,19 +54,39 @@ const mockOIDCConfig = {
};
export const IdpSyncPage: FC = () => {
+ const { organization: organizationName } = useParams() as {
+ organization: string;
+ };
+
// feature visibility and permissions to be implemented when integrating with backend
// const feats = useFeatureVisibility();
// const { organization: organizationName } = useParams() as {
// organization: string;
// };
- // const { organizations } = useOrganizationSettings();
- // const organization = organizations?.find((o) => o.name === organizationName);
- // const permissionsQuery = useQuery(organizationPermissions(organization?.id));
+ const { organizations } = useOrganizationSettings();
+ const organization = organizations?.find((o) => o.name === organizationName);
+ const permissionsQuery = useQuery(
+ organizationsPermissions(organizations?.map((o) => o.id)),
+ );
+ const groupIdpSyncSettingsQuery = useQuery(
+ groupIdpSyncSettings(organizationName),
+ );
+ const roleIdpSyncSettingsQuery = useQuery(
+ roleIdpSyncSettings(organizationName),
+ );
// const permissions = permissionsQuery.data;
- // if (!permissions) {
- // return ;
- // }
+ if (!organization) {
+ return ;
+ }
+
+ if (
+ permissionsQuery.isLoading ||
+ groupIdpSyncSettingsQuery.isLoading ||
+ roleIdpSyncSettingsQuery.isLoading
+ ) {
+ return ;
+ }
return (
<>
@@ -91,7 +120,11 @@ export const IdpSyncPage: FC = () => {
-
+
>
);
};
diff --git a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.tsx b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.tsx
index 08c165d6c7e91..7c7225d0373e8 100644
--- a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.tsx
+++ b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.tsx
@@ -9,7 +9,11 @@ import TableCell from "@mui/material/TableCell";
import TableContainer from "@mui/material/TableContainer";
import TableHead from "@mui/material/TableHead";
import TableRow from "@mui/material/TableRow";
-import type { OIDCConfig } from "api/typesGenerated";
+import type {
+ OIDCConfig,
+ GroupSyncSettings,
+ RoleSyncSettings,
+} from "api/typesGenerated";
import { ChooseOne, Cond } from "components/Conditionals/ChooseOne";
import { EmptyState } from "components/EmptyState/EmptyState";
import { Paywall } from "components/Paywall/Paywall";
@@ -25,16 +29,17 @@ import { docs } from "utils/docs";
export type IdpSyncPageViewProps = {
oidcConfig: OIDCConfig | undefined;
+ groupSyncSettings: GroupSyncSettings | undefined;
+ roleSyncSettings: RoleSyncSettings | undefined;
};
-export const IdpSyncPageView: FC = ({ oidcConfig }) => {
+export const IdpSyncPageView: FC = ({
+ oidcConfig,
+ groupSyncSettings,
+ roleSyncSettings,
+}) => {
const theme = useTheme();
- const {
- groups_field,
- user_role_field,
- group_regex_filter,
- group_auto_create,
- } = oidcConfig || {};
+ const { user_role_field } = oidcConfig || {};
return (
<>
@@ -54,16 +59,22 @@ export const IdpSyncPageView: FC = ({ oidcConfig }) => {
From 7ff9a07c17edd0dfd8b41a0ac9479444b7cbf5bf Mon Sep 17 00:00:00 2001
From: Jaayden Halko
Date: Fri, 20 Sep 2024 21:02:07 +0000
Subject: [PATCH 02/13] fix: hookup backend data for groups and roles
---
.../IdpSyncPage/IdpSyncPage.tsx | 58 ++++--------
.../IdpSyncPage/IdpSyncPageView.stories.tsx | 19 +++-
.../IdpSyncPage/IdpSyncPageView.tsx | 80 +++++++++-------
.../IdpSyncPage/PillList.tsx | 91 +++++++++++++++++++
site/src/testHelpers/entities.ts | 67 +++++++-------
5 files changed, 206 insertions(+), 109 deletions(-)
create mode 100644 site/src/pages/ManagementSettingsPage/IdpSyncPage/PillList.tsx
diff --git a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPage.tsx b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPage.tsx
index 1fe628a11b2e0..a647ff80c66d0 100644
--- a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPage.tsx
+++ b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPage.tsx
@@ -1,57 +1,27 @@
import AddIcon from "@mui/icons-material/AddOutlined";
import LaunchOutlined from "@mui/icons-material/LaunchOutlined";
import Button from "@mui/material/Button";
+import { groupsByOrganization } from "api/queries/groups";
+import {
+ groupIdpSyncSettings,
+ organizationsPermissions,
+ roleIdpSyncSettings,
+} from "api/queries/organizations";
+import { EmptyState } from "components/EmptyState/EmptyState";
import { FeatureStageBadge } from "components/FeatureStageBadge/FeatureStageBadge";
+import { Loader } from "components/Loader/Loader";
import { SettingsHeader } from "components/SettingsHeader/SettingsHeader";
import { Stack } from "components/Stack/Stack";
+import { useDashboard } from "modules/dashboard/useDashboard";
import type { FC } from "react";
import { Helmet } from "react-helmet-async";
+import { useQuery } from "react-query";
import { Link as RouterLink, useParams } from "react-router-dom";
import { docs } from "utils/docs";
import { pageTitle } from "utils/page";
+import { useOrganizationSettings } from "../ManagementSettingsLayout";
import { IdpSyncHelpTooltip } from "./IdpSyncHelpTooltip";
import IdpSyncPageView from "./IdpSyncPageView";
-import {
- organizationsPermissions,
- groupIdpSyncSettings,
- roleIdpSyncSettings,
-} from "api/queries/organizations";
-import { useQuery } from "react-query";
-import { useOrganizationSettings } from "../ManagementSettingsLayout";
-import { Loader } from "components/Loader/Loader";
-import { EmptyState } from "components/EmptyState/EmptyState";
-
-const mockOIDCConfig = {
- allow_signups: true,
- client_id: "test",
- client_secret: "test",
- client_key_file: "test",
- client_cert_file: "test",
- email_domain: [],
- issuer_url: "test",
- scopes: [],
- ignore_email_verified: true,
- username_field: "",
- name_field: "",
- email_field: "",
- auth_url_params: {},
- ignore_user_info: true,
- organization_field: "",
- organization_mapping: {},
- organization_assign_default: true,
- group_auto_create: false,
- group_regex_filter: "^Coder-.*$",
- group_allow_list: [],
- groups_field: "groups",
- group_mapping: { group1: "developers", group2: "admin", group3: "auditors" },
- user_role_field: "roles",
- user_role_mapping: { role1: ["role1", "role2"] },
- user_roles_default: [],
- sign_in_text: "",
- icon_url: "",
- signups_disabled_text: "string",
- skip_issuer_checks: true,
-};
export const IdpSyncPage: FC = () => {
const { organization: organizationName } = useParams() as {
@@ -64,6 +34,7 @@ export const IdpSyncPage: FC = () => {
// organization: string;
// };
const { organizations } = useOrganizationSettings();
+
const organization = organizations?.find((o) => o.name === organizationName);
const permissionsQuery = useQuery(
organizationsPermissions(organizations?.map((o) => o.id)),
@@ -71,9 +42,12 @@ export const IdpSyncPage: FC = () => {
const groupIdpSyncSettingsQuery = useQuery(
groupIdpSyncSettings(organizationName),
);
+
+ const groupsQuery = useQuery(groupsByOrganization(organizationName));
const roleIdpSyncSettingsQuery = useQuery(
roleIdpSyncSettings(organizationName),
);
+
// const permissions = permissionsQuery.data;
if (!organization) {
@@ -121,9 +95,9 @@ export const IdpSyncPage: FC = () => {
>
);
diff --git a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.stories.tsx b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.stories.tsx
index 47952edc61c95..a27dc85e48d79 100644
--- a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.stories.tsx
+++ b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.stories.tsx
@@ -1,5 +1,10 @@
import type { Meta, StoryObj } from "@storybook/react";
-import { MockOIDCConfig } from "testHelpers/entities";
+import {
+ MockGroup,
+ MockGroup2,
+ MockGroupSyncSettings,
+ MockRoleSyncSettings,
+} from "testHelpers/entities";
import { IdpSyncPageView } from "./IdpSyncPageView";
const meta: Meta = {
@@ -11,9 +16,17 @@ export default meta;
type Story = StoryObj;
export const Empty: Story = {
- args: { oidcConfig: undefined },
+ args: {
+ groupSyncSettings: undefined,
+ roleSyncSettings: undefined,
+ groups: [MockGroup, MockGroup2],
+ },
};
export const Default: Story = {
- args: { oidcConfig: MockOIDCConfig },
+ args: {
+ groupSyncSettings: MockGroupSyncSettings,
+ roleSyncSettings: MockRoleSyncSettings,
+ groups: [MockGroup, MockGroup2],
+ },
};
diff --git a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.tsx b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.tsx
index 7c7225d0373e8..424837a356fc2 100644
--- a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.tsx
+++ b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.tsx
@@ -10,7 +10,7 @@ import TableContainer from "@mui/material/TableContainer";
import TableHead from "@mui/material/TableHead";
import TableRow from "@mui/material/TableRow";
import type {
- OIDCConfig,
+ Group,
GroupSyncSettings,
RoleSyncSettings,
} from "api/typesGenerated";
@@ -26,20 +26,32 @@ import {
import type { FC } from "react";
import { MONOSPACE_FONT_FAMILY } from "theme/constants";
import { docs } from "utils/docs";
+import { PillList } from "./PillList";
export type IdpSyncPageViewProps = {
- oidcConfig: OIDCConfig | undefined;
groupSyncSettings: GroupSyncSettings | undefined;
roleSyncSettings: RoleSyncSettings | undefined;
+ groups: Group[] | undefined;
};
export const IdpSyncPageView: FC = ({
- oidcConfig,
groupSyncSettings,
roleSyncSettings,
+ groups,
}) => {
- const theme = useTheme();
- const { user_role_field } = oidcConfig || {};
+ // const theme = useTheme();
+
+ const groupsMap = new Map();
+ if (groups) {
+ for (const group of groups) {
+ groupsMap.set(group.id, group.display_name || group.name);
+ }
+ }
+
+ const getGroupNames = (groupIds: readonly string[]) => {
+ return groupIds.map((groupId) => groupsMap.get(groupId) || groupId);
+ };
+
return (
<>
@@ -67,13 +79,13 @@ export const IdpSyncPageView: FC = ({
fieldText={
typeof groupSyncSettings?.regex_filter === "string"
? groupSyncSettings?.regex_filter
- : ""
+ : "none"
}
/>
@@ -83,7 +95,7 @@ export const IdpSyncPageView: FC = ({
@@ -91,38 +103,38 @@ export const IdpSyncPageView: FC = ({
- {oidcConfig?.user_role_mapping &&
- Object.entries(oidcConfig.user_role_mapping)
+ {groupSyncSettings?.mapping &&
+ Object.entries(groupSyncSettings.mapping)
.sort()
- .map(([idpRole, roles]) => (
- (
+
))}
- {oidcConfig?.user_role_mapping &&
- Object.entries(oidcConfig.group_mapping)
+ {roleSyncSettings?.mapping &&
+ Object.entries(roleSyncSettings.mapping)
.sort()
- .map(([idpGroup, group]) => (
- (
+
))}
@@ -226,28 +238,32 @@ const IdpMappingTable: FC = ({
interface GroupRowProps {
idpGroup: string;
- coderGroup: string;
+ coderGroup: readonly string[];
}
const GroupRow: FC = ({ idpGroup, coderGroup }) => {
return (
{idpGroup}
- {coderGroup}
+
+
+
);
};
interface RoleRowProps {
idpRole: string;
- coderRoles: ReadonlyArray;
+ coderRoles: readonly string[];
}
const RoleRow: FC = ({ idpRole, coderRoles }) => {
return (
{idpRole}
- coderRoles Placeholder
+
+
+
);
};
diff --git a/site/src/pages/ManagementSettingsPage/IdpSyncPage/PillList.tsx b/site/src/pages/ManagementSettingsPage/IdpSyncPage/PillList.tsx
new file mode 100644
index 0000000000000..02b64d1c1a8ab
--- /dev/null
+++ b/site/src/pages/ManagementSettingsPage/IdpSyncPage/PillList.tsx
@@ -0,0 +1,91 @@
+import { type Interpolation, type Theme, useTheme } from "@emotion/react";
+import Stack from "@mui/material/Stack";
+import { Pill } from "components/Pill/Pill";
+import {
+ Popover,
+ PopoverContent,
+ PopoverTrigger,
+} from "components/Popover/Popover";
+import type { FC } from "react";
+
+interface PillListProps {
+ roles: readonly string[];
+}
+
+export const PillList: FC = ({ roles }) => {
+ return (
+
+ {roles.length > 0 ? (
+ {roles[0]}
+ ) : (
+ None
+ )}
+
+ {roles.length > 1 && }
+
+ );
+};
+
+type OverflowPillProps = {
+ roles: string[];
+};
+
+const OverflowPill: FC = ({ roles }) => {
+ const theme = useTheme();
+
+ return (
+
+
+
+ +{roles.length} more
+
+
+
+
+ {roles.map((role) => (
+
+ {role}
+
+ ))}
+
+
+ );
+};
+
+const styles = {
+ pill: (theme) => ({
+ backgroundColor: theme.experimental.pillDefault.background,
+ borderColor: theme.experimental.pillDefault.outline,
+ color: theme.experimental.pillDefault.text,
+ width: "fit-content",
+ }),
+} satisfies Record>;
diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts
index 9d9f3192fd9c6..d134f84203405 100644
--- a/site/src/testHelpers/entities.ts
+++ b/site/src/testHelpers/entities.ts
@@ -451,38 +451,6 @@ export const MockAssignableSiteRoles = [
assignableRole(MockAuditorRole, true),
];
-export const MockOIDCConfig: TypesGen.OIDCConfig = {
- allow_signups: true,
- client_id: "test",
- client_secret: "test",
- client_key_file: "test",
- client_cert_file: "test",
- email_domain: [],
- issuer_url: "test",
- scopes: [],
- ignore_email_verified: true,
- username_field: "",
- name_field: "",
- email_field: "",
- auth_url_params: {},
- ignore_user_info: true,
- organization_field: "",
- organization_mapping: {},
- organization_assign_default: true,
- group_auto_create: false,
- group_regex_filter: "^Coder-.*$",
- group_allow_list: [],
- groups_field: "groups",
- group_mapping: { group1: "developers", group2: "admin", group3: "auditors" },
- user_role_field: "roles",
- user_role_mapping: { role1: ["role1", "role2"] },
- user_roles_default: [],
- sign_in_text: "",
- icon_url: "",
- signups_disabled_text: "string",
- skip_issuer_checks: true,
-};
-
export const MockMemberPermissions = {
viewAuditLog: false,
};
@@ -2632,6 +2600,27 @@ export const MockWorkspaceQuota: TypesGen.WorkspaceQuota = {
budget: 100,
};
+export const MockGroupSyncSettings: TypesGen.GroupSyncSettings = {
+ field: "group-test",
+ mapping: {
+ "idp-group-1": [
+ "fbd2116a-8961-4954-87ae-e4575bd29ce0",
+ "13de3eb4-9b4f-49e7-b0f8-0c3728a0d2e2",
+ ],
+ "idp-group-2": ["fbd2116a-8961-4954-87ae-e4575bd29ce0"],
+ },
+ regex_filter: "@[a-zA-Z0-9_]+",
+ auto_create_missing_groups: false,
+};
+
+export const MockRoleSyncSettings: TypesGen.RoleSyncSettings = {
+ field: "role-test",
+ mapping: {
+ "idp-role-1": ["admin", "developer"],
+ "idp-role-2": ["auditor"],
+ },
+};
+
export const MockGroup: TypesGen.Group = {
id: "fbd2116a-8961-4954-87ae-e4575bd29ce0",
name: "Front-End",
@@ -2646,6 +2635,20 @@ export const MockGroup: TypesGen.Group = {
total_member_count: 2,
};
+export const MockGroup2: TypesGen.Group = {
+ id: "13de3eb4-9b4f-49e7-b0f8-0c3728a0d2e2",
+ name: "developer",
+ display_name: "",
+ avatar_url: "https://example.com",
+ organization_id: MockOrganization.id,
+ organization_name: MockOrganization.name,
+ organization_display_name: MockOrganization.display_name,
+ members: [MockUser, MockUser2],
+ quota_allowance: 5,
+ source: "user",
+ total_member_count: 2,
+};
+
const MockEveryoneGroup: TypesGen.Group = {
// The "Everyone" group must have the same ID as a the organization it belongs
// to.
From 6e303f42df0b1a25e03bc06df49697a85236eb89 Mon Sep 17 00:00:00 2001
From: Jaayden Halko
Date: Fri, 20 Sep 2024 22:28:52 +0000
Subject: [PATCH 03/13] chore: cleanup
---
site/src/api/queries/organizations.ts | 7 +++++
.../IdpSyncPage/IdpSyncPage.tsx | 22 ++++++++++------
.../IdpSyncPage/IdpSyncPageView.tsx | 26 ++++++++++++++++---
.../ManagementSettingsPage/SidebarView.tsx | 2 +-
4 files changed, 45 insertions(+), 12 deletions(-)
diff --git a/site/src/api/queries/organizations.ts b/site/src/api/queries/organizations.ts
index f0a5cca33cb53..6bd8358022aff 100644
--- a/site/src/api/queries/organizations.ts
+++ b/site/src/api/queries/organizations.ts
@@ -269,6 +269,13 @@ export const organizationsPermissions = (
},
action: "read",
},
+ viewIdpSyncSettings: {
+ object: {
+ resource_type: "idpsync_settings",
+ organization_id: organizationId,
+ },
+ action: "read",
+ },
});
// The endpoint takes a flat array, so to avoid collisions prepend each
diff --git a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPage.tsx b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPage.tsx
index a647ff80c66d0..dcca449cf9043 100644
--- a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPage.tsx
+++ b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPage.tsx
@@ -7,6 +7,7 @@ import {
organizationsPermissions,
roleIdpSyncSettings,
} from "api/queries/organizations";
+import { ErrorAlert } from "components/Alert/ErrorAlert";
import { EmptyState } from "components/EmptyState/EmptyState";
import { FeatureStageBadge } from "components/FeatureStageBadge/FeatureStageBadge";
import { Loader } from "components/Loader/Loader";
@@ -27,12 +28,6 @@ export const IdpSyncPage: FC = () => {
const { organization: organizationName } = useParams() as {
organization: string;
};
-
- // feature visibility and permissions to be implemented when integrating with backend
- // const feats = useFeatureVisibility();
- // const { organization: organizationName } = useParams() as {
- // organization: string;
- // };
const { organizations } = useOrganizationSettings();
const organization = organizations?.find((o) => o.name === organizationName);
@@ -48,8 +43,6 @@ export const IdpSyncPage: FC = () => {
roleIdpSyncSettings(organizationName),
);
- // const permissions = permissionsQuery.data;
-
if (!organization) {
return ;
}
@@ -62,6 +55,19 @@ export const IdpSyncPage: FC = () => {
return ;
}
+ const error =
+ groupIdpSyncSettingsQuery.error ||
+ roleIdpSyncSettingsQuery.error ||
+ groupsQuery.error;
+ if (
+ error ||
+ !groupIdpSyncSettingsQuery.data ||
+ !roleIdpSyncSettingsQuery.data ||
+ !groupsQuery.data
+ ) {
+ return ;
+ }
+
return (
<>
diff --git a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.tsx b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.tsx
index 424837a356fc2..ff983529ea126 100644
--- a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.tsx
+++ b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.tsx
@@ -1,5 +1,4 @@
import type { Interpolation, Theme } from "@emotion/react";
-import { useTheme } from "@emotion/react";
import LaunchOutlined from "@mui/icons-material/LaunchOutlined";
import Button from "@mui/material/Button";
import Skeleton from "@mui/material/Skeleton";
@@ -39,8 +38,6 @@ export const IdpSyncPageView: FC = ({
roleSyncSettings,
groups,
}) => {
- // const theme = useTheme();
-
const groupsMap = new Map();
if (groups) {
for (const group of groups) {
@@ -101,6 +98,29 @@ export const IdpSyncPageView: FC = ({
+ {groupSyncSettings?.mapping && roleSyncSettings?.mapping && (
+ ({
+ margin: 0,
+ fontSize: 13,
+ paddingBottom: 14,
+ color: theme.palette.text.secondary,
+ "& strong": {
+ color: theme.palette.text.primary,
+ },
+ })}
+ >
+ Showing{" "}
+
+ {Object.entries(groupSyncSettings?.mapping).length}
+ {" "}
+ groups and{" "}
+
+ {Object.entries(roleSyncSettings?.mapping).length}
+ {" "}
+ provisioners
+
+ )}
)}
- {organization.permissions.editMembers && (
+ {organization.permissions.viewIdpSyncSettings && (
From 310726b62b30bb7d3e8220da499057feb8c3291e Mon Sep 17 00:00:00 2001
From: Jaayden Halko
Date: Sat, 21 Sep 2024 19:24:33 +0000
Subject: [PATCH 04/13] feat: separate groups and roles into tabs
---
.../IdpSyncPage/IdpSyncPage.tsx | 35 +-
.../IdpSyncPage/IdpSyncPageView.tsx | 301 ++++++++++--------
2 files changed, 189 insertions(+), 147 deletions(-)
diff --git a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPage.tsx b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPage.tsx
index dcca449cf9043..f3ed2c973cf56 100644
--- a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPage.tsx
+++ b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPage.tsx
@@ -1,10 +1,8 @@
-import AddIcon from "@mui/icons-material/AddOutlined";
import LaunchOutlined from "@mui/icons-material/LaunchOutlined";
import Button from "@mui/material/Button";
import { groupsByOrganization } from "api/queries/groups";
import {
groupIdpSyncSettings,
- organizationsPermissions,
roleIdpSyncSettings,
} from "api/queries/organizations";
import { ErrorAlert } from "components/Alert/ErrorAlert";
@@ -13,11 +11,10 @@ import { FeatureStageBadge } from "components/FeatureStageBadge/FeatureStageBadg
import { Loader } from "components/Loader/Loader";
import { SettingsHeader } from "components/SettingsHeader/SettingsHeader";
import { Stack } from "components/Stack/Stack";
-import { useDashboard } from "modules/dashboard/useDashboard";
import type { FC } from "react";
import { Helmet } from "react-helmet-async";
-import { useQuery } from "react-query";
-import { Link as RouterLink, useParams } from "react-router-dom";
+import { useQueries } from "react-query";
+import { useParams } from "react-router-dom";
import { docs } from "utils/docs";
import { pageTitle } from "utils/page";
import { useOrganizationSettings } from "../ManagementSettingsLayout";
@@ -29,26 +26,23 @@ export const IdpSyncPage: FC = () => {
organization: string;
};
const { organizations } = useOrganizationSettings();
-
const organization = organizations?.find((o) => o.name === organizationName);
- const permissionsQuery = useQuery(
- organizationsPermissions(organizations?.map((o) => o.id)),
- );
- const groupIdpSyncSettingsQuery = useQuery(
- groupIdpSyncSettings(organizationName),
- );
-
- const groupsQuery = useQuery(groupsByOrganization(organizationName));
- const roleIdpSyncSettingsQuery = useQuery(
- roleIdpSyncSettings(organizationName),
- );
if (!organization) {
return ;
}
+ const [groupIdpSyncSettingsQuery, roleIdpSyncSettingsQuery, groupsQuery] =
+ useQueries({
+ queries: [
+ groupIdpSyncSettings(organizationName),
+ roleIdpSyncSettings(organizationName),
+ groupsByOrganization(organizationName),
+ ],
+ });
+
if (
- permissionsQuery.isLoading ||
+ groupsQuery.isLoading ||
groupIdpSyncSettingsQuery.isLoading ||
roleIdpSyncSettingsQuery.isLoading
) {
@@ -81,7 +75,7 @@ export const IdpSyncPage: FC = () => {
>
}
badges={}
/>
@@ -94,9 +88,6 @@ export const IdpSyncPage: FC = () => {
>
Setup IdP Sync
- } to="export">
- Export Policy
-
diff --git a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.tsx b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.tsx
index ff983529ea126..ae7cc9ce4804b 100644
--- a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.tsx
+++ b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.tsx
@@ -1,4 +1,5 @@
import type { Interpolation, Theme } from "@emotion/react";
+import AddIcon from "@mui/icons-material/AddOutlined";
import LaunchOutlined from "@mui/icons-material/LaunchOutlined";
import Button from "@mui/material/Button";
import Skeleton from "@mui/material/Skeleton";
@@ -22,7 +23,9 @@ import {
TableLoaderSkeleton,
TableRowSkeleton,
} from "components/TableLoader/TableLoader";
+import { TabLink, Tabs, TabsList } from "components/Tabs/Tabs";
import type { FC } from "react";
+import { useSearchParams } from "react-router-dom";
import { MONOSPACE_FONT_FAMILY } from "theme/constants";
import { docs } from "utils/docs";
import { PillList } from "./PillList";
@@ -38,6 +41,7 @@ export const IdpSyncPageView: FC = ({
roleSyncSettings,
groups,
}) => {
+ const [searchParams] = useSearchParams();
const groupsMap = new Map();
if (groups) {
for (const group of groups) {
@@ -49,6 +53,15 @@ export const IdpSyncPageView: FC = ({
return groupIds.map((groupId) => groupsMap.get(groupId) || groupId);
};
+ const tab = searchParams.get("tab") || "groups";
+
+ const groupMappingCount = groupSyncSettings?.mapping
+ ? Object.entries(groupSyncSettings.mapping).length
+ : 0;
+ const roleMappingCount = roleSyncSettings?.mapping
+ ? Object.entries(roleSyncSettings.mapping).length
+ : 0;
+
return (
<>
@@ -60,105 +73,124 @@ export const IdpSyncPageView: FC = ({
/>
-
- {/* Semantically fieldset is used for forms. In the future this screen will allow
- updates to these fields in a form */}
-
-
-
- {groupSyncSettings?.mapping && roleSyncSettings?.mapping && (
- ({
- margin: 0,
- fontSize: 13,
- paddingBottom: 14,
- color: theme.palette.text.secondary,
- "& strong": {
- color: theme.palette.text.primary,
- },
- })}
- >
- Showing{" "}
-
- {Object.entries(groupSyncSettings?.mapping).length}
- {" "}
- groups and{" "}
-
- {Object.entries(roleSyncSettings?.mapping).length}
- {" "}
- provisioners
-
- )}
-
-
+
- {groupSyncSettings?.mapping &&
- Object.entries(groupSyncSettings.mapping)
- .sort()
- .map(([idpGroup, groups]) => (
-
+
+ Group Sync Settings
+
+
+ Role Sync Settings
+
+
+
+ {tab === "groups" ? (
+ <>
+
+
+
- ))}
-
-
- {roleSyncSettings?.mapping &&
- Object.entries(roleSyncSettings.mapping)
- .sort()
- .map(([idpRole, roles]) => (
-
+
- ))}
-
-
+
+
+
+
+ }
+ // to="export"
+ href={docs("/admin/auth#group-sync-enterprise")}
+ >
+ Export Policy
+
+
+
+
+ {groupSyncSettings?.mapping &&
+ Object.entries(groupSyncSettings.mapping)
+ .sort()
+ .map(([idpGroup, groups]) => (
+
+ ))}
+
+
+ >
+ ) : (
+ <>
+
+
+
+
+
+ }
+ // to="export"
+ href={docs("/admin/auth#group-sync-enterprise")}
+ >
+ Export Policy
+
+
+
+ {roleSyncSettings?.mapping &&
+ Object.entries(roleSyncSettings.mapping)
+ .sort()
+ .map(([idpRole, roles]) => (
+
+ ))}
+
+ >
+ )}
+ >
>
@@ -178,27 +210,50 @@ const IdpField: FC = ({
}) => {
return (
- {name}
-
- {fieldText ||
- (showStatusIndicator && (
-
- ))}
-
+ {name}
+ {fieldText ? (
+ {fieldText}
+ ) : (
+ showStatusIndicator && (
+
+ )
+ )}
);
};
+interface TableRowCountProps {
+ count: number;
+ type: string;
+}
+
+const TableRowCount: FC = ({ count, type }) => {
+ return (
+ ({
+ margin: 0,
+ fontSize: 13,
+ color: theme.palette.text.secondary,
+ "& strong": {
+ color: theme.palette.text.primary,
+ },
+ })}
+ >
+ Showing {count} {type}
+
+ );
+};
+
interface IdpMappingTableProps {
type: "Role" | "Group";
isEmpty: boolean;
@@ -237,7 +292,9 @@ const IdpMappingTable: FC = ({
}
component="a"
- href={docs("/admin/auth#group-sync-enterprise")}
+ href={docs(
+ `/admin/auth#${type.toLowerCase()}-sync-enterprise`,
+ )}
target="_blank"
>
How to setup IdP {type} sync
@@ -307,22 +364,16 @@ const TableLoader = () => {
};
const styles = {
- field: (theme) => ({
+ fieldText: (theme) => ({
color: theme.palette.text.secondary,
fontFamily: MONOSPACE_FONT_FAMILY,
}),
fields: () => ({
- marginBottom: "60px",
- }),
- legend: () => ({
- padding: "0px 6px",
- fontWeight: 600,
+ marginBottom: 20,
+ marginLeft: 16,
}),
- box: (theme) => ({
- border: "1px solid",
- borderColor: theme.palette.divider,
- padding: "0px 20px",
- borderRadius: 8,
+ tableInfo: () => ({
+ marginBottom: 16,
}),
} satisfies Record>;
From 615c8d38709a8fd27470441f2f88bfa7f2993a4a Mon Sep 17 00:00:00 2001
From: Jaayden Halko
Date: Sun, 22 Sep 2024 20:58:15 +0000
Subject: [PATCH 05/13] feat: implement export policy button
---
.../CustomRolesPage/CustomRolesPageView.tsx | 4 +-
.../IdpSyncPage/ExportPolicyButton.tsx | 45 +++++++++++++++
.../IdpSyncPage/IdpSyncPage.tsx | 1 +
.../IdpSyncPage/IdpSyncPageView.tsx | 56 +++++++++++--------
4 files changed, 81 insertions(+), 25 deletions(-)
create mode 100644 site/src/pages/ManagementSettingsPage/IdpSyncPage/ExportPolicyButton.tsx
diff --git a/site/src/pages/ManagementSettingsPage/CustomRolesPage/CustomRolesPageView.tsx b/site/src/pages/ManagementSettingsPage/CustomRolesPage/CustomRolesPageView.tsx
index 9be45d16318c0..70ac152652d84 100644
--- a/site/src/pages/ManagementSettingsPage/CustomRolesPage/CustomRolesPageView.tsx
+++ b/site/src/pages/ManagementSettingsPage/CustomRolesPage/CustomRolesPageView.tsx
@@ -29,12 +29,12 @@ import { Link as RouterLink, useNavigate } from "react-router-dom";
import { docs } from "utils/docs";
import { PermissionPillsList } from "./PermissionPillsList";
-export type CustomRolesPageViewProps = {
+interface CustomRolesPageViewProps {
roles: Role[] | undefined;
onDeleteRole: (role: Role) => void;
canAssignOrgRole: boolean;
isCustomRolesEnabled: boolean;
-};
+}
export const CustomRolesPageView: FC = ({
roles,
diff --git a/site/src/pages/ManagementSettingsPage/IdpSyncPage/ExportPolicyButton.tsx b/site/src/pages/ManagementSettingsPage/IdpSyncPage/ExportPolicyButton.tsx
new file mode 100644
index 0000000000000..a6eb4fa25b36f
--- /dev/null
+++ b/site/src/pages/ManagementSettingsPage/IdpSyncPage/ExportPolicyButton.tsx
@@ -0,0 +1,45 @@
+import DownloadOutlined from "@mui/icons-material/DownloadOutlined";
+import Button from "@mui/material/Button";
+import type { Organization } from "api/typesGenerated";
+import { displayError } from "components/GlobalSnackbar/utils";
+import { saveAs } from "file-saver";
+import { type FC, useState } from "react";
+
+interface DownloadPolicyButtonProps {
+ policy: string | null;
+ type: "groups" | "roles";
+ organization: Organization;
+}
+
+export const ExportPolicyButton: FC = ({
+ policy,
+ type,
+ organization,
+}) => {
+ const [isDownloading, setIsDownloading] = useState(false);
+
+ return (
+ }
+ disabled={!policy || isDownloading}
+ onClick={async () => {
+ if (policy) {
+ try {
+ setIsDownloading(true);
+ const file = new Blob([policy], {
+ type: "application/json",
+ });
+ saveAs(file, `${organization.name}_${type}-policy.json`);
+ } catch (e) {
+ console.error(e);
+ displayError("Failed to export policy json");
+ } finally {
+ setIsDownloading(false);
+ }
+ }
+ }}
+ >
+ Export Policy
+
+ );
+};
diff --git a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPage.tsx b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPage.tsx
index f3ed2c973cf56..a41395be33cb0 100644
--- a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPage.tsx
+++ b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPage.tsx
@@ -95,6 +95,7 @@ export const IdpSyncPage: FC = () => {
groupSyncSettings={groupIdpSyncSettingsQuery.data}
roleSyncSettings={roleIdpSyncSettingsQuery.data}
groups={groupsQuery.data}
+ organization={organization}
/>
>
);
diff --git a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.tsx b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.tsx
index ae7cc9ce4804b..c5c6aceb64aaf 100644
--- a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.tsx
+++ b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.tsx
@@ -1,5 +1,4 @@
import type { Interpolation, Theme } from "@emotion/react";
-import AddIcon from "@mui/icons-material/AddOutlined";
import LaunchOutlined from "@mui/icons-material/LaunchOutlined";
import Button from "@mui/material/Button";
import Skeleton from "@mui/material/Skeleton";
@@ -12,6 +11,7 @@ import TableRow from "@mui/material/TableRow";
import type {
Group,
GroupSyncSettings,
+ Organization,
RoleSyncSettings,
} from "api/typesGenerated";
import { ChooseOne, Cond } from "components/Conditionals/ChooseOne";
@@ -28,18 +28,21 @@ import type { FC } from "react";
import { useSearchParams } from "react-router-dom";
import { MONOSPACE_FONT_FAMILY } from "theme/constants";
import { docs } from "utils/docs";
+import { ExportPolicyButton } from "./ExportPolicyButton";
import { PillList } from "./PillList";
-export type IdpSyncPageViewProps = {
+interface IdpSyncPageViewProps {
groupSyncSettings: GroupSyncSettings | undefined;
roleSyncSettings: RoleSyncSettings | undefined;
groups: Group[] | undefined;
-};
+ organization: Organization;
+}
export const IdpSyncPageView: FC = ({
groupSyncSettings,
roleSyncSettings,
groups,
+ organization,
}) => {
const [searchParams] = useSearchParams();
const groupsMap = new Map();
@@ -62,6 +65,15 @@ export const IdpSyncPageView: FC = ({
? Object.entries(roleSyncSettings.mapping).length
: 0;
+ const rolePolicy =
+ roleSyncSettings?.field && roleSyncSettings.mapping
+ ? JSON.stringify(roleSyncSettings, null, 2)
+ : null;
+ const groupPolicy =
+ groupSyncSettings?.field && groupSyncSettings.mapping
+ ? JSON.stringify(groupSyncSettings, null, 2)
+ : null;
+
return (
<>
@@ -77,7 +89,7 @@ export const IdpSyncPageView: FC = ({
@@ -121,14 +133,11 @@ export const IdpSyncPageView: FC = ({
css={styles.tableInfo}
>
- }
- // to="export"
- href={docs("/admin/auth#group-sync-enterprise")}
- >
- Export Policy
-
+
= ({
css={styles.tableInfo}
>
- }
- // to="export"
- href={docs("/admin/auth#group-sync-enterprise")}
- >
- Export Policy
-
+
= ({
}) => {
return (
- {name}
+ {name}
{fieldText ? (
{fieldText}
) : (
@@ -365,12 +371,16 @@ const TableLoader = () => {
const styles = {
fieldText: (theme) => ({
- color: theme.palette.text.secondary,
fontFamily: MONOSPACE_FONT_FAMILY,
+ whiteSpace: "nowrap",
+ }),
+ fieldLabel: (theme) => ({
+ color: theme.palette.text.secondary,
}),
fields: () => ({
- marginBottom: 20,
+ marginBottom: 16,
marginLeft: 16,
+ fontSize: 14,
}),
tableInfo: () => ({
marginBottom: 16,
From 45f4d1365c2e45e4f829e3a4f00e27eb179716b9 Mon Sep 17 00:00:00 2001
From: Jaayden Halko
Date: Sun, 22 Sep 2024 21:29:57 +0000
Subject: [PATCH 06/13] feat: handle missing groups
---
.../{PillList.tsx => IdpPillList.tsx} | 20 ++++++++++++++++---
.../IdpSyncPage/IdpSyncPageView.tsx | 6 +++---
2 files changed, 20 insertions(+), 6 deletions(-)
rename site/src/pages/ManagementSettingsPage/IdpSyncPage/{PillList.tsx => IdpPillList.tsx} (77%)
diff --git a/site/src/pages/ManagementSettingsPage/IdpSyncPage/PillList.tsx b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpPillList.tsx
similarity index 77%
rename from site/src/pages/ManagementSettingsPage/IdpSyncPage/PillList.tsx
rename to site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpPillList.tsx
index 02b64d1c1a8ab..170611786bc5f 100644
--- a/site/src/pages/ManagementSettingsPage/IdpSyncPage/PillList.tsx
+++ b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpPillList.tsx
@@ -12,11 +12,16 @@ interface PillListProps {
roles: readonly string[];
}
-export const PillList: FC = ({ roles }) => {
+const UUID =
+ /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
+
+export const IdpPillList: FC = ({ roles }) => {
return (
{roles.length > 0 ? (
- {roles[0]}
+
+ {roles[0]}
+
) : (
None
)}
@@ -72,7 +77,10 @@ const OverflowPill: FC = ({ roles }) => {
}}
>
{roles.map((role) => (
-
+
{role}
))}
@@ -88,4 +96,10 @@ const styles = {
color: theme.experimental.pillDefault.text,
width: "fit-content",
}),
+ errorPill: (theme) => ({
+ backgroundColor: theme.roles.error.background,
+ borderColor: theme.roles.error.outline,
+ color: theme.roles.error.text,
+ width: "fit-content",
+ }),
} satisfies Record>;
diff --git a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.tsx b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.tsx
index c5c6aceb64aaf..e7a37be6e2610 100644
--- a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.tsx
+++ b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.tsx
@@ -29,7 +29,7 @@ import { useSearchParams } from "react-router-dom";
import { MONOSPACE_FONT_FAMILY } from "theme/constants";
import { docs } from "utils/docs";
import { ExportPolicyButton } from "./ExportPolicyButton";
-import { PillList } from "./PillList";
+import { IdpPillList } from "./IdpPillList";
interface IdpSyncPageViewProps {
groupSyncSettings: GroupSyncSettings | undefined;
@@ -329,7 +329,7 @@ const GroupRow: FC = ({ idpGroup, coderGroup }) => {
{idpGroup}
-
+
);
@@ -345,7 +345,7 @@ const RoleRow: FC = ({ idpRole, coderRoles }) => {
{idpRole}
-
+
);
From 080b0329a0390511dc4e2ea96c3d4c71a70cee2c Mon Sep 17 00:00:00 2001
From: Jaayden Halko
Date: Sun, 22 Sep 2024 22:23:17 +0000
Subject: [PATCH 07/13] chore: add story for missing groups
---
.../IdpSyncPage/IdpSyncPageView.stories.tsx | 8 ++++++++
1 file changed, 8 insertions(+)
diff --git a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.stories.tsx b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.stories.tsx
index a27dc85e48d79..bd01494daff21 100644
--- a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.stories.tsx
+++ b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.stories.tsx
@@ -30,3 +30,11 @@ export const Default: Story = {
groups: [MockGroup, MockGroup2],
},
};
+
+export const MissingGroups: Story = {
+ args: {
+ groupSyncSettings: MockGroupSyncSettings,
+ roleSyncSettings: MockRoleSyncSettings,
+ groups: [],
+ },
+};
From 8e1e021c2ab7634d146e6c0535ae234350f3068e Mon Sep 17 00:00:00 2001
From: Jaayden Halko
Date: Sun, 22 Sep 2024 22:24:04 +0000
Subject: [PATCH 08/13] chore: add stories for export policy button
---
.../ExportPolicyButton.stories.tsx | 69 +++++++++++++++++++
.../IdpSyncPage/ExportPolicyButton.tsx | 4 +-
2 files changed, 72 insertions(+), 1 deletion(-)
create mode 100644 site/src/pages/ManagementSettingsPage/IdpSyncPage/ExportPolicyButton.stories.tsx
diff --git a/site/src/pages/ManagementSettingsPage/IdpSyncPage/ExportPolicyButton.stories.tsx b/site/src/pages/ManagementSettingsPage/IdpSyncPage/ExportPolicyButton.stories.tsx
new file mode 100644
index 0000000000000..e3bbd66ea02aa
--- /dev/null
+++ b/site/src/pages/ManagementSettingsPage/IdpSyncPage/ExportPolicyButton.stories.tsx
@@ -0,0 +1,69 @@
+import type { Meta, StoryObj } from "@storybook/react";
+import { expect, fn, userEvent, waitFor, within } from "@storybook/test";
+import {
+ MockGroupSyncSettings,
+ MockOrganization,
+ MockRoleSyncSettings,
+} from "testHelpers/entities";
+import { ExportPolicyButton } from "./ExportPolicyButton";
+
+const meta: Meta = {
+ title: "modules/resources/ExportPolicyButton",
+ component: ExportPolicyButton,
+ args: {
+ policy: JSON.stringify(MockGroupSyncSettings, null, 2),
+ type: "groups",
+ organization: MockOrganization,
+ },
+};
+
+export default meta;
+type Story = StoryObj;
+
+export const Default: Story = {};
+
+export const ClickExportGroupPolicy: Story = {
+ args: {
+ policy: JSON.stringify(MockGroupSyncSettings, null, 2),
+ type: "groups",
+ organization: MockOrganization,
+ download: fn(),
+ },
+ play: async ({ canvasElement, args }) => {
+ const canvas = within(canvasElement);
+ await userEvent.click(
+ canvas.getByRole("button", { name: "Export Policy" }),
+ );
+ await waitFor(() =>
+ expect(args.download).toHaveBeenCalledWith(
+ expect.anything(),
+ `${MockOrganization.name}_groups-policy.json`,
+ ),
+ );
+ const blob: Blob = (args.download as jest.Mock).mock.calls[0][0];
+ await expect(blob.type).toEqual("application/json");
+ },
+};
+
+export const ClickExportRolePolicy: Story = {
+ args: {
+ policy: JSON.stringify(MockRoleSyncSettings, null, 2),
+ type: "roles",
+ organization: MockOrganization,
+ download: fn(),
+ },
+ play: async ({ canvasElement, args }) => {
+ const canvas = within(canvasElement);
+ await userEvent.click(
+ canvas.getByRole("button", { name: "Export Policy" }),
+ );
+ await waitFor(() =>
+ expect(args.download).toHaveBeenCalledWith(
+ expect.anything(),
+ `${MockOrganization.name}_roles-policy.json`,
+ ),
+ );
+ const blob: Blob = (args.download as jest.Mock).mock.calls[0][0];
+ await expect(blob.type).toEqual("application/json");
+ },
+};
diff --git a/site/src/pages/ManagementSettingsPage/IdpSyncPage/ExportPolicyButton.tsx b/site/src/pages/ManagementSettingsPage/IdpSyncPage/ExportPolicyButton.tsx
index a6eb4fa25b36f..6bdc74c5d1323 100644
--- a/site/src/pages/ManagementSettingsPage/IdpSyncPage/ExportPolicyButton.tsx
+++ b/site/src/pages/ManagementSettingsPage/IdpSyncPage/ExportPolicyButton.tsx
@@ -9,12 +9,14 @@ interface DownloadPolicyButtonProps {
policy: string | null;
type: "groups" | "roles";
organization: Organization;
+ download?: (file: Blob, filename: string) => void;
}
export const ExportPolicyButton: FC = ({
policy,
type,
organization,
+ download = saveAs,
}) => {
const [isDownloading, setIsDownloading] = useState(false);
@@ -29,7 +31,7 @@ export const ExportPolicyButton: FC = ({
const file = new Blob([policy], {
type: "application/json",
});
- saveAs(file, `${organization.name}_${type}-policy.json`);
+ download(file, `${organization.name}_${type}-policy.json`);
} catch (e) {
console.error(e);
displayError("Failed to export policy json");
From c8636195cb3716f81444e219dba1947e0c8af4c0 Mon Sep 17 00:00:00 2001
From: Jaayden Halko
Date: Mon, 23 Sep 2024 20:30:44 +0000
Subject: [PATCH 09/13] fix: updates for PR review
---
site/src/api/api.ts | 3 ++
site/src/api/queries/organizations.ts | 4 +-
.../ExportPolicyButton.stories.tsx | 6 +--
.../IdpSyncPage/ExportPolicyButton.tsx | 24 ++++++---
.../IdpSyncPage/IdpPillList.tsx | 4 +-
.../IdpSyncPage/IdpSyncPage.tsx | 16 ++++--
.../IdpSyncPage/IdpSyncPageView.tsx | 54 ++++++++-----------
7 files changed, 60 insertions(+), 51 deletions(-)
diff --git a/site/src/api/api.ts b/site/src/api/api.ts
index b44b03c1a5456..103a3c50e7900 100644
--- a/site/src/api/api.ts
+++ b/site/src/api/api.ts
@@ -704,6 +704,9 @@ class ApiMethods {
return response.data;
};
+ /**
+ * @param organization Can be the organization's ID or name
+ */
getGroupIdpSyncSettingsByOrganization = async (
organization: string,
): Promise => {
diff --git a/site/src/api/queries/organizations.ts b/site/src/api/queries/organizations.ts
index 6bd8358022aff..d1df8f409dcdf 100644
--- a/site/src/api/queries/organizations.ts
+++ b/site/src/api/queries/organizations.ts
@@ -142,7 +142,7 @@ export const provisionerDaemonGroups = (organization: string) => {
};
export const getGroupIdpSyncSettingsKey = (organization: string) => [
- "organization",
+ "organizations",
organization,
"groupIdpSyncSettings",
];
@@ -155,7 +155,7 @@ export const groupIdpSyncSettings = (organization: string) => {
};
export const getRoleIdpSyncSettingsKey = (organization: string) => [
- "organization",
+ "organizations",
organization,
"roleIdpSyncSettings",
];
diff --git a/site/src/pages/ManagementSettingsPage/IdpSyncPage/ExportPolicyButton.stories.tsx b/site/src/pages/ManagementSettingsPage/IdpSyncPage/ExportPolicyButton.stories.tsx
index e3bbd66ea02aa..0da470de6a580 100644
--- a/site/src/pages/ManagementSettingsPage/IdpSyncPage/ExportPolicyButton.stories.tsx
+++ b/site/src/pages/ManagementSettingsPage/IdpSyncPage/ExportPolicyButton.stories.tsx
@@ -11,7 +11,7 @@ const meta: Meta = {
title: "modules/resources/ExportPolicyButton",
component: ExportPolicyButton,
args: {
- policy: JSON.stringify(MockGroupSyncSettings, null, 2),
+ syncSettings: MockGroupSyncSettings,
type: "groups",
organization: MockOrganization,
},
@@ -24,7 +24,7 @@ export const Default: Story = {};
export const ClickExportGroupPolicy: Story = {
args: {
- policy: JSON.stringify(MockGroupSyncSettings, null, 2),
+ syncSettings: MockGroupSyncSettings,
type: "groups",
organization: MockOrganization,
download: fn(),
@@ -47,7 +47,7 @@ export const ClickExportGroupPolicy: Story = {
export const ClickExportRolePolicy: Story = {
args: {
- policy: JSON.stringify(MockRoleSyncSettings, null, 2),
+ syncSettings: MockRoleSyncSettings,
type: "roles",
organization: MockOrganization,
download: fn(),
diff --git a/site/src/pages/ManagementSettingsPage/IdpSyncPage/ExportPolicyButton.tsx b/site/src/pages/ManagementSettingsPage/IdpSyncPage/ExportPolicyButton.tsx
index 6bdc74c5d1323..9cb4cb06e7385 100644
--- a/site/src/pages/ManagementSettingsPage/IdpSyncPage/ExportPolicyButton.tsx
+++ b/site/src/pages/ManagementSettingsPage/IdpSyncPage/ExportPolicyButton.tsx
@@ -1,34 +1,44 @@
import DownloadOutlined from "@mui/icons-material/DownloadOutlined";
import Button from "@mui/material/Button";
-import type { Organization } from "api/typesGenerated";
+import type {
+ GroupSyncSettings,
+ Organization,
+ RoleSyncSettings,
+} from "api/typesGenerated";
import { displayError } from "components/GlobalSnackbar/utils";
import { saveAs } from "file-saver";
-import { type FC, useState } from "react";
+import { type FC, useMemo, useState } from "react";
interface DownloadPolicyButtonProps {
- policy: string | null;
+ syncSettings: RoleSyncSettings | GroupSyncSettings | undefined;
type: "groups" | "roles";
organization: Organization;
download?: (file: Blob, filename: string) => void;
}
export const ExportPolicyButton: FC = ({
- policy,
+ syncSettings,
type,
organization,
download = saveAs,
}) => {
const [isDownloading, setIsDownloading] = useState(false);
+ const policyJSON = useMemo(() => {
+ return syncSettings?.field && syncSettings.mapping
+ ? JSON.stringify(syncSettings, null, 2)
+ : null;
+ }, [syncSettings]);
+
return (
}
- disabled={!policy || isDownloading}
+ disabled={!policyJSON || isDownloading}
onClick={async () => {
- if (policy) {
+ if (policyJSON) {
try {
setIsDownloading(true);
- const file = new Blob([policy], {
+ const file = new Blob([policyJSON], {
type: "application/json",
});
download(file, `${organization.name}_${type}-policy.json`);
diff --git a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpPillList.tsx b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpPillList.tsx
index 170611786bc5f..8de97f5555114 100644
--- a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpPillList.tsx
+++ b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpPillList.tsx
@@ -31,9 +31,9 @@ export const IdpPillList: FC = ({ roles }) => {
);
};
-type OverflowPillProps = {
+interface OverflowPillProps {
roles: string[];
-};
+}
const OverflowPill: FC = ({ roles }) => {
const theme = useTheme();
diff --git a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPage.tsx b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPage.tsx
index a41395be33cb0..42a700f926633 100644
--- a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPage.tsx
+++ b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPage.tsx
@@ -28,10 +28,6 @@ export const IdpSyncPage: FC = () => {
const { organizations } = useOrganizationSettings();
const organization = organizations?.find((o) => o.name === organizationName);
- if (!organization) {
- return ;
- }
-
const [groupIdpSyncSettingsQuery, roleIdpSyncSettingsQuery, groupsQuery] =
useQueries({
queries: [
@@ -41,6 +37,10 @@ export const IdpSyncPage: FC = () => {
],
});
+ if (!organization) {
+ return ;
+ }
+
if (
groupsQuery.isLoading ||
groupIdpSyncSettingsQuery.isLoading ||
@@ -62,6 +62,13 @@ export const IdpSyncPage: FC = () => {
return ;
}
+ const groupsMap = new Map();
+ if (groupsQuery.data) {
+ for (const group of groupsQuery.data) {
+ groupsMap.set(group.id, group.display_name || group.name);
+ }
+ }
+
return (
<>
@@ -95,6 +102,7 @@ export const IdpSyncPage: FC = () => {
groupSyncSettings={groupIdpSyncSettingsQuery.data}
roleSyncSettings={roleIdpSyncSettingsQuery.data}
groups={groupsQuery.data}
+ groupsMap={groupsMap}
organization={organization}
/>
>
diff --git a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.tsx b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.tsx
index e7a37be6e2610..cc803fb9c386b 100644
--- a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.tsx
+++ b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.tsx
@@ -35,6 +35,7 @@ interface IdpSyncPageViewProps {
groupSyncSettings: GroupSyncSettings | undefined;
roleSyncSettings: RoleSyncSettings | undefined;
groups: Group[] | undefined;
+ groupsMap: Map;
organization: Organization;
}
@@ -42,15 +43,10 @@ export const IdpSyncPageView: FC = ({
groupSyncSettings,
roleSyncSettings,
groups,
+ groupsMap,
organization,
}) => {
const [searchParams] = useSearchParams();
- const groupsMap = new Map();
- if (groups) {
- for (const group of groups) {
- groupsMap.set(group.id, group.display_name || group.name);
- }
- }
const getGroupNames = (groupIds: readonly string[]) => {
return groupIds.map((groupId) => groupsMap.get(groupId) || groupId);
@@ -65,15 +61,6 @@ export const IdpSyncPageView: FC = ({
? Object.entries(roleSyncSettings.mapping).length
: 0;
- const rolePolicy =
- roleSyncSettings?.field && roleSyncSettings.mapping
- ? JSON.stringify(roleSyncSettings, null, 2)
- : null;
- const groupPolicy =
- groupSyncSettings?.field && groupSyncSettings.mapping
- ? JSON.stringify(groupSyncSettings, null, 2)
- : null;
-
return (
<>
@@ -85,13 +72,8 @@ export const IdpSyncPageView: FC = ({
/>
- <>
-
+
+
Group Sync Settings
@@ -108,13 +90,13 @@ export const IdpSyncPageView: FC = ({
@@ -134,7 +116,7 @@ export const IdpSyncPageView: FC = ({
>
@@ -163,7 +145,7 @@ export const IdpSyncPageView: FC = ({
= ({
>
@@ -196,7 +178,7 @@ export const IdpSyncPageView: FC = ({
>
)}
- >
+
>
@@ -206,21 +188,27 @@ export const IdpSyncPageView: FC = ({
interface IdpFieldProps {
name: string;
fieldText: string | undefined;
- showStatusIndicator?: boolean;
+ showDisabled?: boolean;
}
const IdpField: FC = ({
name,
fieldText,
- showStatusIndicator = false,
+ showDisabled = false,
}) => {
return (
-
+
{name}
{fieldText ? (
{fieldText}
) : (
- showStatusIndicator && (
+ showDisabled && (
({
fontFamily: MONOSPACE_FONT_FAMILY,
whiteSpace: "nowrap",
+ paddingBottom: ".02rem",
}),
fieldLabel: (theme) => ({
color: theme.palette.text.secondary,
}),
fields: () => ({
- marginBottom: 16,
marginLeft: 16,
fontSize: 14,
}),
From 41631f0ba15533354df331e00ff189ef09761e3c Mon Sep 17 00:00:00 2001
From: Jaayden Halko
Date: Tue, 24 Sep 2024 00:39:00 +0000
Subject: [PATCH 10/13] chore: update tests
---
.../IdpSyncPage/ExportPolicyButton.stories.tsx | 10 ++++++++--
1 file changed, 8 insertions(+), 2 deletions(-)
diff --git a/site/src/pages/ManagementSettingsPage/IdpSyncPage/ExportPolicyButton.stories.tsx b/site/src/pages/ManagementSettingsPage/IdpSyncPage/ExportPolicyButton.stories.tsx
index 0da470de6a580..da2f5140d3d73 100644
--- a/site/src/pages/ManagementSettingsPage/IdpSyncPage/ExportPolicyButton.stories.tsx
+++ b/site/src/pages/ManagementSettingsPage/IdpSyncPage/ExportPolicyButton.stories.tsx
@@ -40,8 +40,11 @@ export const ClickExportGroupPolicy: Story = {
`${MockOrganization.name}_groups-policy.json`,
),
);
- const blob: Blob = (args.download as jest.Mock).mock.calls[0][0];
+ const blob: Blob = (args.download as jest.Mock).mock.lastCall[0];
await expect(blob.type).toEqual("application/json");
+ await expect(await blob.text()).toEqual(
+ JSON.stringify(MockGroupSyncSettings, null, 2),
+ );
},
};
@@ -63,7 +66,10 @@ export const ClickExportRolePolicy: Story = {
`${MockOrganization.name}_roles-policy.json`,
),
);
- const blob: Blob = (args.download as jest.Mock).mock.calls[0][0];
+ const blob: Blob = (args.download as jest.Mock).mock.lastCall[0];
await expect(blob.type).toEqual("application/json");
+ await expect(await blob.text()).toEqual(
+ JSON.stringify(MockRoleSyncSettings, null, 2),
+ );
},
};
From 3db9f487f0d38207c5bc48102833f10e942d4bf7 Mon Sep 17 00:00:00 2001
From: Jaayden Halko
Date: Tue, 24 Sep 2024 00:39:20 +0000
Subject: [PATCH 11/13] chore: document uuid regex
---
.../pages/ManagementSettingsPage/IdpSyncPage/IdpPillList.tsx | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpPillList.tsx b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpPillList.tsx
index 8de97f5555114..4f489747f0bba 100644
--- a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpPillList.tsx
+++ b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpPillList.tsx
@@ -12,8 +12,9 @@ interface PillListProps {
roles: readonly string[];
}
+// used to check if the role is a UUID
const UUID =
- /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
+ /^[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
export const IdpPillList: FC = ({ roles }) => {
return (
From 431beea1c078333c261aa5271518dc9c021628d9 Mon Sep 17 00:00:00 2001
From: Jaayden Halko
Date: Tue, 24 Sep 2024 00:47:58 +0000
Subject: [PATCH 12/13] chore: remove unused
---
.../pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.tsx | 1 -
1 file changed, 1 deletion(-)
diff --git a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.tsx b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.tsx
index cc803fb9c386b..f73395e254e8b 100644
--- a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.tsx
+++ b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.tsx
@@ -42,7 +42,6 @@ interface IdpSyncPageViewProps {
export const IdpSyncPageView: FC = ({
groupSyncSettings,
roleSyncSettings,
- groups,
groupsMap,
organization,
}) => {
From f166784b54cc7f0432d0f779a49f184dc865a33d Mon Sep 17 00:00:00 2001
From: Jaayden Halko
Date: Tue, 24 Sep 2024 01:12:20 +0000
Subject: [PATCH 13/13] fix: fix stories
---
.../IdpSyncPage/IdpSyncPageView.stories.tsx | 15 +++++++++++----
site/src/testHelpers/entities.ts | 13 +++++++++++++
2 files changed, 24 insertions(+), 4 deletions(-)
diff --git a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.stories.tsx b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.stories.tsx
index bd01494daff21..646e64d763b86 100644
--- a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.stories.tsx
+++ b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.stories.tsx
@@ -3,6 +3,7 @@ import {
MockGroup,
MockGroup2,
MockGroupSyncSettings,
+ MockGroupSyncSettings2,
MockRoleSyncSettings,
} from "testHelpers/entities";
import { IdpSyncPageView } from "./IdpSyncPageView";
@@ -15,11 +16,17 @@ const meta: Meta = {
export default meta;
type Story = StoryObj;
+const groupsMap = new Map();
+
+for (const group of [MockGroup, MockGroup2]) {
+ groupsMap.set(group.id, group.display_name || group.name);
+}
+
export const Empty: Story = {
args: {
groupSyncSettings: undefined,
roleSyncSettings: undefined,
- groups: [MockGroup, MockGroup2],
+ groupsMap: undefined,
},
};
@@ -27,14 +34,14 @@ export const Default: Story = {
args: {
groupSyncSettings: MockGroupSyncSettings,
roleSyncSettings: MockRoleSyncSettings,
- groups: [MockGroup, MockGroup2],
+ groupsMap,
},
};
export const MissingGroups: Story = {
args: {
- groupSyncSettings: MockGroupSyncSettings,
+ groupSyncSettings: MockGroupSyncSettings2,
roleSyncSettings: MockRoleSyncSettings,
- groups: [],
+ groupsMap,
},
};
diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts
index d134f84203405..c3395fa042bda 100644
--- a/site/src/testHelpers/entities.ts
+++ b/site/src/testHelpers/entities.ts
@@ -2613,6 +2613,19 @@ export const MockGroupSyncSettings: TypesGen.GroupSyncSettings = {
auto_create_missing_groups: false,
};
+export const MockGroupSyncSettings2: TypesGen.GroupSyncSettings = {
+ field: "group-test",
+ mapping: {
+ "idp-group-1": [
+ "fbd2116a-8961-4954-87ae-e4575bd29ce0",
+ "13de3eb4-9b4f-49e7-b0f8-0c3728a0d2e3",
+ ],
+ "idp-group-2": ["fbd2116a-8961-4954-87ae-e4575bd29ce2"],
+ },
+ regex_filter: "@[a-zA-Z0-9_]+",
+ auto_create_missing_groups: false,
+};
+
export const MockRoleSyncSettings: TypesGen.RoleSyncSettings = {
field: "role-test",
mapping: {