Skip to content

feat: create idp sync page skeleton #14543

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 14 commits into from
Sep 6, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
feat: add mock data and update pageview for mock data
  • Loading branch information
jaaydenh committed Sep 5, 2024
commit a75ad856e5eaf97d5d541da297c40826e97b08d4
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,38 @@ 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-.*$",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know this is a mock value, but could I get more context on where this regex is used? Is it strictly server-controlled?

Wondering because the .*$ at the end literally does nothing in this case except make the regex run slower

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All the data is readonly and for now I just took the value from Steven. Im still learning myself how this feature works.

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 = () => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just calling this out because I don't see anything about this in the commented-out code: do we want to do a redirect if the user navigates to this page if organization.permissions.editMembers is false?

I know we're using that property to define whether we show the page link in the side navbar, but is there anything stopping someone from navigating to the page directly?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Parkreiner I haven't addressed permissions yet because a permission hasn't been defined for this page. Btw, how do other pages handle this case when the user doesn't have the permissions?

const queryClient = useQueryClient();
// const { custom_roles: isCustomRolesEnabled } = useFeatureVisibility();
Expand Down Expand Up @@ -77,7 +109,7 @@ export const IdpSyncPage: FC = () => {
</Stack>
</Stack>

<IdpSyncPageView roles={[]} />
<IdpSyncPageView oidcConfig={mockOIDCConfig} />
</>
);
};
Expand Down
122 changes: 89 additions & 33 deletions site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ 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 { Role } from "api/typesGenerated";
import type { OIDCConfig } from "api/typesGenerated";
import { ChooseOne, Cond } from "components/Conditionals/ChooseOne";
import { EmptyState } from "components/EmptyState/EmptyState";
import { Paywall } from "components/Paywall/Paywall";
Expand All @@ -21,10 +21,10 @@ import type { FC } from "react";
import { docs } from "utils/docs";

export type IdpSyncPageViewProps = {
roles: Role[] | undefined;
oidcConfig: OIDCConfig | undefined;
};

