fix: show paywall and correctly display auto create groups for IDP sync (#14800)

* fix: show paywall and correctly display auto create groups

* fix: update stories

* fix: format

* chore: cleanup

* fix: update stories
This commit is contained in:
Jaayden Halko
2024-09-26 12:12:04 -04:00
committed by GitHub
parent 4be5b2ff98
commit 9ea2f6f267
3 changed files with 184 additions and 156 deletions
@@ -5,11 +5,12 @@ import {
groupIdpSyncSettings,
roleIdpSyncSettings,
} from "api/queries/organizations";
import { ErrorAlert } from "components/Alert/ErrorAlert";
import { ChooseOne, Cond } from "components/Conditionals/ChooseOne";
import { EmptyState } from "components/EmptyState/EmptyState";
import { Loader } from "components/Loader/Loader";
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 type { FC } from "react";
import { Helmet } from "react-helmet-async";
import { useQueries } from "react-query";
@@ -24,6 +25,8 @@ export const IdpSyncPage: FC = () => {
const { organization: organizationName } = useParams() as {
organization: string;
};
// IdP sync does not have its own entitlement and is based on templace_rbac
const { template_rbac: isIdpSyncEnabled } = useFeatureVisibility();
const { organizations } = useOrganizationSettings();
const organization = organizations?.find((o) => o.name === organizationName);
@@ -40,26 +43,10 @@ export const IdpSyncPage: FC = () => {
return <EmptyState message="Organization not found" />;
}
if (
groupsQuery.isLoading ||
groupIdpSyncSettingsQuery.isLoading ||
roleIdpSyncSettingsQuery.isLoading
) {
return <Loader />;
}
const error =
groupIdpSyncSettingsQuery.error ||
roleIdpSyncSettingsQuery.error ||
groupsQuery.error;
if (
error ||
!groupIdpSyncSettingsQuery.data ||
!roleIdpSyncSettingsQuery.data ||
!groupsQuery.data
) {
return <ErrorAlert error={error} />;
}
const groupsMap = new Map<string, string>();
if (groupsQuery.data) {
@@ -84,25 +71,34 @@ export const IdpSyncPage: FC = () => {
description="Group and role sync mappings (configured using Coder CLI)."
tooltip={<IdpSyncHelpTooltip />}
/>
<Stack direction="row" spacing={2}>
<Button
startIcon={<LaunchOutlined />}
component="a"
href={docs("/admin/auth#group-sync-enterprise")}
target="_blank"
>
Setup IdP Sync
</Button>
</Stack>
<Button
startIcon={<LaunchOutlined />}
component="a"
href={docs("/admin/auth#group-sync-enterprise")}
target="_blank"
>
Setup IdP Sync
</Button>
</Stack>
<IdpSyncPageView
groupSyncSettings={groupIdpSyncSettingsQuery.data}
roleSyncSettings={roleIdpSyncSettingsQuery.data}
groups={groupsQuery.data}
groupsMap={groupsMap}
organization={organization}
/>
<ChooseOne>
<Cond condition={!isIdpSyncEnabled}>
<Paywall
message="IdP Sync"
description="Configure group and role mappings to manage permissions outside of Coder. You need an Premium license to use this feature."
documentationLink={docs("/admin/groups")}
/>
</Cond>
<Cond>
<IdpSyncPageView
groupSyncSettings={groupIdpSyncSettingsQuery.data}
roleSyncSettings={roleIdpSyncSettingsQuery.data}
groups={groupsQuery.data}
groupsMap={groupsMap}
organization={organization}
error={error}
/>
</Cond>
</ChooseOne>
</>
);
};
@@ -4,6 +4,7 @@ import {
MockGroup2,
MockGroupSyncSettings,
MockGroupSyncSettings2,
MockOrganization,
MockRoleSyncSettings,
} from "testHelpers/entities";
import { IdpSyncPageView } from "./IdpSyncPageView";
@@ -24,9 +25,31 @@ for (const group of [MockGroup, MockGroup2]) {
export const Empty: Story = {
args: {
groupSyncSettings: undefined,
roleSyncSettings: undefined,
groupSyncSettings: {
field: "",
mapping: {},
regex_filter: "",
auto_create_missing_groups: false,
},
roleSyncSettings: {
field: "",
mapping: {},
},
groups: [],
groupsMap: undefined,
organization: MockOrganization,
error: undefined,
},
};
export const HasError: Story = {
args: {
groupSyncSettings: MockGroupSyncSettings,
roleSyncSettings: MockRoleSyncSettings,
groups: [MockGroup, MockGroup2],
groupsMap,
organization: MockOrganization,
error: "This is a test error",
},
};
@@ -34,7 +57,10 @@ export const Default: Story = {
args: {
groupSyncSettings: MockGroupSyncSettings,
roleSyncSettings: MockRoleSyncSettings,
groups: [MockGroup, MockGroup2],
groupsMap,
organization: MockOrganization,
error: undefined,
},
};
@@ -42,6 +68,9 @@ export const MissingGroups: Story = {
args: {
groupSyncSettings: MockGroupSyncSettings2,
roleSyncSettings: MockRoleSyncSettings,
groups: [MockGroup, MockGroup2],
groupsMap,
organization: MockOrganization,
error: undefined,
},
};
@@ -14,9 +14,10 @@ import type {
Organization,
RoleSyncSettings,
} from "api/typesGenerated";
import { ErrorAlert } from "components/Alert/ErrorAlert";
import { ChooseOne, Cond } from "components/Conditionals/ChooseOne";
import { EmptyState } from "components/EmptyState/EmptyState";
import { Paywall } from "components/Paywall/Paywall";
import { Loader } from "components/Loader/Loader";
import { Stack } from "components/Stack/Stack";
import { StatusIndicator } from "components/StatusIndicator/StatusIndicator";
import {
@@ -37,13 +38,16 @@ interface IdpSyncPageViewProps {
groups: Group[] | undefined;
groupsMap: Map<string, string>;
organization: Organization;
error?: unknown;
}
export const IdpSyncPageView: FC<IdpSyncPageViewProps> = ({
groupSyncSettings,
roleSyncSettings,
groups,
groupsMap,
organization,
error,
}) => {
const [searchParams] = useSearchParams();
@@ -60,126 +64,125 @@ export const IdpSyncPageView: FC<IdpSyncPageViewProps> = ({
? Object.entries(roleSyncSettings.mapping).length
: 0;
if (error) {
return <ErrorAlert error={error} />;
}
if (!groupSyncSettings || !roleSyncSettings || !groups) {
return <Loader />;
}
return (
<>
<ChooseOne>
<Cond condition={false}>
<Paywall
message="IdP Sync"
description="Configure group and role mappings to manage permissions outside of Coder. You need an Premium license to use this feature."
documentationLink={docs("/admin/groups")}
/>
</Cond>
<Cond>
<Stack spacing={2}>
<Tabs active={tab}>
<TabsList>
<TabLink to="?tab=groups" value="groups">
Group Sync Settings
</TabLink>
<TabLink to="?tab=roles" value="roles">
Role Sync Settings
</TabLink>
</TabsList>
</Tabs>
{tab === "groups" ? (
<>
<div css={styles.fields}>
<Stack direction={"row"} alignItems={"center"} spacing={6}>
<IdpField
name={"Sync Field"}
fieldText={groupSyncSettings?.field}
showDisabled
<Stack spacing={2}>
<Tabs active={tab}>
<TabsList>
<TabLink to="?tab=groups" value="groups">
Group Sync Settings
</TabLink>
<TabLink to="?tab=roles" value="roles">
Role Sync Settings
</TabLink>
</TabsList>
</Tabs>
{tab === "groups" ? (
<>
<div css={styles.fields}>
<Stack direction={"row"} alignItems={"center"} spacing={6}>
<IdpField
name={"Sync Field"}
fieldText={groupSyncSettings?.field}
showDisabled
/>
<IdpField
name={"Regex Filter"}
fieldText={
typeof groupSyncSettings?.regex_filter === "string"
? groupSyncSettings.regex_filter
: "none"
}
/>
<IdpField
name={"Auto Create"}
fieldText={
groupSyncSettings?.field
? String(groupSyncSettings?.auto_create_missing_groups)
: "n/a"
}
/>
</Stack>
</div>
<Stack
direction="row"
alignItems="baseline"
justifyContent="space-between"
css={styles.tableInfo}
>
<TableRowCount count={groupMappingCount} type="groups" />
<ExportPolicyButton
syncSettings={groupSyncSettings}
organization={organization}
type="groups"
/>
</Stack>
<Stack spacing={6}>
<IdpMappingTable
type="Group"
isEmpty={Boolean(groupMappingCount === 0)}
>
{groupSyncSettings?.mapping &&
Object.entries(groupSyncSettings.mapping)
.sort()
.map(([idpGroup, groups]) => (
<GroupRow
key={idpGroup}
idpGroup={idpGroup}
coderGroup={getGroupNames(groups)}
/>
))}
</IdpMappingTable>
</Stack>
</>
) : (
<>
<div css={styles.fields}>
<IdpField
name={"Sync Field"}
fieldText={roleSyncSettings?.field}
showDisabled
/>
</div>
<Stack
direction="row"
alignItems="baseline"
justifyContent="space-between"
css={styles.tableInfo}
>
<TableRowCount count={roleMappingCount} type="roles" />
<ExportPolicyButton
syncSettings={roleSyncSettings}
organization={organization}
type="roles"
/>
</Stack>
<IdpMappingTable
type="Role"
isEmpty={Boolean(roleMappingCount === 0)}
>
{roleSyncSettings?.mapping &&
Object.entries(roleSyncSettings.mapping)
.sort()
.map(([idpRole, roles]) => (
<RoleRow
key={idpRole}
idpRole={idpRole}
coderRoles={roles}
/>
<IdpField
name={"Regex Filter"}
fieldText={
typeof groupSyncSettings?.regex_filter === "string"
? groupSyncSettings.regex_filter
: "none"
}
/>
<IdpField
name={"Auto Create"}
fieldText={String(
groupSyncSettings?.auto_create_missing_groups || "n/a",
)}
/>
</Stack>
</div>
<Stack
direction="row"
alignItems="baseline"
justifyContent="space-between"
css={styles.tableInfo}
>
<TableRowCount count={groupMappingCount} type="groups" />
<ExportPolicyButton
syncSettings={groupSyncSettings}
organization={organization}
type="groups"
/>
</Stack>
<Stack spacing={6}>
<IdpMappingTable
type="Group"
isEmpty={Boolean(groupMappingCount === 0)}
>
{groupSyncSettings?.mapping &&
Object.entries(groupSyncSettings.mapping)
.sort()
.map(([idpGroup, groups]) => (
<GroupRow
key={idpGroup}
idpGroup={idpGroup}
coderGroup={getGroupNames(groups)}
/>
))}
</IdpMappingTable>
</Stack>
</>
) : (
<>
<div css={styles.fields}>
<IdpField
name={"Sync Field"}
fieldText={roleSyncSettings?.field}
showDisabled
/>
</div>
<Stack
direction="row"
alignItems="baseline"
justifyContent="space-between"
css={styles.tableInfo}
>
<TableRowCount count={roleMappingCount} type="roles" />
<ExportPolicyButton
syncSettings={roleSyncSettings}
organization={organization}
type="roles"
/>
</Stack>
<IdpMappingTable
type="Role"
isEmpty={Boolean(roleMappingCount === 0)}
>
{roleSyncSettings?.mapping &&
Object.entries(roleSyncSettings.mapping)
.sort()
.map(([idpRole, roles]) => (
<RoleRow
key={idpRole}
idpRole={idpRole}
coderRoles={roles}
/>
))}
</IdpMappingTable>
</>
)}
</Stack>
</Cond>
</ChooseOne>
))}
</IdpMappingTable>
</>
)}
</Stack>
</>
);
};