From b2c3e11f679e5ab36782605f1198557d7a48d676 Mon Sep 17 00:00:00 2001 From: Jaayden Halko Date: Fri, 30 Aug 2024 20:11:44 +0000 Subject: [PATCH 01/14] feat: initial commit for idp skeleton page --- site/src/components/EmptyState/EmptyState.tsx | 6 +- .../IdpSyncPage/IdpSyncPage.tsx | 83 ++++++++ .../IdpSyncPage/IdpSyncPageView.tsx | 181 ++++++++++++++++++ .../ManagementSettingsPage/SidebarView.tsx | 7 + site/src/router.tsx | 4 + 5 files changed, 279 insertions(+), 2 deletions(-) create mode 100644 site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPage.tsx create mode 100644 site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.tsx diff --git a/site/src/components/EmptyState/EmptyState.tsx b/site/src/components/EmptyState/EmptyState.tsx index c34f634d71bab..a6d27169b9fce 100644 --- a/site/src/components/EmptyState/EmptyState.tsx +++ b/site/src/components/EmptyState/EmptyState.tsx @@ -7,6 +7,7 @@ export interface EmptyStateProps extends HTMLAttributes { description?: string | ReactNode; cta?: ReactNode; image?: ReactNode; + isCompact?: boolean; } /** @@ -19,6 +20,7 @@ export const EmptyState: FC = ({ description, cta, image, + isCompact, ...attrs }) => { return ( @@ -30,8 +32,8 @@ export const EmptyState: FC = ({ justifyContent: "center", alignItems: "center", textAlign: "center", - minHeight: 360, - padding: "80px 40px", + minHeight: isCompact ? 180 : 360, + padding: isCompact ? "10px 40px" : "80px 40px", position: "relative", }} {...attrs} diff --git a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPage.tsx b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPage.tsx new file mode 100644 index 0000000000000..07a6dc7485839 --- /dev/null +++ b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPage.tsx @@ -0,0 +1,83 @@ +import AddIcon from "@mui/icons-material/AddOutlined"; +import LaunchOutlined from "@mui/icons-material/LaunchOutlined"; +import Button from "@mui/material/Button"; +import { getErrorMessage } from "api/errors"; +import { organizationPermissions } from "api/queries/organizations"; +import { organizationRoles } from "api/queries/roles"; +import { displayError, displaySuccess } from "components/GlobalSnackbar/utils"; +import { Loader } from "components/Loader/Loader"; +import { SettingsHeader } from "components/SettingsHeader/SettingsHeader"; +import { Stack } from "components/Stack/Stack"; +import { useFeatureVisibility } from "modules/dashboard/useFeatureVisibility"; +import { type FC, useEffect, useState } from "react"; +import { Helmet } from "react-helmet-async"; +import { useQuery, useQueryClient } from "react-query"; +import { Link as RouterLink, useParams } from "react-router-dom"; +import { pageTitle } from "utils/page"; +import { useOrganizationSettings } from "../ManagementSettingsLayout"; +import { docs } from "utils/docs"; +import IdpSyncPageView from "./IdpSyncPageView"; + +export const IdpSyncPage: FC = () => { + const queryClient = useQueryClient(); + // const { custom_roles: isCustomRolesEnabled } = useFeatureVisibility(); + const { organization: organizationName } = useParams() as { + organization: string; + }; + const { organizations } = useOrganizationSettings(); + const organization = organizations?.find((o) => o.name === organizationName); + const permissionsQuery = useQuery(organizationPermissions(organization?.id)); + const organizationRolesQuery = useQuery(organizationRoles(organizationName)); + const permissions = permissionsQuery.data; + + useEffect(() => { + if (organizationRolesQuery.error) { + displayError( + getErrorMessage( + organizationRolesQuery.error, + "Error loading custom roles.", + ), + ); + } + }, [organizationRolesQuery.error]); + + if (!permissions) { + return ; + } + + return ( + <> + + {pageTitle("IdP Sync")} + + + + + + + + + + + + + ); +}; + +export default IdpSyncPage; diff --git a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.tsx b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.tsx new file mode 100644 index 0000000000000..d4503810cba2d --- /dev/null +++ b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.tsx @@ -0,0 +1,181 @@ +import type { Interpolation, Theme } from "@emotion/react"; +import LaunchOutlined from "@mui/icons-material/LaunchOutlined"; +import Button from "@mui/material/Button"; +import Skeleton from "@mui/material/Skeleton"; +import Table from "@mui/material/Table"; +import TableBody from "@mui/material/TableBody"; +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 { ChooseOne, Cond } from "components/Conditionals/ChooseOne"; +import { EmptyState } from "components/EmptyState/EmptyState"; +import { Paywall } from "components/Paywall/Paywall"; +import { Stack } from "components/Stack/Stack"; +import { + TableLoaderSkeleton, + TableRowSkeleton, +} from "components/TableLoader/TableLoader"; +import type { FC } from "react"; +import { docs } from "utils/docs"; + +export type IdpSyncPageViewProps = { + roles: Role[] | undefined; +}; + +export const IdpSyncPageView: FC = ({ roles }) => { + return ( + <> + + + + + + + {/* Semantically fieldset is used for forms. In the future this screen will allow + updates to these fields in a form */} +
+ Groups + +

