mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
feat: create UI badges for labeling beta features (#14661)
* chore: finish draft work for FeatureBadge component * fix: add visually-hidden helper text for screen readers * chore: add stories for highlighted state * fix: update base styles * chore: remove debug display option * chore: update Popover to propagate events * wip: commit progress on FeatureBadge update * wip: commit more progress * chore: update tag definitions to satify Biome * chore: update all colors for preview role * fix: make sure badge shows as hovered while inside tooltip * wip: commit progress on adding story for controlled variant * fix: sort imports * refactor: change component API to be more obvious/ergonomic * fix: add biome-ignore comments to more base files * fix: update import order again * chore: revert biome-ignore comment * chore: update body text for tooltip * chore: update dark preivew role to use sky palette * chore: update color palettes for light/darkBlue themes * chore: add beta badge to organizations subheader * chore: add beta badge to organizations settings page * chore: beef up font weight for form header * fix: update text contrast for org menu list * chore: add beta badge to deployment dropdown * fix: run biome on imports * chore: remove API for controlling FeatureBadge hover styling externally * chore: add xs size for badge * fix: update font weight for xs feature badges * chore: add beta badges to all org headers * fix: turn badges and tooltips into separate concerns * fix: update hover styling * docs: update wording on comment * fix: apply formatting * chore: rename FeatureBadge to FeatureStageBadge * refactor: redefine FeatureStageBadge * chore: update stories * fix: add blur behavior to popover * chore: revert theme colors * chore: create featureStage branding namespace * fix: make sure cleanup function is set up properly * docs: update wording on comment for clarity * refactor: move styles down
This commit is contained in:
Vendored
+1
@@ -120,6 +120,7 @@
|
||||
"stretchr",
|
||||
"STTY",
|
||||
"stuntest",
|
||||
"subpage",
|
||||
"tailbroker",
|
||||
"tailcfg",
|
||||
"tailexchange",
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { FeatureStageBadge } from "./FeatureStageBadge";
|
||||
|
||||
const meta: Meta<typeof FeatureStageBadge> = {
|
||||
title: "components/FeatureStageBadge",
|
||||
component: FeatureStageBadge,
|
||||
args: {
|
||||
contentType: "beta",
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof FeatureStageBadge>;
|
||||
|
||||
export const MediumBeta: Story = {
|
||||
args: {
|
||||
size: "md",
|
||||
},
|
||||
};
|
||||
|
||||
export const SmallBeta: Story = {
|
||||
args: {
|
||||
size: "sm",
|
||||
},
|
||||
};
|
||||
|
||||
export const LargeBeta: Story = {
|
||||
args: {
|
||||
size: "lg",
|
||||
},
|
||||
};
|
||||
|
||||
export const MediumExperimental: Story = {
|
||||
args: {
|
||||
size: "md",
|
||||
contentType: "experimental",
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,133 @@
|
||||
import type { Interpolation, Theme } from "@emotion/react";
|
||||
import Link from "@mui/material/Link";
|
||||
import { visuallyHidden } from "@mui/utils";
|
||||
import { HelpTooltipContent } from "components/HelpTooltip/HelpTooltip";
|
||||
import { Popover, PopoverTrigger } from "components/Popover/Popover";
|
||||
import type { FC, HTMLAttributes, ReactNode } from "react";
|
||||
import { docs } from "utils/docs";
|
||||
|
||||
/**
|
||||
* All types of feature that we are currently supporting. Defined as record to
|
||||
* ensure that we can't accidentally make typos when writing the badge text.
|
||||
*/
|
||||
const featureStageBadgeTypes = {
|
||||
beta: "beta",
|
||||
experimental: "experimental",
|
||||
} as const satisfies Record<string, ReactNode>;
|
||||
|
||||
type FeatureStageBadgeProps = Readonly<
|
||||
Omit<HTMLAttributes<HTMLSpanElement>, "children"> & {
|
||||
contentType: keyof typeof featureStageBadgeTypes;
|
||||
size?: "sm" | "md" | "lg";
|
||||
}
|
||||
>;
|
||||
|
||||
export const FeatureStageBadge: FC<FeatureStageBadgeProps> = ({
|
||||
contentType,
|
||||
size = "md",
|
||||
...delegatedProps
|
||||
}) => {
|
||||
return (
|
||||
<Popover mode="hover">
|
||||
<PopoverTrigger>
|
||||
{({ isOpen }) => (
|
||||
<span
|
||||
css={[
|
||||
styles.badge,
|
||||
size === "sm" && styles.badgeSmallText,
|
||||
size === "lg" && styles.badgeLargeText,
|
||||
isOpen && styles.badgeHover,
|
||||
]}
|
||||
{...delegatedProps}
|
||||
>
|
||||
<span style={visuallyHidden}> (This is a</span>
|
||||
{featureStageBadgeTypes[contentType]}
|
||||
<span style={visuallyHidden}> feature)</span>
|
||||
</span>
|
||||
)}
|
||||
</PopoverTrigger>
|
||||
|
||||
<HelpTooltipContent
|
||||
anchorOrigin={{ vertical: "bottom", horizontal: "center" }}
|
||||
transformOrigin={{ vertical: "top", horizontal: "center" }}
|
||||
>
|
||||
<p css={styles.tooltipDescription}>
|
||||
This feature has not yet reached general availability (GA).
|
||||
</p>
|
||||
|
||||
<Link
|
||||
href={docs("/contributing/feature-stages")}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
css={styles.tooltipLink}
|
||||
>
|
||||
Learn about feature stages
|
||||
<span style={visuallyHidden}> (link opens in new tab)</span>
|
||||
</Link>
|
||||
</HelpTooltipContent>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = {
|
||||
badge: (theme) => ({
|
||||
// Base type is based on a span so that the element can be placed inside
|
||||
// more types of HTML elements without creating invalid markdown, but we
|
||||
// still want the default display behavior to be div-like
|
||||
display: "block",
|
||||
maxWidth: "fit-content",
|
||||
|
||||
// Base style assumes that medium badges will be the default
|
||||
fontSize: "0.75rem",
|
||||
|
||||
cursor: "default",
|
||||
flexShrink: 0,
|
||||
padding: "4px 8px",
|
||||
lineHeight: 1,
|
||||
whiteSpace: "nowrap",
|
||||
border: `1px solid ${theme.branding.featureStage.border}`,
|
||||
color: theme.branding.featureStage.text,
|
||||
backgroundColor: theme.branding.featureStage.background,
|
||||
borderRadius: "6px",
|
||||
transition:
|
||||
"color 0.2s ease-in-out, border-color 0.2s ease-in-out, background-color 0.2s ease-in-out",
|
||||
}),
|
||||
|
||||
badgeHover: (theme) => ({
|
||||
color: theme.branding.featureStage.hover.text,
|
||||
borderColor: theme.branding.featureStage.hover.border,
|
||||
backgroundColor: theme.branding.featureStage.hover.background,
|
||||
}),
|
||||
|
||||
badgeLargeText: {
|
||||
fontSize: "1rem",
|
||||
},
|
||||
|
||||
badgeSmallText: {
|
||||
// Have to beef up font weight so that the letters still maintain the
|
||||
// same relative thickness as all our other main UI text
|
||||
fontWeight: 500,
|
||||
fontSize: "0.625rem",
|
||||
},
|
||||
|
||||
tooltipTitle: (theme) => ({
|
||||
color: theme.palette.text.primary,
|
||||
fontWeight: 600,
|
||||
fontFamily: "inherit",
|
||||
fontSize: 18,
|
||||
margin: 0,
|
||||
lineHeight: 1,
|
||||
paddingBottom: "8px",
|
||||
}),
|
||||
|
||||
tooltipDescription: {
|
||||
margin: 0,
|
||||
lineHeight: 1.4,
|
||||
paddingBottom: "8px",
|
||||
},
|
||||
|
||||
tooltipLink: {
|
||||
fontWeight: 600,
|
||||
lineHeight: 1.2,
|
||||
},
|
||||
} as const satisfies Record<string, Interpolation<Theme>>;
|
||||
@@ -170,7 +170,7 @@ const styles = {
|
||||
formSectionInfoTitle: (theme) => ({
|
||||
fontSize: 20,
|
||||
color: theme.palette.text.primary,
|
||||
fontWeight: 400,
|
||||
fontWeight: 500,
|
||||
margin: 0,
|
||||
marginBottom: 8,
|
||||
display: "flex",
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
import MuiPopover, {
|
||||
type PopoverProps as MuiPopoverProps,
|
||||
// biome-ignore lint/nursery/noRestrictedImports: Used as base component
|
||||
// biome-ignore lint/nursery/noRestrictedImports: This is the base component that our custom popover is based on
|
||||
} from "@mui/material/Popover";
|
||||
import {
|
||||
type FC,
|
||||
type HTMLAttributes,
|
||||
type PointerEvent,
|
||||
type PointerEventHandler,
|
||||
type ReactElement,
|
||||
type ReactNode,
|
||||
type RefObject,
|
||||
cloneElement,
|
||||
createContext,
|
||||
useContext,
|
||||
useEffect,
|
||||
useId,
|
||||
useRef,
|
||||
useState,
|
||||
@@ -20,10 +23,13 @@ type TriggerMode = "hover" | "click";
|
||||
|
||||
type TriggerRef = RefObject<HTMLElement>;
|
||||
|
||||
type TriggerElement = ReactElement<{
|
||||
ref: TriggerRef;
|
||||
onClick?: () => void;
|
||||
}>;
|
||||
// Have to append ReactNode type to satisfy React's cloneElement function. It
|
||||
// has absolutely no bearing on what happens at runtime
|
||||
type TriggerElement = ReactNode &
|
||||
ReactElement<{
|
||||
ref: TriggerRef;
|
||||
onClick?: () => void;
|
||||
}>;
|
||||
|
||||
type PopoverContextValue = {
|
||||
id: string;
|
||||
@@ -61,6 +67,15 @@ export const Popover: FC<PopoverProps> = (props) => {
|
||||
const [uncontrolledOpen, setUncontrolledOpen] = useState(false);
|
||||
const triggerRef: TriggerRef = useRef(null);
|
||||
|
||||
// Helps makes sure that popovers close properly when the user switches to
|
||||
// a different tab. This won't help with controlled instances of the
|
||||
// component, but this is basically the most we can do from here
|
||||
useEffect(() => {
|
||||
const closeOnTabSwitch = () => setUncontrolledOpen(false);
|
||||
window.addEventListener("blur", closeOnTabSwitch);
|
||||
return () => window.removeEventListener("blur", closeOnTabSwitch);
|
||||
}, []);
|
||||
|
||||
const value: PopoverContextValue = {
|
||||
triggerRef,
|
||||
id: `${hookId}-popover`,
|
||||
@@ -86,30 +101,47 @@ export const usePopover = () => {
|
||||
return context;
|
||||
};
|
||||
|
||||
export const PopoverTrigger = (
|
||||
props: HTMLAttributes<HTMLElement> & {
|
||||
children: TriggerElement;
|
||||
},
|
||||
) => {
|
||||
type PopoverTriggerRenderProps = Readonly<{
|
||||
isOpen: boolean;
|
||||
}>;
|
||||
|
||||
type PopoverTriggerProps = Readonly<
|
||||
Omit<HTMLAttributes<HTMLElement>, "children"> & {
|
||||
children:
|
||||
| TriggerElement
|
||||
| ((props: PopoverTriggerRenderProps) => TriggerElement);
|
||||
}
|
||||
>;
|
||||
|
||||
export const PopoverTrigger: FC<PopoverTriggerProps> = (props) => {
|
||||
const popover = usePopover();
|
||||
const { children, ...elementProps } = props;
|
||||
const { children, onClick, onPointerEnter, onPointerLeave, ...elementProps } =
|
||||
props;
|
||||
|
||||
const clickProps = {
|
||||
onClick: () => {
|
||||
onClick: (event: PointerEvent<HTMLElement>) => {
|
||||
popover.setOpen(true);
|
||||
onClick?.(event);
|
||||
},
|
||||
};
|
||||
|
||||
const hoverProps = {
|
||||
onPointerEnter: () => {
|
||||
onPointerEnter: (event: PointerEvent<HTMLElement>) => {
|
||||
popover.setOpen(true);
|
||||
onPointerEnter?.(event);
|
||||
},
|
||||
onPointerLeave: () => {
|
||||
onPointerLeave: (event: PointerEvent<HTMLElement>) => {
|
||||
popover.setOpen(false);
|
||||
onPointerLeave?.(event);
|
||||
},
|
||||
};
|
||||
|
||||
return cloneElement(props.children, {
|
||||
const evaluatedChildren =
|
||||
typeof children === "function"
|
||||
? children({ isOpen: popover.open })
|
||||
: children;
|
||||
|
||||
return cloneElement(evaluatedChildren, {
|
||||
...elementProps,
|
||||
...(popover.mode === "click" ? clickProps : hoverProps),
|
||||
"aria-haspopup": true,
|
||||
@@ -130,6 +162,8 @@ export type PopoverContentProps = Omit<
|
||||
|
||||
export const PopoverContent: FC<PopoverContentProps> = ({
|
||||
horizontal = "left",
|
||||
onPointerEnter,
|
||||
onPointerLeave,
|
||||
...popoverProps
|
||||
}) => {
|
||||
const popover = usePopover();
|
||||
@@ -152,7 +186,7 @@ export const PopoverContent: FC<PopoverContentProps> = ({
|
||||
},
|
||||
}}
|
||||
{...horizontalProps(horizontal)}
|
||||
{...modeProps(popover)}
|
||||
{...modeProps(popover, onPointerEnter, onPointerLeave)}
|
||||
{...popoverProps}
|
||||
id={popover.id}
|
||||
open={popover.open}
|
||||
@@ -162,14 +196,20 @@ export const PopoverContent: FC<PopoverContentProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
const modeProps = (popover: PopoverContextValue) => {
|
||||
const modeProps = (
|
||||
popover: PopoverContextValue,
|
||||
externalOnPointerEnter: PointerEventHandler<HTMLDivElement> | undefined,
|
||||
externalOnPointerLeave: PointerEventHandler<HTMLDivElement> | undefined,
|
||||
) => {
|
||||
if (popover.mode === "hover") {
|
||||
return {
|
||||
onPointerEnter: () => {
|
||||
onPointerEnter: (event: PointerEvent<HTMLDivElement>) => {
|
||||
popover.setOpen(true);
|
||||
externalOnPointerEnter?.(event);
|
||||
},
|
||||
onPointerLeave: () => {
|
||||
onPointerLeave: (event: PointerEvent<HTMLDivElement>) => {
|
||||
popover.setOpen(false);
|
||||
externalOnPointerLeave?.(event);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ interface HeaderProps {
|
||||
secondary?: boolean;
|
||||
docsHref?: string;
|
||||
tooltip?: ReactNode;
|
||||
badges?: ReactNode;
|
||||
}
|
||||
|
||||
export const SettingsHeader: FC<HeaderProps> = ({
|
||||
@@ -18,35 +19,40 @@ export const SettingsHeader: FC<HeaderProps> = ({
|
||||
docsHref,
|
||||
secondary,
|
||||
tooltip,
|
||||
badges,
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<Stack alignItems="baseline" direction="row" justifyContent="space-between">
|
||||
<div css={{ maxWidth: 420, marginBottom: 24 }}>
|
||||
<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 direction="row" spacing={2} alignItems="center">
|
||||
<Stack direction="row" spacing={1} alignItems="center">
|
||||
<h1
|
||||
css={[
|
||||
{
|
||||
fontSize: 32,
|
||||
fontWeight: 700,
|
||||
display: "flex",
|
||||
alignItems: "baseline",
|
||||
lineHeight: "initial",
|
||||
margin: 0,
|
||||
marginBottom: 4,
|
||||
gap: 8,
|
||||
},
|
||||
secondary && {
|
||||
fontSize: 24,
|
||||
fontWeight: 500,
|
||||
},
|
||||
]}
|
||||
>
|
||||
{title}
|
||||
</h1>
|
||||
{tooltip}
|
||||
</Stack>
|
||||
{badges}
|
||||
</Stack>
|
||||
|
||||
{description && (
|
||||
<span
|
||||
css={{
|
||||
|
||||
@@ -149,7 +149,7 @@ const styles = {
|
||||
menuItem: (theme) => css`
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
gap: 20px;
|
||||
gap: 8px;
|
||||
padding: 8px 20px;
|
||||
font-size: 14px;
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import { organizationPermissions } from "api/queries/organizations";
|
||||
import { deleteOrganizationRole, organizationRoles } from "api/queries/roles";
|
||||
import type { Role } from "api/typesGenerated";
|
||||
import { DeleteDialog } from "components/Dialogs/DeleteDialog/DeleteDialog";
|
||||
import { FeatureStageBadge } from "components/FeatureStageBadge/FeatureStageBadge";
|
||||
import { displayError, displaySuccess } from "components/GlobalSnackbar/utils";
|
||||
import { Loader } from "components/Loader/Loader";
|
||||
import { SettingsHeader } from "components/SettingsHeader/SettingsHeader";
|
||||
@@ -66,6 +67,7 @@ export const CustomRolesPage: FC = () => {
|
||||
<SettingsHeader
|
||||
title="Custom Roles"
|
||||
description="Manage custom roles for this organization."
|
||||
badges={<FeatureStageBadge contentType="beta" size="lg" />}
|
||||
/>
|
||||
{permissions.assignOrgRole && isCustomRolesEnabled && (
|
||||
<Button component={RouterLink} startIcon={<AddIcon />} to="create">
|
||||
|
||||
@@ -5,6 +5,7 @@ import { groupsByOrganization } from "api/queries/groups";
|
||||
import { organizationPermissions } from "api/queries/organizations";
|
||||
import type { Organization } from "api/typesGenerated";
|
||||
import { EmptyState } from "components/EmptyState/EmptyState";
|
||||
import { FeatureStageBadge } from "components/FeatureStageBadge/FeatureStageBadge";
|
||||
import { displayError } from "components/GlobalSnackbar/utils";
|
||||
import { Loader } from "components/Loader/Loader";
|
||||
import { SettingsHeader } from "components/SettingsHeader/SettingsHeader";
|
||||
@@ -80,6 +81,7 @@ export const GroupsPage: FC = () => {
|
||||
<SettingsHeader
|
||||
title="Groups"
|
||||
description="Manage groups for this organization."
|
||||
badges={<FeatureStageBadge contentType="beta" size="lg" />}
|
||||
/>
|
||||
{permissions.createGroup && feats.template_rbac && (
|
||||
<Button component={RouterLink} startIcon={<GroupAdd />} to="create">
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import AddIcon from "@mui/icons-material/AddOutlined";
|
||||
import LaunchOutlined from "@mui/icons-material/LaunchOutlined";
|
||||
import Button from "@mui/material/Button";
|
||||
import { FeatureStageBadge } from "components/FeatureStageBadge/FeatureStageBadge";
|
||||
import { SettingsHeader } from "components/SettingsHeader/SettingsHeader";
|
||||
import { Stack } from "components/Stack/Stack";
|
||||
import type { FC } from "react";
|
||||
@@ -73,6 +74,7 @@ export const IdpSyncPage: FC = () => {
|
||||
title="IdP Sync"
|
||||
description="Group and role sync mappings (configured outside Coder)."
|
||||
tooltip={<IdpSyncHelpTooltip />}
|
||||
badges={<FeatureStageBadge contentType="beta" size="lg" />}
|
||||
/>
|
||||
<Stack direction="row" spacing={2}>
|
||||
<Button
|
||||
|
||||
@@ -17,6 +17,7 @@ import type {
|
||||
} from "api/typesGenerated";
|
||||
import { ErrorAlert } from "components/Alert/ErrorAlert";
|
||||
import { AvatarData } from "components/AvatarData/AvatarData";
|
||||
import { FeatureStageBadge } from "components/FeatureStageBadge/FeatureStageBadge";
|
||||
import { displayError, displaySuccess } from "components/GlobalSnackbar/utils";
|
||||
import {
|
||||
MoreMenu,
|
||||
@@ -60,7 +61,10 @@ export const OrganizationMembersPageView: FC<
|
||||
> = (props) => {
|
||||
return (
|
||||
<div>
|
||||
<SettingsHeader title="Members" />
|
||||
<SettingsHeader
|
||||
title="Members"
|
||||
badges={<FeatureStageBadge contentType="beta" size="lg" />}
|
||||
/>
|
||||
|
||||
<Stack>
|
||||
{Boolean(props.error) && <ErrorAlert error={props.error} />}
|
||||
|
||||
@@ -6,7 +6,8 @@ import type {
|
||||
ProvisionerKeyDaemons,
|
||||
} from "api/typesGenerated";
|
||||
import { EmptyState } from "components/EmptyState/EmptyState";
|
||||
import { PageHeader, PageHeaderTitle } from "components/PageHeader/PageHeader";
|
||||
import { FeatureStageBadge } from "components/FeatureStageBadge/FeatureStageBadge";
|
||||
import { SettingsHeader } from "components/SettingsHeader/SettingsHeader";
|
||||
import { Stack } from "components/Stack/Stack";
|
||||
import { ProvisionerGroup } from "modules/provisioners/ProvisionerGroup";
|
||||
import type { FC } from "react";
|
||||
@@ -17,7 +18,7 @@ interface OrganizationProvisionersPageViewProps {
|
||||
buildInfo?: BuildInfoResponse;
|
||||
|
||||
/** Groups of provisioners, along with their key information */
|
||||
provisioners: ProvisionerKeyDaemons[];
|
||||
provisioners: readonly ProvisionerKeyDaemons[];
|
||||
}
|
||||
|
||||
export const OrganizationProvisionersPageView: FC<
|
||||
@@ -33,21 +34,23 @@ export const OrganizationProvisionersPageView: FC<
|
||||
|
||||
return (
|
||||
<div>
|
||||
<PageHeader
|
||||
// The deployment settings layout already has padding.
|
||||
css={{ paddingTop: 0, paddingBottom: 12 }}
|
||||
actions={
|
||||
<Button
|
||||
endIcon={<OpenInNewIcon />}
|
||||
target="_blank"
|
||||
href={docs("/admin/provisioners")}
|
||||
>
|
||||
Create a provisioner
|
||||
</Button>
|
||||
}
|
||||
<Stack
|
||||
alignItems="baseline"
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
>
|
||||
<PageHeaderTitle>Provisioners</PageHeaderTitle>
|
||||
</PageHeader>
|
||||
<SettingsHeader
|
||||
title="Provisioners"
|
||||
badges={<FeatureStageBadge contentType="beta" size="lg" />}
|
||||
/>
|
||||
<Button
|
||||
endIcon={<OpenInNewIcon />}
|
||||
target="_blank"
|
||||
href={docs("/admin/provisioners")}
|
||||
>
|
||||
Create a provisioner
|
||||
</Button>
|
||||
</Stack>
|
||||
{isEmpty ? (
|
||||
<EmptyState
|
||||
message="No provisioners"
|
||||
|
||||
@@ -8,6 +8,7 @@ import type {
|
||||
} from "api/typesGenerated";
|
||||
import { ErrorAlert } from "components/Alert/ErrorAlert";
|
||||
import { DeleteDialog } from "components/Dialogs/DeleteDialog/DeleteDialog";
|
||||
import { FeatureStageBadge } from "components/FeatureStageBadge/FeatureStageBadge";
|
||||
import {
|
||||
FormFields,
|
||||
FormFooter,
|
||||
@@ -66,7 +67,10 @@ export const OrganizationSettingsPageView: FC<
|
||||
|
||||
return (
|
||||
<div>
|
||||
<SettingsHeader title="Settings" />
|
||||
<SettingsHeader
|
||||
title="Settings"
|
||||
badges={<FeatureStageBadge contentType="beta" size="lg" />}
|
||||
/>
|
||||
|
||||
{Boolean(error) && !isApiValidationError(error) && (
|
||||
<div css={{ marginBottom: 32 }}>
|
||||
|
||||
@@ -7,6 +7,7 @@ import type {
|
||||
Experiments,
|
||||
Organization,
|
||||
} from "api/typesGenerated";
|
||||
import { FeatureStageBadge } from "components/FeatureStageBadge/FeatureStageBadge";
|
||||
import { Loader } from "components/Loader/Loader";
|
||||
import { Sidebar as BaseSidebar } from "components/Sidebar/Sidebar";
|
||||
import { Stack } from "components/Stack/Stack";
|
||||
@@ -47,7 +48,10 @@ export const SidebarView: FC<SidebarProps> = ({
|
||||
// TODO: Do something nice to scroll to the active org.
|
||||
return (
|
||||
<BaseSidebar>
|
||||
<header css={styles.sidebarHeader}>Deployment</header>
|
||||
<header>
|
||||
<h2 css={styles.sidebarHeader}>Deployment</h2>
|
||||
</header>
|
||||
|
||||
<DeploymentSettingsNavigation
|
||||
active={!activeOrganizationName && activeSettings}
|
||||
experiments={experiments}
|
||||
@@ -190,7 +194,18 @@ const OrganizationsSettingsNavigation: FC<
|
||||
|
||||
return (
|
||||
<>
|
||||
<header css={styles.sidebarHeader}>Organizations</header>
|
||||
<header
|
||||
css={{
|
||||
display: "flex",
|
||||
flexFlow: "row wrap",
|
||||
columnGap: "8px",
|
||||
alignItems: "baseline",
|
||||
}}
|
||||
>
|
||||
<h2 css={styles.sidebarHeader}>Organizations</h2>
|
||||
<FeatureStageBadge contentType="beta" size="sm" />
|
||||
</header>
|
||||
|
||||
{permissions.createOrganization && (
|
||||
<SidebarNavItem
|
||||
active="auto"
|
||||
@@ -364,7 +379,8 @@ const SidebarNavSubItem: FC<SidebarNavSubItemProps> = ({
|
||||
const styles = {
|
||||
sidebarHeader: {
|
||||
textTransform: "uppercase",
|
||||
letterSpacing: "0.15em",
|
||||
letterSpacing: "0.1em",
|
||||
margin: 0,
|
||||
fontSize: 11,
|
||||
fontWeight: 500,
|
||||
paddingBottom: 4,
|
||||
@@ -396,7 +412,7 @@ const classNames = {
|
||||
`,
|
||||
|
||||
subLink: (css, theme) => css`
|
||||
color: inherit;
|
||||
color: ${theme.palette.text.secondary};
|
||||
text-decoration: none;
|
||||
|
||||
display: block;
|
||||
@@ -409,11 +425,13 @@ const classNames = {
|
||||
position: relative;
|
||||
|
||||
&:hover {
|
||||
color: ${theme.palette.text.primary};
|
||||
background-color: ${theme.palette.action.hover};
|
||||
}
|
||||
`,
|
||||
|
||||
activeSubLink: (css) => css`
|
||||
activeSubLink: (css, theme) => css`
|
||||
color: ${theme.palette.text.primary};
|
||||
font-weight: 600;
|
||||
`,
|
||||
} satisfies Record<string, ClassName>;
|
||||
|
||||
@@ -175,23 +175,23 @@ const ThemePreview: FC<ThemePreviewProps> = ({
|
||||
<div css={styles.page}>
|
||||
<div css={styles.header}>
|
||||
<div css={styles.headerLinks}>
|
||||
<div css={[styles.headerLink, styles.activeHeaderLink]}></div>
|
||||
<div css={styles.headerLink}></div>
|
||||
<div css={styles.headerLink}></div>
|
||||
<div css={[styles.headerLink, styles.activeHeaderLink]} />
|
||||
<div css={styles.headerLink} />
|
||||
<div css={styles.headerLink} />
|
||||
</div>
|
||||
<div css={styles.headerLinks}>
|
||||
<div css={styles.proxy}></div>
|
||||
<div css={styles.user}></div>
|
||||
<div css={styles.proxy} />
|
||||
<div css={styles.user} />
|
||||
</div>
|
||||
</div>
|
||||
<div css={styles.body}>
|
||||
<div css={styles.title}></div>
|
||||
<div css={styles.title} />
|
||||
<div css={styles.table}>
|
||||
<div css={styles.tableHeader}></div>
|
||||
<div css={styles.workspace}></div>
|
||||
<div css={styles.workspace}></div>
|
||||
<div css={styles.workspace}></div>
|
||||
<div css={styles.workspace}></div>
|
||||
<div css={styles.tableHeader} />
|
||||
<div css={styles.workspace} />
|
||||
<div css={styles.workspace} />
|
||||
<div css={styles.workspace} />
|
||||
<div css={styles.workspace} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,14 +1,29 @@
|
||||
export interface Branding {
|
||||
enterprise: {
|
||||
export type Branding = Readonly<{
|
||||
enterprise: Readonly<{
|
||||
background: string;
|
||||
divider: string;
|
||||
border: string;
|
||||
text: string;
|
||||
};
|
||||
premium: {
|
||||
}>;
|
||||
|
||||
premium: Readonly<{
|
||||
background: string;
|
||||
divider: string;
|
||||
border: string;
|
||||
text: string;
|
||||
};
|
||||
}
|
||||
}>;
|
||||
|
||||
featureStage: Readonly<{
|
||||
background: string;
|
||||
divider: string;
|
||||
border: string;
|
||||
text: string;
|
||||
|
||||
hover: Readonly<{
|
||||
background: string;
|
||||
divider: string;
|
||||
border: string;
|
||||
text: string;
|
||||
}>;
|
||||
}>;
|
||||
}>;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { Branding } from "../branding";
|
||||
import colors from "../tailwindColors";
|
||||
|
||||
export default {
|
||||
export const branding: Branding = {
|
||||
enterprise: {
|
||||
background: colors.blue[950],
|
||||
divider: colors.blue[900],
|
||||
@@ -14,4 +14,20 @@ export default {
|
||||
border: colors.violet[400],
|
||||
text: colors.violet[50],
|
||||
},
|
||||
} satisfies Branding;
|
||||
|
||||
featureStage: {
|
||||
background: colors.sky[950],
|
||||
divider: colors.sky[900],
|
||||
border: colors.sky[400],
|
||||
text: colors.sky[400],
|
||||
|
||||
hover: {
|
||||
background: colors.zinc[950],
|
||||
divider: colors.zinc[900],
|
||||
border: colors.sky[400],
|
||||
text: colors.sky[400],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default branding;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { Roles } from "../roles";
|
||||
import colors from "../tailwindColors";
|
||||
|
||||
export default {
|
||||
const roles: Roles = {
|
||||
danger: {
|
||||
background: colors.orange[950],
|
||||
outline: colors.orange[500],
|
||||
@@ -152,4 +152,6 @@ export default {
|
||||
text: colors.white,
|
||||
},
|
||||
},
|
||||
} satisfies Roles;
|
||||
};
|
||||
|
||||
export default roles;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { Branding } from "../branding";
|
||||
import colors from "../tailwindColors";
|
||||
|
||||
export default {
|
||||
export const branding: Branding = {
|
||||
enterprise: {
|
||||
background: colors.blue[950],
|
||||
divider: colors.blue[900],
|
||||
@@ -14,4 +14,20 @@ export default {
|
||||
border: colors.violet[400],
|
||||
text: colors.violet[50],
|
||||
},
|
||||
} satisfies Branding;
|
||||
|
||||
featureStage: {
|
||||
background: colors.sky[900],
|
||||
divider: colors.sky[800],
|
||||
border: colors.sky[400],
|
||||
text: colors.sky[400],
|
||||
|
||||
hover: {
|
||||
background: colors.gray[900],
|
||||
divider: colors.gray[800],
|
||||
border: colors.sky[400],
|
||||
text: colors.sky[400],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default branding;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { Roles } from "../roles";
|
||||
import colors from "../tailwindColors";
|
||||
|
||||
export default {
|
||||
const roles: Roles = {
|
||||
danger: {
|
||||
background: colors.orange[950],
|
||||
outline: colors.orange[500],
|
||||
@@ -152,4 +152,6 @@ export default {
|
||||
text: colors.white,
|
||||
},
|
||||
},
|
||||
} satisfies Roles;
|
||||
};
|
||||
|
||||
export default roles;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { Branding } from "../branding";
|
||||
import colors from "../tailwindColors";
|
||||
|
||||
export default {
|
||||
export const branding: Branding = {
|
||||
enterprise: {
|
||||
background: colors.blue[100],
|
||||
divider: colors.blue[300],
|
||||
@@ -14,4 +14,20 @@ export default {
|
||||
border: colors.violet[600],
|
||||
text: colors.violet[950],
|
||||
},
|
||||
} satisfies Branding;
|
||||
|
||||
featureStage: {
|
||||
background: colors.sky[50],
|
||||
divider: colors.sky[100],
|
||||
border: colors.sky[700],
|
||||
text: colors.sky[700],
|
||||
|
||||
hover: {
|
||||
background: colors.white,
|
||||
divider: colors.zinc[100],
|
||||
border: colors.sky[700],
|
||||
text: colors.sky[700],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default branding;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { Roles } from "../roles";
|
||||
import colors from "../tailwindColors";
|
||||
|
||||
export default {
|
||||
const roles: Roles = {
|
||||
danger: {
|
||||
background: colors.orange[50],
|
||||
outline: colors.orange[400],
|
||||
@@ -152,4 +152,6 @@ export default {
|
||||
text: colors.white,
|
||||
},
|
||||
},
|
||||
} satisfies Roles;
|
||||
};
|
||||
|
||||
export default roles;
|
||||
|
||||
Reference in New Issue
Block a user