mirror of
https://github.com/coder/coder.git
synced 2025-07-23 21:32:07 +00:00
feat: create idp sync page skeleton (#14543)
* feat: initial commit for idp skeleton page * feat: add optional tooltip icon to settings header * feat: add help tooltip * feat: add mock data and update pageview for mock data * feat: initial stories * feat: error circle * feat: cleanup * feat: update StatusIndicator for outlined variant * feat: use StatusIndicator instead of Circle * chore: cleanup * fix: remove ternaries in css * fix: updates for PR review comments * chore: add story for compact empty state * feat: extract IdpField and improve field spacing
This commit is contained in:
@ -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,
|
||||
},
|
||||
};
|
||||
|
@ -7,6 +7,7 @@ export interface EmptyStateProps extends HTMLAttributes<HTMLDivElement> {
|
||||
description?: string | ReactNode;
|
||||
cta?: ReactNode;
|
||||
image?: ReactNode;
|
||||
isCompact?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -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>
|
||||
|
@ -9,6 +9,7 @@ interface HeaderProps {
|
||||
description?: ReactNode;
|
||||
secondary?: boolean;
|
||||
docsHref?: string;
|
||||
tooltip?: ReactNode;
|
||||
}
|
||||
|
||||
export const SettingsHeader: FC<HeaderProps> = ({
|
||||
@ -16,32 +17,36 @@ export const SettingsHeader: FC<HeaderProps> = ({
|
||||
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={{
|
||||
|
@ -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}`,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
@ -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-.*$",
|
||||
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 = () => {
|
||||
// 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;
|
@ -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 },
|
||||
};
|
@ -0,0 +1,284 @@
|
||||
import type { Interpolation, Theme } from "@emotion/react";
|
||||
import { useTheme } 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 { OIDCConfig } 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 { StatusIndicator } from "components/StatusIndicator/StatusIndicator";
|
||||
import {
|
||||
TableLoaderSkeleton,
|
||||
TableRowSkeleton,
|
||||
} from "components/TableLoader/TableLoader";
|
||||
import type { FC } from "react";
|
||||
import { docs } from "utils/docs";
|
||||
|
||||
export type IdpSyncPageViewProps = {
|
||||
oidcConfig: OIDCConfig | undefined;
|
||||
};
|
||||
|
||||
export const IdpSyncPageView: FC<IdpSyncPageViewProps> = ({ oidcConfig }) => {
|
||||
const theme = useTheme();
|
||||
const {
|
||||
groups_field,
|
||||
user_role_field,
|
||||
group_regex_filter,
|
||||
group_auto_create,
|
||||
} = oidcConfig || {};
|
||||
return (
|
||||
<>
|
||||
<ChooseOne>
|
||||
<Cond condition={false}>
|
||||
<Paywall
|
||||
message="IdP Sync"
|
||||
description="Configure group and role mappings to manage permissions outside of Coder."
|
||||
documentationLink={docs("/admin/groups")}
|
||||
/>
|
||||
</Cond>
|
||||
<Cond>
|
||||
<Stack spacing={2} css={styles.fields}>
|
||||
{/* Semantically fieldset is used for forms. In the future this screen will allow
|
||||
updates to these fields in a form */}
|
||||
<fieldset css={styles.box}>
|
||||
<legend css={styles.legend}>Groups</legend>
|
||||
<Stack direction={"row"} alignItems={"center"} spacing={8}>
|
||||
<IdpField
|
||||
name={"Sync Field"}
|
||||
fieldText={groups_field}
|
||||
showStatusIndicator
|
||||
/>
|
||||
<IdpField
|
||||
name={"Regex Filter"}
|
||||
fieldText={group_regex_filter}
|
||||
/>
|
||||
<IdpField
|
||||
name={"Auto Create"}
|
||||
fieldText={group_auto_create?.toString()}
|
||||
/>
|
||||
</Stack>
|
||||
</fieldset>
|
||||
<fieldset css={styles.box}>
|
||||
<legend css={styles.legend}>Roles</legend>
|
||||
<Stack direction={"row"} alignItems={"center"} spacing={3}>
|
||||
<IdpField
|
||||
name={"Sync Field"}
|
||||
fieldText={user_role_field}
|
||||
showStatusIndicator
|
||||
/>
|
||||
</Stack>
|
||||
</fieldset>
|
||||
</Stack>
|
||||
<Stack spacing={6}>
|
||||
<IdpMappingTable
|
||||
type="Role"
|
||||
isEmpty={Boolean(
|
||||
!oidcConfig?.user_role_mapping ||
|
||||
Object.entries(oidcConfig?.user_role_mapping).length === 0,
|
||||
)}
|
||||
>
|
||||
<>
|
||||
{oidcConfig?.user_role_mapping &&
|
||||
Object.entries(oidcConfig.user_role_mapping)
|
||||
.sort()
|
||||
.map(([idpRole, roles]) => (
|
||||
<RoleRow
|
||||
key={idpRole}
|
||||
idpRole={idpRole}
|
||||
coderRoles={roles}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
</IdpMappingTable>
|
||||
<IdpMappingTable
|
||||
type="Group"
|
||||
isEmpty={Boolean(
|
||||
!oidcConfig?.group_mapping ||
|
||||
Object.entries(oidcConfig?.group_mapping).length === 0,
|
||||
)}
|
||||
>
|
||||
<>
|
||||
{oidcConfig?.user_role_mapping &&
|
||||
Object.entries(oidcConfig.group_mapping)
|
||||
.sort()
|
||||
.map(([idpGroup, group]) => (
|
||||
<GroupRow
|
||||
key={idpGroup}
|
||||
idpGroup={idpGroup}
|
||||
coderGroup={group}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
</IdpMappingTable>
|
||||
</Stack>
|
||||
</Cond>
|
||||
</ChooseOne>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
interface IdpFieldProps {
|
||||
name: string;
|
||||
fieldText: string | undefined;
|
||||
showStatusIndicator?: boolean;
|
||||
}
|
||||
|
||||
const IdpField: FC<IdpFieldProps> = ({
|
||||
name,
|
||||
fieldText,
|
||||
showStatusIndicator = false,
|
||||
}) => {
|
||||
return (
|
||||
<span css={{ display: "flex", alignItems: "center", gap: "16px" }}>
|
||||
<h4>{name}</h4>
|
||||
<p css={styles.secondary}>
|
||||
{fieldText ||
|
||||
(showStatusIndicator && (
|
||||
<div
|
||||
css={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "8px",
|
||||
height: 0,
|
||||
}}
|
||||
>
|
||||
<StatusIndicator color="error" />
|
||||
<p>disabled</p>
|
||||
</div>
|
||||
))}
|
||||
</p>
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
interface IdpMappingTableProps {
|
||||
type: "Role" | "Group";
|
||||
isEmpty: boolean;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const IdpMappingTable: FC<IdpMappingTableProps> = ({
|
||||
type,
|
||||
isEmpty,
|
||||
children,
|
||||
}) => {
|
||||
const isLoading = false;
|
||||
|
||||
return (
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell width="45%">Idp {type}</TableCell>
|
||||
<TableCell width="55%">Coder {type}</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
<ChooseOne>
|
||||
<Cond condition={isLoading}>
|
||||
<TableLoader />
|
||||
</Cond>
|
||||
|
||||
<Cond condition={isEmpty}>
|
||||
<TableRow>
|
||||
<TableCell colSpan={999}>
|
||||
<EmptyState
|
||||
message={`No ${type} Mappings`}
|
||||
isCompact
|
||||
cta={
|
||||
<Button
|
||||
startIcon={<LaunchOutlined />}
|
||||
component="a"
|
||||
href={docs("/admin/auth#group-sync-enterprise")}
|
||||
target="_blank"
|
||||
>
|
||||
How to setup IdP {type} sync
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</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 {
|
||||
idpRole: string;
|
||||
coderRoles: ReadonlyArray<string>;
|
||||
}
|
||||
|
||||
const RoleRow: FC<RoleRowProps> = ({ idpRole, coderRoles }) => {
|
||||
return (
|
||||
<TableRow data-testid={`role-${idpRole}`}>
|
||||
<TableCell>{idpRole}</TableCell>
|
||||
<TableCell css={styles.secondary}>coderRoles Placeholder</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
};
|
||||
|
||||
const TableLoader = () => {
|
||||
return (
|
||||
<TableLoaderSkeleton>
|
||||
<TableRowSkeleton>
|
||||
<TableCell>
|
||||
<Skeleton variant="text" width="25%" />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Skeleton variant="text" width="25%" />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Skeleton variant="text" width="25%" />
|
||||
</TableCell>
|
||||
</TableRowSkeleton>
|
||||
</TableLoaderSkeleton>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = {
|
||||
secondary: (theme) => ({
|
||||
color: theme.palette.text.secondary,
|
||||
}),
|
||||
fields: () => ({
|
||||
marginBottom: "60px",
|
||||
}),
|
||||
legend: () => ({
|
||||
padding: "0px 6px",
|
||||
fontWeight: 600,
|
||||
}),
|
||||
box: (theme) => ({
|
||||
border: "1px solid",
|
||||
borderColor: theme.palette.divider,
|
||||
padding: "0px 20px",
|
||||
borderRadius: 8,
|
||||
}),
|
||||
} satisfies Record<string, Interpolation<Theme>>;
|
||||
|
||||
export default IdpSyncPageView;
|
@ -282,6 +282,13 @@ const OrganizationSettingsNavigation: FC<
|
||||
Provisioners
|
||||
</SidebarNavSubItem>
|
||||
)}
|
||||
{organization.permissions.editMembers && (
|
||||
<SidebarNavSubItem
|
||||
href={urlForSubpage(organization.name, "idp-sync")}
|
||||
>
|
||||
IdP Sync
|
||||
</SidebarNavSubItem>
|
||||
)}
|
||||
</Stack>
|
||||
)}
|
||||
</>
|
||||
|
@ -1,32 +1,12 @@
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { Stack } from "components/Stack/Stack";
|
||||
import { StatusIndicator } from "components/StatusIndicator/StatusIndicator";
|
||||
import dayjs from "dayjs";
|
||||
import relativeTime from "dayjs/plugin/relativeTime";
|
||||
import { useTime } from "hooks/useTime";
|
||||
import type { FC } from "react";
|
||||
|
||||
dayjs.extend(relativeTime);
|
||||
|
||||
type CircleProps = {
|
||||
color: string;
|
||||
variant?: "solid" | "outlined";
|
||||
};
|
||||
|
||||
const Circle: FC<CircleProps> = ({ color, variant = "solid" }) => {
|
||||
return (
|
||||
<div
|
||||
aria-hidden
|
||||
css={{
|
||||
width: 8,
|
||||
height: 8,
|
||||
backgroundColor: variant === "solid" ? color : undefined,
|
||||
border: variant === "outlined" ? `1px solid ${color}` : undefined,
|
||||
borderRadius: 9999,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
interface LastUsedProps {
|
||||
lastUsedAt: string;
|
||||
}
|
||||
@ -38,21 +18,19 @@ export const LastUsed: FC<LastUsedProps> = ({ lastUsedAt }) => {
|
||||
const t = dayjs(lastUsedAt);
|
||||
const now = dayjs();
|
||||
let message = t.fromNow();
|
||||
let circle = (
|
||||
<Circle color={theme.palette.text.secondary} variant="outlined" />
|
||||
);
|
||||
let circle = <StatusIndicator color="info" variant="outlined" />;
|
||||
|
||||
if (t.isAfter(now.subtract(1, "hour"))) {
|
||||
circle = <Circle color={theme.roles.success.fill.solid} />;
|
||||
circle = <StatusIndicator color="success" />;
|
||||
// Since the agent reports on a 10m interval,
|
||||
// the last_used_at can be inaccurate when recent.
|
||||
message = "Now";
|
||||
} else if (t.isAfter(now.subtract(3, "day"))) {
|
||||
circle = <Circle color={theme.palette.text.secondary} />;
|
||||
circle = <StatusIndicator color="info" />;
|
||||
} else if (t.isAfter(now.subtract(1, "month"))) {
|
||||
circle = <Circle color={theme.roles.warning.fill.solid} />;
|
||||
circle = <StatusIndicator color="warning" />;
|
||||
} else if (t.isAfter(now.subtract(100, "year"))) {
|
||||
circle = <Circle color={theme.roles.error.fill.solid} />;
|
||||
circle = <StatusIndicator color="error" />;
|
||||
} else {
|
||||
message = "Never";
|
||||
}
|
||||
|
@ -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={<OrganizationProvisionersPage />}
|
||||
/>
|
||||
<Route path="idp-sync" element={<OrganizationIdPSyncPage />} />
|
||||
</Route>
|
||||
</Route>
|
||||
|
||||
|
@ -451,6 +451,38 @@ export const MockAssignableSiteRoles = [
|
||||
assignableRole(MockAuditorRole, true),
|
||||
];
|
||||
|
||||
export const MockOIDCConfig: TypesGen.OIDCConfig = {
|
||||
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 MockMemberPermissions = {
|
||||
viewAuditLog: false,
|
||||
};
|
||||
|
Reference in New Issue
Block a user