Sync Field

+

groups

+

Regex Filter

+

^Coder-.*$

+

Auto Create

+

false

+
+
+
+ Roles + +

Sync Field

+

roles

+
+
+
+ + + + +
+
+ + ); +}; + +interface RoleTableProps { + roles: Role[] | undefined; +} + +const RoleTable: FC = ({ roles }) => { + const isLoading = false; + const isEmpty = Boolean(roles && roles.length === 0); + return ( + + + + + Idp Role + Coder Role + + + + + + + + + + + + } + component="a" + href={docs( + "/cli/server#--notifications-webhook-endpoint", + )} + target="_blank" + > + How to setup IdP role sync + + } + /> + + + + + + {roles?.map((role) => ( + + ))} + + + +
+
+ ); +}; + +interface RoleRowProps { + role: Role; +} + +const RoleRow: FC = ({ role }) => { + return ( + + {role.display_name || role.name} + test + + ); +}; + +const TableLoader = () => { + return ( + + + + + + + + + + + + + + ); +}; + +const styles = { + secondary: (theme) => ({ + color: theme.palette.text.secondary, + }), + fields: (theme) => ({ + marginBottom: "60px", + }), + legend: (theme) => ({ + padding: "0px 6px", + fontWeight: 600, + }), + box: (theme) => ({ + border: "1px solid", + borderColor: theme.palette.divider, + padding: "0px 20px", + borderRadius: 8, + }), +} satisfies Record>; + +export default IdpSyncPageView; diff --git a/site/src/pages/ManagementSettingsPage/SidebarView.tsx b/site/src/pages/ManagementSettingsPage/SidebarView.tsx index d635279a4d94a..7b0cce8468573 100644 --- a/site/src/pages/ManagementSettingsPage/SidebarView.tsx +++ b/site/src/pages/ManagementSettingsPage/SidebarView.tsx @@ -282,6 +282,13 @@ const OrganizationSettingsNavigation: FC< Provisioners )} + {organization.permissions.editMembers && ( + + Idp Sync + + )} )} diff --git a/site/src/router.tsx b/site/src/router.tsx index a85cdb9a31bfb..c5c9e68f0f314 100644 --- a/site/src/router.tsx +++ b/site/src/router.tsx @@ -247,6 +247,9 @@ const OrganizationCustomRolesPage = lazy( () => import("./pages/ManagementSettingsPage/CustomRolesPage/CustomRolesPage"), ); +const OrganizationIdPSyncPage = lazy( + () => import("./pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPage"), +); const CreateEditRolePage = lazy( () => import("./pages/ManagementSettingsPage/CustomRolesPage/CreateEditRolePage"), @@ -406,6 +409,7 @@ export const router = createBrowserRouter( path="provisioners" element={} /> + } /> From 2de6bfcdf4210201cba2ffca3bfa5acb90bc00f9 Mon Sep 17 00:00:00 2001 From: Jaayden Halko Date: Sun, 1 Sep 2024 19:21:52 +0000 Subject: [PATCH 02/14] feat: add optional tooltip icon to settings header --- .../SettingsHeader/SettingsHeader.tsx | 45 ++++++++++--------- 1 file changed, 25 insertions(+), 20 deletions(-) diff --git a/site/src/components/SettingsHeader/SettingsHeader.tsx b/site/src/components/SettingsHeader/SettingsHeader.tsx index 954c7b6a64341..ea68415cc1e5b 100644 --- a/site/src/components/SettingsHeader/SettingsHeader.tsx +++ b/site/src/components/SettingsHeader/SettingsHeader.tsx @@ -9,6 +9,7 @@ interface HeaderProps { description?: ReactNode; secondary?: boolean; docsHref?: string; + tooltip?: ReactNode; } export const SettingsHeader: FC = ({ @@ -16,32 +17,36 @@ export const SettingsHeader: FC = ({ description, docsHref, secondary, + tooltip, }) => { const theme = useTheme(); return (
-

- {title} -

+ +

+ {title} +

+ {tooltip} +
{description && ( Date: Sun, 1 Sep 2024 19:22:47 +0000 Subject: [PATCH 03/14] feat: add help tooltip --- .../IdpSyncPage/IdpSyncHelpTooltip.tsx | 31 +++++++++++++++++++ .../IdpSyncPage/IdpSyncPage.tsx | 6 ++-- .../IdpSyncPage/IdpSyncPageView.tsx | 4 +-- .../ManagementSettingsPage/SidebarView.tsx | 2 +- 4 files changed, 37 insertions(+), 6 deletions(-) create mode 100644 site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncHelpTooltip.tsx diff --git a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncHelpTooltip.tsx b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncHelpTooltip.tsx new file mode 100644 index 0000000000000..d2907e4d192f7 --- /dev/null +++ b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncHelpTooltip.tsx @@ -0,0 +1,31 @@ +import { + HelpTooltip, + HelpTooltipContent, + HelpTooltipLink, + HelpTooltipLinksGroup, + HelpTooltipText, + HelpTooltipTitle, + HelpTooltipTrigger, +} from "components/HelpTooltip/HelpTooltip"; +import type { FC } from "react"; +import { docs } from "utils/docs"; + +export const IdpSyncHelpTooltip: FC = () => { + return ( + + + + What is IdP Sync? + + View the current mappings between your external OIDC provider and + Coder. Use the Coder CLI to configure these mappings. + + + + Configure IdP Sync + + + + + ); +}; diff --git a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPage.tsx b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPage.tsx index 07a6dc7485839..a115cd2f61406 100644 --- a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPage.tsx +++ b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPage.tsx @@ -13,9 +13,10 @@ import { type FC, useEffect, useState } from "react"; import { Helmet } from "react-helmet-async"; import { useQuery, useQueryClient } 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 { docs } from "utils/docs"; +import { IdpSyncHelpTooltip } from "./IdpSyncHelpTooltip"; import IdpSyncPageView from "./IdpSyncPageView"; export const IdpSyncPage: FC = () => { @@ -59,12 +60,13 @@ export const IdpSyncPage: FC = () => { } /> , }, }; -export { Example as EmptyState }; +export const Compact: Story = { + args: { + description: "It is easy, just click the button below", + cta: , + isCompact: true, + }, +}; From 1523025e9de578b9f5b9efebfb56fc5117a8f996 Mon Sep 17 00:00:00 2001 From: Jaayden Halko Date: Thu, 5 Sep 2024 23:01:20 +0000 Subject: [PATCH 14/14] feat: extract IdpField and improve field spacing --- .../IdpSyncPage/IdpSyncPageView.tsx | 90 +++++++++++-------- 1 file changed, 53 insertions(+), 37 deletions(-) diff --git a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.tsx b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.tsx index cd24a264ea74f..44552f11f7429 100644 --- a/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.tsx +++ b/site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPageView.tsx @@ -50,48 +50,30 @@ export const IdpSyncPageView: FC = ({ oidcConfig }) => { updates to these fields in a form */}
Groups - -

Sync Field

-

- {groups_field || ( -

- -

disabled

-
- )} -

-

Regex Filter

-

{group_regex_filter || "none"}

-

Auto Create

-

{group_auto_create?.toString()}

+ + + +
Roles -

Sync Field

-

- {user_role_field || ( -

- -

disabled

-
- )} -

+
@@ -143,6 +125,40 @@ export const IdpSyncPageView: FC = ({ oidcConfig }) => { ); }; +interface IdpFieldProps { + name: string; + fieldText: string | undefined; + showStatusIndicator?: boolean; +} + +const IdpField: FC = ({ + name, + fieldText, + showStatusIndicator = false, +}) => { + return ( + +

{name}

+

+ {fieldText || + (showStatusIndicator && ( +

+ +

disabled

+
+ ))} +

+
+ ); +}; + interface IdpMappingTableProps { type: "Role" | "Group"; isEmpty: boolean;