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
10 changes: 8 additions & 2 deletions site/src/components/EmptyState/EmptyState.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,17 @@ const meta: Meta<typeof EmptyState> = {
export default meta;
type Story = StoryObj<typeof EmptyState>;

const Example: Story = {
export const Example: Story = {
args: {
description: "It is easy, just click the button below",
cta: <Button>Create workspace</Button>,
},
};

export { Example as EmptyState };
export const Compact: Story = {
args: {
description: "It is easy, just click the button below",
cta: <Button>Create workspace</Button>,
isCompact: true,
},
};
30 changes: 19 additions & 11 deletions site/src/components/EmptyState/EmptyState.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export interface EmptyStateProps extends HTMLAttributes<HTMLDivElement> {
description?: string | ReactNode;
cta?: ReactNode;
image?: ReactNode;
isCompact?: boolean;
}

/**
Expand All @@ -19,21 +20,28 @@ export const EmptyState: FC<EmptyStateProps> = ({
description,
cta,
image,
isCompact,
...attrs
}) => {
return (
<div
css={{
overflow: "hidden",
display: "flex",
flexDirection: "column",
justifyContent: "center",
alignItems: "center",
textAlign: "center",
minHeight: 360,
padding: "80px 40px",
position: "relative",
}}
css={[
{
overflow: "hidden",
display: "flex",
flexDirection: "column",
justifyContent: "center",
alignItems: "center",
textAlign: "center",
minHeight: 360,
padding: "80px 40px",
position: "relative",
},
isCompact && {
minHeight: 180,
padding: "10px 40px",
},
]}
{...attrs}
>
<h5 css={{ fontSize: 24, fontWeight: 500, margin: 0 }}>{message}</h5>
Expand Down
45 changes: 25 additions & 20 deletions site/src/components/SettingsHeader/SettingsHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,39 +9,44 @@ interface HeaderProps {
description?: ReactNode;
secondary?: boolean;
docsHref?: string;
tooltip?: ReactNode;
}

export const SettingsHeader: FC<HeaderProps> = ({
title,
description,
docsHref,
secondary,
tooltip,
}) => {
const theme = useTheme();

return (
<Stack alignItems="baseline" direction="row" justifyContent="space-between">
<div css={{ maxWidth: 420, marginBottom: 24 }}>
<h1
css={[
{
fontSize: 32,
fontWeight: 700,
display: "flex",
alignItems: "center",
lineHeight: "initial",
margin: 0,
marginBottom: 4,
gap: 8,
},
secondary && {
fontSize: 24,
fontWeight: 500,
},
]}
>
{title}
</h1>
<Stack direction="row" spacing={1} alignItems="center">
<h1
css={[
{
fontSize: 32,
fontWeight: 700,
display: "flex",
alignItems: "center",
lineHeight: "initial",
margin: 0,
marginBottom: 4,
gap: 8,
},
secondary && {
fontSize: 24,
fontWeight: 500,
},
]}
>
{title}
</h1>
{tooltip}
</Stack>
{description && (
<span
css={{
Expand Down
25 changes: 18 additions & 7 deletions site/src/components/StatusIndicator/StatusIndicator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,30 @@ import type { ThemeRole } from "theme/roles";

interface StatusIndicatorProps {
color: ThemeRole;
variant?: "solid" | "outlined";
}

export const StatusIndicator: FC<StatusIndicatorProps> = ({ color }) => {
export const StatusIndicator: FC<StatusIndicatorProps> = ({
color,
variant = "solid",
}) => {
const theme = useTheme();

return (
<div
css={{
height: 8,
width: 8,
borderRadius: 4,
backgroundColor: theme.roles[color].fill.solid,
}}
css={[
{
height: 8,
width: 8,
borderRadius: 4,
},
variant === "solid" && {
backgroundColor: theme.roles[color].fill.solid,
},
variant === "outlined" && {
border: `1px solid ${theme.roles[color].outline}`,
},
]}
/>
);
};
Original file line number Diff line number Diff line change
@@ -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 (
<HelpTooltip>
<HelpTooltipTrigger />
<HelpTooltipContent>
<HelpTooltipTitle>What is IdP Sync?</HelpTooltipTitle>
<HelpTooltipText>
View the current mappings between your external OIDC provider and
Coder. Use the Coder CLI to configure these mappings.
</HelpTooltipText>
<HelpTooltipLinksGroup>
<HelpTooltipLink href={docs("/admin/auth#group-sync-enterprise")}>
Configure IdP Sync
</HelpTooltipLink>
</HelpTooltipLinksGroup>
</HelpTooltipContent>
</HelpTooltip>
);
};
97 changes: 97 additions & 0 deletions site/src/pages/ManagementSettingsPage/IdpSyncPage/IdpSyncPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import AddIcon from "@mui/icons-material/AddOutlined";
import LaunchOutlined from "@mui/icons-material/LaunchOutlined";
import Button from "@mui/material/Button";
import { SettingsHeader } from "components/SettingsHeader/SettingsHeader";
import { Stack } from "components/Stack/Stack";
import type { FC } from "react";
import { Helmet } from "react-helmet-async";
import { Link as RouterLink } from "react-router-dom";
import { docs } from "utils/docs";
import { pageTitle } from "utils/page";
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?

// feature visibility and permissions to be implemented when integrating with backend
// const feats = 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 permissions = permissionsQuery.data;

// if (!permissions) {
// return <Loader />;
// }

return (
<>
<Helmet>
<title>{pageTitle("IdP Sync")}</title>
</Helmet>

<Stack
alignItems="baseline"
direction="row"
justifyContent="space-between"
>
<SettingsHeader
title="IdP Sync"
description="Group and role sync mappings (configured outside Coder)."
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>
<Button component={RouterLink} startIcon={<AddIcon />} to="export">
Export Policy
</Button>
</Stack>
</Stack>

<IdpSyncPageView oidcConfig={mockOIDCConfig} />
</>
);
};

export default IdpSyncPage;
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import type { Meta, StoryObj } from "@storybook/react";
import { MockOIDCConfig } from "testHelpers/entities";
import { IdpSyncPageView } from "./IdpSyncPageView";

const meta: Meta<typeof IdpSyncPageView> = {
title: "pages/OrganizationIdpSyncPage",
component: IdpSyncPageView,
};

export default meta;
type Story = StoryObj<typeof IdpSyncPageView>;

export const Empty: Story = {
args: { oidcConfig: undefined },
};

export const Default: Story = {
args: { oidcConfig: MockOIDCConfig },
};
Loading
Loading