mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
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:
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user