Skip to content

Commit 0aa247f

Browse files
committed
feat: initial commit for idp skeleton page
1 parent 0f8251b commit 0aa247f

File tree

5 files changed

+281
-2
lines changed

5 files changed

+281
-2
lines changed

site/src/components/EmptyState/EmptyState.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export interface EmptyStateProps extends HTMLAttributes<HTMLDivElement> {
77
description?: string | ReactNode;
88
cta?: ReactNode;
99
image?: ReactNode;
10+
isCompact?: boolean;
1011
}
1112

1213
/**
@@ -19,6 +20,7 @@ export const EmptyState: FC<EmptyStateProps> = ({
1920
description,
2021
cta,
2122
image,
23+
isCompact,
2224
...attrs
2325
}) => {
2426
return (
@@ -30,8 +32,8 @@ export const EmptyState: FC<EmptyStateProps> = ({
3032
justifyContent: "center",
3133
alignItems: "center",
3234
textAlign: "center",
33-
minHeight: 360,
34-
padding: "80px 40px",
35+
minHeight: isCompact ? 180 : 360,
36+
padding: isCompact ? "10px 40px" : "80px 40px",
3537
position: "relative",
3638
}}
3739
{...attrs}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import AddIcon from "@mui/icons-material/AddOutlined";
2+
import LaunchOutlined from "@mui/icons-material/LaunchOutlined";
3+
import Button from "@mui/material/Button";
4+
import { getErrorMessage } from "api/errors";
5+
import { organizationPermissions } from "api/queries/organizations";
6+
import { organizationRoles } from "api/queries/roles";
7+
import { displayError, displaySuccess } from "components/GlobalSnackbar/utils";
8+
import { Loader } from "components/Loader/Loader";
9+
import { SettingsHeader } from "components/SettingsHeader/SettingsHeader";
10+
import { Stack } from "components/Stack/Stack";
11+
import { useFeatureVisibility } from "modules/dashboard/useFeatureVisibility";
12+
import { type FC, useEffect, useState } from "react";
13+
import { Helmet } from "react-helmet-async";
14+
import { useQuery, useQueryClient } from "react-query";
15+
import { Link as RouterLink, useParams } from "react-router-dom";
16+
import { pageTitle } from "utils/page";
17+
import { useOrganizationSettings } from "../ManagementSettingsLayout";
18+
import { docs } from "utils/docs";
19+
import IdpSyncPageView from "./IdpSyncPageView";
20+
21+
export const IdpSyncPage: FC = () => {
22+
const queryClient = useQueryClient();
23+
// const { custom_roles: isCustomRolesEnabled } = useFeatureVisibility();
24+
const { organization: organizationName } = useParams() as {
25+
organization: string;
26+
};
27+
const { organizations } = useOrganizationSettings();
28+
const organization = organizations?.find((o) => o.name === organizationName);
29+
const permissionsQuery = useQuery(organizationPermissions(organization?.id));
30+
const organizationRolesQuery = useQuery(organizationRoles(organizationName));
31+
const permissions = permissionsQuery.data;
32+
33+
useEffect(() => {
34+
if (organizationRolesQuery.error) {
35+
displayError(
36+
getErrorMessage(
37+
organizationRolesQuery.error,
38+
"Error loading custom roles.",
39+
),
40+
);
41+
}
42+
}, [organizationRolesQuery.error]);
43+
44+
if (!permissions) {
45+
return <Loader />;
46+
}
47+
48+
return (
49+
<>
50+
<Helmet>
51+
<title>{pageTitle("IdP Sync")}</title>
52+
</Helmet>
53+
54+
<Stack
55+
alignItems="baseline"
56+
direction="row"
57+
justifyContent="space-between"
58+
>
59+
<SettingsHeader
60+
title="IdP Sync"
61+
description="Group and role sync mappings (configured outside Coder)."
62+
/>
63+
<Stack direction="row" spacing={2}>
64+
<Button
65+
startIcon={<LaunchOutlined />}
66+
component="a"
67+
href={docs("/cli/server#--notifications-webhook-endpoint")}
68+
target="_blank"
69+
>
70+
Setup IdP Sync
71+
</Button>
72+
<Button component={RouterLink} startIcon={<AddIcon />} to="export">
73+
Export Policy
74+
</Button>
75+
</Stack>
76+
</Stack>
77+
78+
<IdpSyncPageView roles={[]} />
79+
</>
80+
);
81+
};
82+
83+
export default IdpSyncPage;
Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
import type { Interpolation, Theme } from "@emotion/react";
2+
import LaunchOutlined from "@mui/icons-material/LaunchOutlined";
3+
import Button from "@mui/material/Button";
4+
import Skeleton from "@mui/material/Skeleton";
5+
import Table from "@mui/material/Table";
6+
import TableBody from "@mui/material/TableBody";
7+
import TableCell from "@mui/material/TableCell";
8+
import TableContainer from "@mui/material/TableContainer";
9+
import TableHead from "@mui/material/TableHead";
10+
import TableRow from "@mui/material/TableRow";
11+
import type { Role } from "api/typesGenerated";
12+
import { ChooseOne, Cond } from "components/Conditionals/ChooseOne";
13+
import { EmptyState } from "components/EmptyState/EmptyState";
14+
import { Paywall } from "components/Paywall/Paywall";
15+
import { Stack } from "components/Stack/Stack";
16+
import {
17+
TableLoaderSkeleton,
18+
TableRowSkeleton,
19+
} from "components/TableLoader/TableLoader";
20+
import type { FC } from "react";
21+
import { docs } from "utils/docs";
22+
23+
export type IdpSyncPageViewProps = {
24+
roles: Role[] | undefined;
25+
};
26+
27+
export const IdpSyncPageView: FC<IdpSyncPageViewProps> = ({ roles }) => {
28+
return (
29+
<>
30+
<ChooseOne>
31+
<Cond condition={false}>
32+
<Paywall
33+
message="IdP Sync"
34+
description="Configure group and role mappings to manage permissions outside of Coder."
35+
documentationLink={docs("/admin/groups")}
36+
/>
37+
</Cond>
38+
<Cond>
39+
<Stack spacing={2} css={styles.fields}>
40+
{/* Semantically fieldset is used for forms. In the future this screen will allow
41+
updates to these fields in a form */}
42+
<fieldset css={styles.box}>
43+
<legend css={styles.legend}>Groups</legend>
44+
<Stack direction={"row"} alignItems={"center"} spacing={3}>
45+
<h4>Sync Field</h4>
46+
<p css={styles.secondary}>groups</p>
47+
<h4>Regex Filter</h4>
48+
<p css={styles.secondary}>^Coder-.*$</p>
49+
<h4>Auto Create</h4>
50+
<p css={styles.secondary}>false</p>
51+
</Stack>
52+
</fieldset>
53+
<fieldset css={styles.box}>
54+
<legend css={styles.legend}>Roles</legend>
55+
<Stack direction={"row"} alignItems={"center"} spacing={3}>
56+
<h4>Sync Field</h4>
57+
<p css={styles.secondary}>roles</p>
58+
</Stack>
59+
</fieldset>
60+
</Stack>
61+
<Stack spacing={4}>
62+
<RoleTable roles={roles} />
63+
<RoleTable roles={roles} />
64+
</Stack>
65+
</Cond>
66+
</ChooseOne>
67+
</>
68+
);
69+
};
70+
71+
interface RoleTableProps {
72+
roles: Role[] | undefined;
73+
}
74+
75+
const RoleTable: FC<RoleTableProps> = ({ roles }) => {
76+
const isLoading = false;
77+
const isEmpty = Boolean(roles && roles.length === 0);
78+
return (
79+
<TableContainer>
80+
<Table>
81+
<TableHead>
82+
<TableRow>
83+
<TableCell width="45%">Idp Role</TableCell>
84+
<TableCell width="55%">Coder Role</TableCell>
85+
</TableRow>
86+
</TableHead>
87+
<TableBody>
88+
<ChooseOne>
89+
<Cond condition={isLoading}>
90+
<TableLoader />
91+
</Cond>
92+
93+
<Cond condition={isEmpty}>
94+
<TableRow>
95+
<TableCell colSpan={999}>
96+
<EmptyState
97+
message="No Role Mappings"
98+
description={
99+
"Configure role sync mappings to manage permissions outside of Coder."
100+
}
101+
isCompact
102+
cta={
103+
<Button
104+
startIcon={<LaunchOutlined />}
105+
component="a"
106+
href={docs(
107+
"/cli/server#--notifications-webhook-endpoint",
108+
)}
109+
target="_blank"
110+
>
111+
How to setup IdP role sync
112+
</Button>
113+
}
114+
/>
115+
</TableCell>
116+
</TableRow>
117+
</Cond>
118+
119+
<Cond>
120+
{roles?.map((role) => (
121+
<RoleRow key={role.name} role={role} />
122+
))}
123+
</Cond>
124+
</ChooseOne>
125+
</TableBody>
126+
</Table>
127+
</TableContainer>
128+
);
129+
};
130+
131+
interface RoleRowProps {
132+
role: Role;
133+
}
134+
135+
const RoleRow: FC<RoleRowProps> = ({ role }) => {
136+
return (
137+
<TableRow data-testid={`role-${role.name}`}>
138+
<TableCell>{role.display_name || role.name}</TableCell>
139+
<TableCell css={styles.secondary}>test</TableCell>
140+
</TableRow>
141+
);
142+
};
143+
144+
const TableLoader = () => {
145+
return (
146+
<TableLoaderSkeleton>
147+
<TableRowSkeleton>
148+
<TableCell>
149+
<Skeleton variant="text" width="25%" />
150+
</TableCell>
151+
<TableCell>
152+
<Skeleton variant="text" width="25%" />
153+
</TableCell>
154+
<TableCell>
155+
<Skeleton variant="text" width="25%" />
156+
</TableCell>
157+
</TableRowSkeleton>
158+
</TableLoaderSkeleton>
159+
);
160+
};
161+
162+
const styles = {
163+
secondary: (theme) => ({
164+
color: theme.palette.text.secondary,
165+
}),
166+
fields: (theme) => ({
167+
marginBottom: "60px",
168+
}),
169+
legend: (theme) => ({
170+
padding: "0px 6px",
171+
fontWeight: 600,
172+
}),
173+
box: (theme) => ({
174+
border: "1px solid",
175+
borderColor: theme.palette.divider,
176+
padding: "0px 20px",
177+
borderRadius: 8,
178+
}),
179+
} satisfies Record<string, Interpolation<Theme>>;
180+
181+
export default IdpSyncPageView;