export const IdpSyncPageView: FC<IdpSyncPageViewProps> = ({ roles }) => {
export const IdpSyncPageView: FC<IdpSyncPageViewProps> = ({ oidcConfig }) => {
return (
<>
<ChooseOne>
Expand All @@ -43,45 +43,93 @@ export const IdpSyncPageView: FC<IdpSyncPageViewProps> = ({ roles }) => {
<legend css={styles.legend}>Groups</legend>
<Stack direction={"row"} alignItems={"center"} spacing={3}>
<h4>Sync Field</h4>
<p css={styles.secondary}>groups</p>
<p css={styles.secondary}>{oidcConfig?.groups_field}</p>
<h4>Regex Filter</h4>
<p css={styles.secondary}>^Coder-.*$</p>
<p css={styles.secondary}>{oidcConfig?.group_regex_filter}</p>
<h4>Auto Create</h4>
<p css={styles.secondary}>false</p>
<p css={styles.secondary}>
{oidcConfig?.group_auto_create.toString()}
</p>
</Stack>
</fieldset>
<fieldset css={styles.box}>
<legend css={styles.legend}>Roles</legend>
<Stack direction={"row"} alignItems={"center"} spacing={3}>
<h4>Sync Field</h4>
<p css={styles.secondary}>roles</p>
<p css={styles.secondary}>{oidcConfig?.user_role_field}</p>
</Stack>
</fieldset>
</Stack>
<Stack spacing={4}>
<RoleTable roles={roles} />
<RoleTable roles={roles} />
<Stack spacing={6}>
<IdpMappingTable
type="Role"
isEmpty={Boolean(
(oidcConfig?.user_role_mapping &&
Object.entries(oidcConfig?.user_role_mapping).length === 0) ||
false,
)}
>
<>
{oidcConfig?.user_role_mapping &&
Object.entries(oidcConfig.user_role_mapping).map(
([idpRole, roles]) => (
<RoleRow
key={idpRole}
idpRole={idpRole}
coderRoles={roles}
/>
),
)}
</>
</IdpMappingTable>
<IdpMappingTable
type="Group"
isEmpty={Boolean(
(oidcConfig?.user_role_mapping &&
Object.entries(oidcConfig?.group_mapping).length === 0) ||
false,
)}
>
<>
{oidcConfig?.user_role_mapping &&
Object.entries(oidcConfig.group_mapping).map(
([idpGroup, group]) => (
<GroupRow
key={idpGroup}
idpGroup={idpGroup}
coderGroup={group}
/>
),
)}
</>
</IdpMappingTable>
</Stack>
</Cond>
</ChooseOne>
</>
);
};

interface RoleTableProps {
roles: Role[] | undefined;
interface IdpMappingTableProps {
type: "Role" | "Group";
isEmpty: boolean;
children: React.ReactNode;
}

const RoleTable: FC<RoleTableProps> = ({ roles }) => {
const IdpMappingTable: FC<IdpMappingTableProps> = ({
type,
isEmpty,
children,
}) => {
const isLoading = false;
const isEmpty = Boolean(roles && roles.length === 0);

return (
<TableContainer>
<Table>
<TableHead>
<TableRow>
<TableCell width="45%">Idp Role</TableCell>
<TableCell width="55%">Coder Role</TableCell>
<TableCell width="45%">Idp {type}</TableCell>
<TableCell width="55%">Coder {type}</TableCell>
</TableRow>
</TableHead>
<TableBody>
Expand All @@ -94,10 +142,7 @@ const RoleTable: FC<RoleTableProps> = ({ roles }) => {
<TableRow>
<TableCell colSpan={999}>
<EmptyState
message="No Role Mappings"
description={
"Configure role sync mappings to manage permissions outside of Coder."
}
message={`No ${type} Mappings`}
isCompact
cta={
<Button
Expand All @@ -106,35 +151,46 @@ const RoleTable: FC<RoleTableProps> = ({ roles }) => {
href={docs("/admin/auth#group-sync-enterprise")}
target="_blank"
>
How to setup IdP role sync
How to setup IdP {type} sync
</Button>
}
/>
</TableCell>
</TableRow>
</Cond>

<Cond>
{roles?.map((role) => (
<RoleRow key={role.name} role={role} />
))}
</Cond>
<Cond>{children}</Cond>
</ChooseOne>
</TableBody>
</Table>
</TableContainer>
);
};

interface GroupRowProps {
idpGroup: string;
coderGroup: string;
}

const GroupRow: FC<GroupRowProps> = ({ idpGroup, coderGroup }) => {
return (
<TableRow data-testid={`group-${idpGroup}`}>
<TableCell>{idpGroup}</TableCell>
<TableCell css={styles.secondary}>{coderGroup}</TableCell>
</TableRow>
);
};

interface RoleRowProps {
role: Role;
idpRole: string;
coderRoles: ReadonlyArray<string>;
}

const RoleRow: FC<RoleRowProps> = ({ role }) => {
const RoleRow: FC<RoleRowProps> = ({ idpRole, coderRoles }) => {
return (
<TableRow data-testid={`role-${role.name}`}>
<TableCell>{role.display_name || role.name}</TableCell>
<TableCell css={styles.secondary}>test</TableCell>
<TableRow data-testid={`role-${idpRole}`}>
<TableCell>{idpRole}</TableCell>
<TableCell css={styles.secondary}>coderRoles Placeholder</TableCell>
</TableRow>
);
};
Expand All @@ -161,10 +217,10 @@ const styles = {
secondary: (theme) => ({
color: theme.palette.text.secondary,
}),
fields: (theme) => ({
fields: () => ({
marginBottom: "60px",
}),
legend: (theme) => ({
legend: () => ({
padding: "0px 6px",
fontWeight: 600,
}),
Expand Down