diff --git a/site/src/api/api.ts b/site/src/api/api.ts
index e0781846ff4fe..103a3c50e7900 100644
--- a/site/src/api/api.ts
+++ b/site/src/api/api.ts
@@ -704,6 +704,30 @@ class ApiMethods {
return response.data;
};
+ /**
+ * @param organization Can be the organization's ID or name
+ */
+ 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..d1df8f409dcdf 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) => [
+ "organizations",
+ organization,
+ "groupIdpSyncSettings",
+];
+
+export const groupIdpSyncSettings = (organization: string) => {
+ return {
+ queryKey: getGroupIdpSyncSettingsKey(organization),
+ queryFn: () => API.getGroupIdpSyncSettingsByOrganization(organization),
+ };
+};
+
+export const getRoleIdpSyncSettingsKey = (organization: string) => [
+ "organizations",
+ organization,
+ "roleIdpSyncSettings",
+];
+
+export const roleIdpSyncSettings = (organization: string) => {
+ return {
+ queryKey: getRoleIdpSyncSettingsKey(organization),
+ queryFn: () => API.getRoleIdpSyncSettingsByOrganization(organization),
+ };
+};
+
/**
* Fetch permissions for a single organization.
*
@@ -243,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/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/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.stories.tsx b/site/src/pages/ManagementSettingsPage/IdpSyncPage/ExportPolicyButton.stories.tsx
new file mode 100644
index 0000000000000..da2f5140d3d73
--- /dev/null
+++ b/site/src/pages/ManagementSettingsPage/IdpSyncPage/ExportPolicyButton.stories.tsx
@@ -0,0 +1,75 @@
+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: {
+ syncSettings: MockGroupSyncSettings,
+ type: "groups",
+ organization: MockOrganization,
+ },
+};
+
+export default meta;
+type Story = StoryObj;
+
+export const Default: Story = {};
+
+export const ClickExportGroupPolicy: Story = {
+ args: {
+ syncSettings: MockGroupSyncSettings,
+ 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.lastCall[0];
+ await expect(blob.type).toEqual("application/json");
+ await expect(await blob.text()).toEqual(
+ JSON.stringify(MockGroupSyncSettings, null, 2),
+ );
+ },
+};
+
+export const ClickExportRolePolicy: Story = {
+ args: {
+ syncSettings: MockRoleSyncSettings,
+ 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.lastCall[0];
+ await expect(blob.type).toEqual("application/json");
+ await expect(await blob.text()).toEqual(
+ JSON.stringify(MockRoleSyncSettings, null, 2),
+ );
+ },
+};
diff --git a/site/src/pages/ManagementSettingsPage/IdpSyncPage/ExportPolicyButton.tsx b/site/src/pages/ManagementSettingsPage/IdpSyncPage/ExportPolicyButton.tsx
new file mode 100644
index 0000000000000..9cb4cb06e7385
--- /dev/null
+++ b/site/src/pages/ManagementSettingsPage/IdpSyncPage/ExportPolicyButton.tsx
@@ -0,0 +1,57 @@
+import DownloadOutlined from "@mui/icons-material/DownloadOutlined";
+import Button from "@mui/material/Button";
+import type {
+ GroupSyncSettings,
+ Organization,
+ RoleSyncSettings,
+} from "api/typesGenerated";
+import { displayError } from "components/GlobalSnackbar/utils";
+import { saveAs } from "file-saver";
+import { type FC, useMemo, useState } from "react";
+
+interface DownloadPolicyButtonProps {
+ syncSettings: RoleSyncSettings | GroupSyncSettings | undefined;
+ type: "groups" | "roles";
+ organization: Organization;
+ download?: (file: Blob, filename: string) => void;
+}
+
+export const ExportPolicyButton: FC = ({
+ 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={!policyJSON || isDownloading}
+ onClick={async () => {
+ if (policyJSON) {
+ try {
+ setIsDownloading(true);
+ const file = new Blob([policyJSON], {
+ type: "application/json",
+ });
+ download(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/IdpPillList.tsx b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpPillList.tsx
new file mode 100644
index 0000000000000..4f489747f0bba
--- /dev/null
+++ b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpPillList.tsx
@@ -0,0 +1,106 @@
+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[];
+}
+
+// used to check if the role is a UUID
+const UUID =
+ /^[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 (
+
+ {roles.length > 0 ? (
+
+ {roles[0]}
+
+ ) : (
+ None
+ )}
+
+ {roles.length > 1 && }
+
+ );
+};
+
+interface 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",
+ }),
+ 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/IdpSyncPage.tsx b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPage.tsx
index 93b4e59455409..42a700f926633 100644
--- a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPage.tsx
+++ b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPage.tsx
@@ -1,63 +1,73 @@
-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,
+ 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";
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 { useQueries } from "react-query";
+import { 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";
-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 = () => {
- // 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 permissions = permissionsQuery.data;
+ const { organization: organizationName } = useParams() as {
+ organization: string;
+ };
+ const { organizations } = useOrganizationSettings();
+ const organization = organizations?.find((o) => o.name === organizationName);
+
+ const [groupIdpSyncSettingsQuery, roleIdpSyncSettingsQuery, groupsQuery] =
+ useQueries({
+ queries: [
+ groupIdpSyncSettings(organizationName),
+ roleIdpSyncSettings(organizationName),
+ groupsByOrganization(organizationName),
+ ],
+ });
+
+ if (!organization) {
+ return ;
+ }
- // if (!permissions) {
- // return ;
- // }
+ if (
+ groupsQuery.isLoading ||
+ groupIdpSyncSettingsQuery.isLoading ||
+ roleIdpSyncSettingsQuery.isLoading
+ ) {
+ return ;
+ }
+
+ const error =
+ groupIdpSyncSettingsQuery.error ||
+ roleIdpSyncSettingsQuery.error ||
+ groupsQuery.error;
+ if (
+ error ||
+ !groupIdpSyncSettingsQuery.data ||
+ !roleIdpSyncSettingsQuery.data ||
+ !groupsQuery.data
+ ) {
+ return ;
+ }
+
+ const groupsMap = new Map();
+ if (groupsQuery.data) {
+ for (const group of groupsQuery.data) {
+ groupsMap.set(group.id, group.display_name || group.name);
+ }
+ }
return (
<>
@@ -72,7 +82,7 @@ export const IdpSyncPage: FC = () => {
>
}
badges={}
/>
@@ -85,13 +95,16 @@ export const IdpSyncPage: FC = () => {
>
Setup IdP Sync
- } to="export">
- Export Policy
-
-
+
>
);
};
diff --git a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.stories.tsx b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.stories.tsx
index 47952edc61c95..646e64d763b86 100644
--- a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.stories.tsx
+++ b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.stories.tsx
@@ -1,5 +1,11 @@
import type { Meta, StoryObj } from "@storybook/react";
-import { MockOIDCConfig } from "testHelpers/entities";
+import {
+ MockGroup,
+ MockGroup2,
+ MockGroupSyncSettings,
+ MockGroupSyncSettings2,
+ MockRoleSyncSettings,
+} from "testHelpers/entities";
import { IdpSyncPageView } from "./IdpSyncPageView";
const meta: Meta = {
@@ -10,10 +16,32 @@ 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: { oidcConfig: undefined },
+ args: {
+ groupSyncSettings: undefined,
+ roleSyncSettings: undefined,
+ groupsMap: undefined,
+ },
};
export const Default: Story = {
- args: { oidcConfig: MockOIDCConfig },
+ args: {
+ groupSyncSettings: MockGroupSyncSettings,
+ roleSyncSettings: MockRoleSyncSettings,
+ groupsMap,
+ },
+};
+
+export const MissingGroups: Story = {
+ args: {
+ groupSyncSettings: MockGroupSyncSettings2,
+ roleSyncSettings: MockRoleSyncSettings,
+ groupsMap,
+ },
};
diff --git a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.tsx b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.tsx
index 08c165d6c7e91..f73395e254e8b 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";
@@ -9,7 +8,12 @@ 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 {
+ Group,
+ GroupSyncSettings,
+ Organization,
+ RoleSyncSettings,
+} from "api/typesGenerated";
import { ChooseOne, Cond } from "components/Conditionals/ChooseOne";
import { EmptyState } from "components/EmptyState/EmptyState";
import { Paywall } from "components/Paywall/Paywall";
@@ -19,22 +23,43 @@ 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 { ExportPolicyButton } from "./ExportPolicyButton";
+import { IdpPillList } from "./IdpPillList";
-export type IdpSyncPageViewProps = {
- oidcConfig: OIDCConfig | undefined;
-};
+interface IdpSyncPageViewProps {
+ groupSyncSettings: GroupSyncSettings | undefined;
+ roleSyncSettings: RoleSyncSettings | undefined;
+ groups: Group[] | undefined;
+ groupsMap: Map;
+ organization: Organization;
+}
+
+export const IdpSyncPageView: FC = ({
+ groupSyncSettings,
+ roleSyncSettings,
+ groupsMap,
+ organization,
+}) => {
+ 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;
+ const roleMappingCount = roleSyncSettings?.mapping
+ ? Object.entries(roleSyncSettings.mapping).length
+ : 0;
-export const IdpSyncPageView: FC = ({ oidcConfig }) => {
- const theme = useTheme();
- const {
- groups_field,
- user_role_field,
- group_regex_filter,
- group_auto_create,
- } = oidcConfig || {};
return (
<>
@@ -46,75 +71,112 @@ export const IdpSyncPageView: FC = ({ oidcConfig }) => {
/>
-
- {/* Semantically fieldset is used for forms. In the future this screen will allow
- updates to these fields in a form */}
-
-
-
-
-
- {oidcConfig?.user_role_mapping &&
- Object.entries(oidcConfig.user_role_mapping)
- .sort()
- .map(([idpRole, roles]) => (
-
+
+
+
+ Group Sync Settings
+
+
+ Role Sync Settings
+
+
+
+ {tab === "groups" ? (
+ <>
+
+
+
+
- ))}
-
-
- {oidcConfig?.user_role_mapping &&
- Object.entries(oidcConfig.group_mapping)
- .sort()
- .map(([idpGroup, group]) => (
-
- ))}
-
+
+
+
+
+
+
+
+
+ {groupSyncSettings?.mapping &&
+ Object.entries(groupSyncSettings.mapping)
+ .sort()
+ .map(([idpGroup, groups]) => (
+
+ ))}
+
+
+ >
+ ) : (
+ <>
+
+
+
+
+
+
+
+
+ {roleSyncSettings?.mapping &&
+ Object.entries(roleSyncSettings.mapping)
+ .sort()
+ .map(([idpRole, roles]) => (
+
+ ))}
+
+ >
+ )}
@@ -125,37 +187,66 @@ export const IdpSyncPageView: FC = ({ oidcConfig }) => {
interface IdpFieldProps {
name: string;
fieldText: string | undefined;
- showStatusIndicator?: boolean;
+ showDisabled?: boolean;
}
const IdpField: FC = ({
name,
fieldText,
- showStatusIndicator = false,
+ showDisabled = false,
}) => {
return (
-
- {name}
-
- {fieldText ||
- (showStatusIndicator && (
-
- ))}
-
+
+ {name}
+ {fieldText ? (
+ {fieldText}
+ ) : (
+ showDisabled && (
+
+ )
+ )}
);
};
+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;
@@ -194,7 +285,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
@@ -215,28 +308,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
+
+
+
);
};
@@ -260,22 +357,20 @@ const TableLoader = () => {
};
const styles = {
- field: (theme) => ({
- color: theme.palette.text.secondary,
+ fieldText: (theme) => ({
fontFamily: MONOSPACE_FONT_FAMILY,
+ whiteSpace: "nowrap",
+ paddingBottom: ".02rem",
}),
- fields: () => ({
- marginBottom: "60px",
+ fieldLabel: (theme) => ({
+ color: theme.palette.text.secondary,
}),
- legend: () => ({
- padding: "0px 6px",
- fontWeight: 600,
+ fields: () => ({
+ marginLeft: 16,
+ fontSize: 14,
}),
- box: (theme) => ({
- border: "1px solid",
- borderColor: theme.palette.divider,
- padding: "0px 20px",
- borderRadius: 8,
+ tableInfo: () => ({
+ marginBottom: 16,
}),
} satisfies Record>;
diff --git a/site/src/pages/ManagementSettingsPage/SidebarView.tsx b/site/src/pages/ManagementSettingsPage/SidebarView.tsx
index 01610596f7ffa..eeae70bec80dd 100644
--- a/site/src/pages/ManagementSettingsPage/SidebarView.tsx
+++ b/site/src/pages/ManagementSettingsPage/SidebarView.tsx
@@ -297,7 +297,7 @@ const OrganizationSettingsNavigation: FC<
Provisioners
)}
- {organization.permissions.editMembers && (
+ {organization.permissions.viewIdpSyncSettings && (
diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts
index 9d9f3192fd9c6..c3395fa042bda 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,40 @@ 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 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: {
+ "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 +2648,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.