8000 feat: integrate backend with idp sync page by jaaydenh · Pull Request #14755 · coder/coder · GitHub
[go: up one dir, main page]

Skip to content

feat: integrate backend with idp sync page #14755

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 13 commits into from
Sep 24, 2024
Prev Previous commit
Next Next commit
fix: hookup backend data for groups and roles
  • Loading branch information
jaaydenh committed Sep 20, 2024
commit 7ff9a07c17edd0dfd8b41a0ac9479444b7cbf5bf
58 changes: 16 additions & 42 deletions site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPage.tsx
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -64,16 +34,20 @@ 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),
);

// const permissions = permissionsQuery.data;

if (!organization) {
Expand Down Expand Up @@ -121,9 +95,9 @@ export const IdpSyncPage: FC = () => {
</Stack>

<IdpSyncPageView
oidcConfig={mockOIDCConfig}
groupSyncSettings= 8000 {groupIdpSyncSettingsQuery.data}
roleSyncSettings={roleIdpSyncSettingsQuery.data}
groups={groupsQuery.data}
/>
</>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -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<typeof IdpSyncPageView> = {
Expand All @@ -11,9 +16,17 @@ export default meta;
type Story = StoryObj<typeof IdpSyncPageView>;

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],
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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<IdpSyncPageViewProps> = ({
Copy link
Member

Choose a reason for hiding this comment

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

Have to put the comment here because the code was already in place previously, and it's not selectable for review:

I think we need to add some CSS optical adjustments to the key-value pairs for the IdpField component. Since we're using two different typefaces, their baselines are slightly different. And when you place them right next to each other on the same line, there's enough variation that the text looks sloppy

You might have to open this in a new tab, but the baselines are ever so slightly off, and the cap-heights are, too:
Screenshot 2024-09-23 at 12 21 28 PM

Copy link
Member

Choose a reason for hiding this comment

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

Though a slightly easier and probably much more manageable alternative would be to set all the text in monospace

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The monospace font was supposed to be a temporary measure until this page becomes a form and these would become fields. I'll see if I can improve it for now.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

as a temporary fix, I can add some padding to make the baselines match. I really wanted to use the monospace font on only the field value to signify that it is the value of the configuration.

oidcConfig,
groupSyncSettings,
roleSyncSettings,
groups,
}) => {
const theme = useTheme();
const { user_role_field } = oidcConfig || {};
// const theme = useTheme();

const groupsMap = new Map<string, string>();
if (groups) {
for (const group of groups) {
groupsMap.set(group.id, group.display_name || group.name);
}
}
Copy link
Member

Choose a reason for hiding this comment

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

Would it make sense to lift this up to the page component? I don't expect this map to change all that often, and putting it there should limit re-renders more without having to resort to useMemo


const getGroupNames = (groupIds: readonly string[]) => {
return groupIds.map((groupId) => groupsMap.get(groupId) || groupId);
};

return (
<>
<ChooseOne>
Expand Down Expand Up @@ -67,13 +79,13 @@ export const IdpSyncPageView: FC<IdpSyncPageViewProps> = ({
fieldText={
typeof groupSyncSettings?.regex_filter === "string"
? groupSyncSettings?.regex_filter
: ""
: "none"
}
/>
<IdpField
name={"Auto Create"}
fieldText={String(
groupSyncSettings?.auto_create_missing_groups,
groupSyncSettings?.auto_create_missing_groups || "n/a",
)}
/>
</Stack>
Expand All @@ -83,46 +95,46 @@ export const IdpSyncPageView: FC<IdpSyncPageViewProps> = ({
<Stack direction={"row"} alignItems={"center"} spacing={3}>
<IdpField
name={"Sync Field"}
fieldText={user_role_field}
fieldText={roleSyncSettings?.field}
showStatusIndicator
/>
</Stack>
</fieldset>
</Stack>
<Stack spacing={6}>
<IdpMappingTable
type="Role"
type="Group"
isEmpty={Boolean(
!oidcConfig?.user_role_mapping ||
Object.entries(oidcConfig?.user_role_mapping).length === 0,
!groupSyncSettings?.mapping ||
Object.entries(groupSyncSettings?.mapping).length === 0,
)}
>
{oidcConfig?.user_role_mapping &&
Object.entries(oidcConfig.user_role_mapping)
{groupSyncSettings?.mapping &&
Object.entries(groupSyncSettings.mapping)
.sort()
.map(([idpRole, roles]) => (
<RoleRow
key={idpRole}
idpRole={idpRole}
coderRoles={roles}
.map(([idpGroup, groups]) => (
<GroupRow
key={idpGroup}
idpGroup={idpGroup}
coderGroup={getGroupNames(groups)}
/>
))}
</IdpMappingTable>
<IdpMappingTable
type="Group"
type="Role"
isEmpty={Boolean(
!oidcConfig?.group_mapping ||
Object.entries(oidcConfig?.group_mapping).length === 0,
!roleSyncSettings?.mapping ||
Object.entries(roleSyncSettings?.mapping).length === 0,
)}
>
{oidcConfig?.user_role_mapping &&
Object.entries(oidcConfig.group_mapping)
{roleSyncSettings?.mapping &&
Object.entries(roleSyncSettings.mapping)
.sort()
.map(([idpGroup, group]) => (
<GroupRow
key={idpGroup}
idpGroup={idpGroup}
coderGroup={group}
.map(([idpRole, roles]) => (
<RoleRow
key={idpRole}
idpRole={idpRole}
coderRoles={roles}
/>
))}
</IdpMappingTable>
Expand Down Expand Up @@ -226,28 +238,32 @@ const IdpMappingTable: FC<IdpMappingTableProps> = ({

interface GroupRowProps {
idpGroup: string;
coderGroup: string;
coderGroup: readonly string[];
}

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

interface RoleRowProps {
idpRole: string;
coderRoles: ReadonlyArray<string>;
coderRoles: readonly string[];
}

const RoleRow: FC<RoleRowProps> = ({ idpRole, coderRoles }) => {
return (
<TableRow data-testid={`role-${idpRole}`}>
<TableCell>{idpRole}</TableCell>
<TableCell>coderRoles Placeholder</TableCell>
<TableCell>
<PillList roles={coderRoles} />
</TableCell>
</TableRow>
);
};
Expand Down
91 changes: 91 addit 10000 ions & 0 deletions site/src/pages/ManagementSettingsPage/IdpSyncPage/PillList.tsx
Original file line number Diff line number Diff line change
@@ -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<PillListProps> = ({ roles }) => {
return (
<Stack direction="row" spacing={1}>
{roles.length > 0 ? (
<Pill css={styles.pill}>{roles[0]}</Pill>
) : (
<p>None</p>
)}

{roles.length > 1 && <OverflowPill roles={roles.slice(1)} />}
</Stack>
);
};

type OverflowPillProps = {
roles: string[];
};

const OverflowPill: FC<OverflowPillProps> = ({ roles }) => {
const theme = useTheme();

return (
<Popover mode="hover">
<PopoverTrigger>
<Pill
css={{
backgroundColor: theme.palette.background.paper,
borderColor: theme.palette.divider,
}}
data-testid="overflow-pill"
>
+{roles.length} more
</Pill>
</PopoverTrigger>

<PopoverContent
disableRestoreFocus
disableScrollLock
css={{
".MuiPaper-root": {
display: "flex",
flexFlow: "column wrap",
columnGap: 8,
rowGap: 12,
padding: "12px 16px",
alignContent: "space-around",
minWidth: "auto",
backgroundColor: theme.palette.background.default,
},
}}
anchorOrigin={{
vertical: -4,
horizontal: "center",
}}
transformOrigin={{
vertical: "bottom",
horizontal: "center",
}}
>
{roles.map((role) => (
<Pill key={role} css={styles.pill}>
{role}
</Pill>
))}
</PopoverContent>
</Popover>
);
};

const styles = {
pill: (theme) => ({
backgroundColor: theme.experimental.pillDefault.background,
borderColor: theme.experimental.pillDefault.outline,
color: theme.experimental.pillDefault.text,
width: "fit-content",
}),
} satisfies Record<string, Interpolation<Theme>>;
Loading
0