site/src/pages/ManagementSettingsPage/SidebarView.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,13 @@ const OrganizationSettingsNavigation: FC<
275275
Roles
276276
</SidebarNavSubItem>
277277
)}
278+
{organization.permissions.editMembers && (
279+
<SidebarNavSubItem
280+
href={urlForSubpage(organization.name, "idp-sync")}
281+
>
282+
Idp Sync
283+
</SidebarNavSubItem>
284+
)}
278285
</Stack>
279286
)}
280287
</>

site/src/router.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,9 @@ const OrganizationCustomRolesPage = lazy(
247247
() =>
248248
import("./pages/ManagementSettingsPage/CustomRolesPage/CustomRolesPage"),
249249
);
250+
const OrganizationIdPSyncPage = lazy(
251+
() => import("./pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPage"),
252+
);
250253
const CreateEditRolePage = lazy(
251254
() =>
252255
import("./pages/ManagementSettingsPage/CustomRolesPage/CreateEditRolePage"),
@@ -399,6 +402,9 @@ export const router = createBrowserRouter(
399402
<Route path="create" element={<CreateEditRolePage />} />
400403
<Route path=":roleName" element={<CreateEditRolePage />} />
401404
</Route>
405+
<Route path="idp-sync">
406+
<Route index element={<OrganizationIdPSyncPage />} />
407+
</Route>
402408
</Route>
403409
</Route>
404410

0 commit comments

Comments
 (0)