refactor: start using emotion for styling (#9909)

This commit is contained in:
Kayla Washburn
2023-09-29 13:08:17 -06:00
committed by GitHub
parent 4da1223a80
commit 2b5428e95f
29 changed files with 614 additions and 646 deletions
+5
View File
@@ -0,0 +1,5 @@
import type { Theme as MuiTheme } from "@mui/system";
declare module "@emotion/react" {
interface Theme extends MuiTheme {}
}
+18 -12
View File
@@ -8,7 +8,11 @@ import { ErrorBoundary } from "./components/ErrorBoundary/ErrorBoundary";
import { GlobalSnackbar } from "./components/GlobalSnackbar/GlobalSnackbar";
import { dark } from "./theme";
import "./theme/globalFonts";
import { StyledEngineProvider, ThemeProvider } from "@mui/material/styles";
import {
StyledEngineProvider,
ThemeProvider as MuiThemeProvider,
} from "@mui/material/styles";
import { ThemeProvider as EmotionThemeProvider } from "@emotion/react";
const queryClient = new QueryClient({
defaultOptions: {
@@ -25,17 +29,19 @@ export const AppProviders: FC<PropsWithChildren> = ({ children }) => {
return (
<HelmetProvider>
<StyledEngineProvider injectFirst>
<ThemeProvider theme={dark}>
<CssBaseline enableColorScheme />
<ErrorBoundary>
<QueryClientProvider client={queryClient}>
<AuthProvider>
{children}
<GlobalSnackbar />
</AuthProvider>
</QueryClientProvider>
</ErrorBoundary>
</ThemeProvider>
<MuiThemeProvider theme={dark}>
<EmotionThemeProvider theme={dark}>
<CssBaseline enableColorScheme />
<ErrorBoundary>
<QueryClientProvider client={queryClient}>
<AuthProvider>
{children}
<GlobalSnackbar />
</AuthProvider>
</QueryClientProvider>
</ErrorBoundary>
</EmotionThemeProvider>
</MuiThemeProvider>
</StyledEngineProvider>
</HelmetProvider>
);
+43 -46
View File
@@ -1,9 +1,8 @@
// This is the only place MuiAvatar can be used
// eslint-disable-next-line no-restricted-imports -- Read above
import MuiAvatar, { AvatarProps as MuiAvatarProps } from "@mui/material/Avatar";
import { makeStyles } from "@mui/styles";
import { FC } from "react";
import { combineClasses } from "utils/combineClasses";
import { css, type Theme } from "@emotion/react";
export type AvatarProps = MuiAvatarProps & {
size?: "sm" | "md" | "xl";
@@ -11,25 +10,49 @@ export type AvatarProps = MuiAvatarProps & {
fitImage?: boolean;
};
const sizeStyles = {
sm: (theme: Theme) => ({
width: theme.spacing(3),
height: theme.spacing(3),
fontSize: theme.spacing(1.5),
}),
md: {},
xl: (theme: Theme) => ({
width: theme.spacing(6),
height: theme.spacing(6),
fontSize: theme.spacing(3),
}),
};
const colorStyles = {
light: {},
darken: (theme: Theme) => ({
background: theme.palette.divider,
color: theme.palette.text.primary,
}),
};
const fitImageStyles = css`
& .MuiAvatar-img {
object-fit: contain;
}
`;
export const Avatar: FC<AvatarProps> = ({
size = "md",
colorScheme = "light",
fitImage,
className,
children,
...muiProps
}) => {
const styles = useStyles();
return (
<MuiAvatar
{...muiProps}
className={combineClasses([
className,
styles[size],
styles[colorScheme],
fitImage && styles.fitImage,
])}
css={[
sizeStyles[size],
colorStyles[colorScheme],
fitImage && fitImageStyles,
]}
>
{typeof children === "string" ? firstLetter(children) : children}
</MuiAvatar>
@@ -40,8 +63,15 @@ export const Avatar: FC<AvatarProps> = ({
* Use it to make an img element behaves like a MaterialUI Icon component
*/
export const AvatarIcon: FC<{ src: string }> = ({ src }) => {
const styles = useStyles();
return <img src={src} alt="" className={styles.avatarIcon} />;
return (
<img
src={src}
alt=""
css={{
maxWidth: "50%",
}}
/>
);
};
const firstLetter = (str: string): string => {
@@ -51,36 +81,3 @@ const firstLetter = (str: string): string => {
return "";
};
const useStyles = makeStyles((theme) => ({
// Size styles
sm: {
width: theme.spacing(3),
height: theme.spacing(3),
fontSize: theme.spacing(1.5),
},
// Just use the default value from theme
md: {},
xl: {
width: theme.spacing(6),
height: theme.spacing(6),
fontSize: theme.spacing(3),
},
// Colors
// Just use the default value from theme
light: {},
darken: {
background: theme.palette.divider,
color: theme.palette.text.primary,
},
// Avatar icon
avatarIcon: {
maxWidth: "50%",
},
// Fit image
fitImage: {
"& .MuiAvatar-img": {
objectFit: "contain",
},
},
}));
@@ -1,39 +1,22 @@
import { makeStyles } from "@mui/styles";
import Tooltip from "@mui/material/Tooltip";
import { useClickable } from "hooks/useClickable";
import { useClipboard } from "hooks/useClipboard";
import { FC, HTMLProps } from "react";
import { combineClasses } from "utils/combineClasses";
interface CopyableValueProps extends HTMLProps<HTMLDivElement> {
value: string;
}
export const CopyableValue: FC<CopyableValueProps> = ({
value,
className,
...props
}) => {
export const CopyableValue: FC<CopyableValueProps> = ({ value, ...props }) => {
const { isCopied, copy } = useClipboard(value);
const clickableProps = useClickable<HTMLSpanElement>(copy);
const styles = useStyles();
return (
<Tooltip
title={isCopied ? "Copied!" : "Click to copy"}
placement="bottom-start"
>
<span
{...props}
{...clickableProps}
className={combineClasses([styles.value, className])}
/>
<span {...props} {...clickableProps} css={{ cursor: "pointer" }} />
</Tooltip>
);
};
const useStyles = makeStyles(() => ({
value: {
cursor: "pointer",
},
}));
@@ -1,4 +1,3 @@
import { makeStyles } from "@mui/styles";
import { useMachine } from "@xstate/react";
import { DeploymentBanner } from "./DeploymentBanner/DeploymentBanner";
import { LicenseBanner } from "components/Dashboard/LicenseBanner/LicenseBanner";
@@ -19,7 +18,6 @@ import { docs } from "utils/docs";
import { HealthBanner } from "./HealthBanner";
export const DashboardLayout: FC = () => {
const styles = useStyles();
const permissions = usePermissions();
const [updateCheckState, updateCheckSend] = useMachine(updateCheckMachine, {
context: {
@@ -35,10 +33,23 @@ export const DashboardLayout: FC = () => {
<ServiceBanner />
{canViewDeployment && <LicenseBanner />}
<div className={styles.site}>
<div
css={{
display: "flex",
minHeight: "100%",
flexDirection: "column",
}}
>
<Navbar />
<div className={styles.siteContent}>
<div
css={{
flex: 1,
paddingBottom: dashboardContentBottomPadding, // Add bottom space since we don't use a footer
display: "flex",
flexDirection: "column",
}}
>
<Suspense fallback={<Loader />}>
<Outlet />
</Suspense>
@@ -118,17 +129,3 @@ export const DashboardFullPage = (props: BoxProps) => {
/>
);
};
const useStyles = makeStyles({
site: {
display: "flex",
minHeight: "100%",
flexDirection: "column",
},
siteContent: {
flex: 1,
paddingBottom: dashboardContentBottomPadding, // Add bottom space since we don't use a footer
display: "flex",
flexDirection: "column",
},
});
@@ -1,6 +1,5 @@
import Badge from "@mui/material/Badge";
import MenuItem from "@mui/material/MenuItem";
import { makeStyles } from "@mui/styles";
import { useState, FC, PropsWithChildren, MouseEvent } from "react";
import { colors } from "theme/colors";
import * as TypesGen from "api/typesGenerated";
@@ -13,6 +12,7 @@ import {
import { UserAvatar } from "components/UserAvatar/UserAvatar";
import { UserDropdownContent } from "./UserDropdownContent";
import { BUTTON_SM_HEIGHT } from "theme/theme";
import { css } from "@emotion/react";
export interface UserDropdownProps {
user: TypesGen.User;
@@ -27,7 +27,6 @@ export const UserDropdown: FC<PropsWithChildren<UserDropdownProps>> = ({
supportLinks,
onSignOut,
}: UserDropdownProps) => {
const styles = useStyles();
const [anchorEl, setAnchorEl] = useState<HTMLElement | undefined>();
const handleDropdownClick = (ev: MouseEvent<HTMLLIElement>): void => {
@@ -40,11 +39,25 @@ export const UserDropdown: FC<PropsWithChildren<UserDropdownProps>> = ({
return (
<>
<MenuItem
className={styles.menuItem}
css={(theme) => css`
height: ${navHeight}px;
padding: ${theme.spacing(1.5, 0)};
&:hover {
background-color: transparent;
}
`}
onClick={handleDropdownClick}
data-testid="user-dropdown-trigger"
>
<div className={styles.inner}>
<div
css={{
display: "flex",
alignItems: "center",
minWidth: 0,
maxWidth: 300,
}}
>
<Badge overlap="circular">
<UserAvatar
sx={{
@@ -90,24 +103,3 @@ export const UserDropdown: FC<PropsWithChildren<UserDropdownProps>> = ({
</>
);
};
export const useStyles = makeStyles((theme) => ({
divider: {
marginTop: theme.spacing(1),
marginBottom: theme.spacing(1),
},
inner: {
display: "flex",
alignItems: "center",
minWidth: 0,
maxWidth: 300,
},
menuItem: {
height: navHeight,
padding: theme.spacing(1.5, 0),
"&:hover": {
backgroundColor: "transparent",
},
},
}));
@@ -1,8 +1,9 @@
import { makeStyles } from "@mui/styles";
import { Pill } from "components/Pill/Pill";
import ReactMarkdown from "react-markdown";
import { colors } from "theme/colors";
import { hex } from "color-convert";
import { useTheme } from "@mui/system";
import { css } from "@emotion/react";
import { readableForegroundColor } from "utils/colors";
export interface ServiceBannerViewProps {
message: string;
@@ -15,7 +16,7 @@ export const ServiceBannerView: React.FC<ServiceBannerViewProps> = ({
backgroundColor,
isPreview,
}) => {
const styles = useStyles();
const theme = useTheme();
// We don't want anything funky like an image or a heading in the service
// banner.
const markdownElementsAllowed = [
@@ -31,15 +32,29 @@ export const ServiceBannerView: React.FC<ServiceBannerViewProps> = ({
];
return (
<div
className={styles.container}
style={{ backgroundColor: backgroundColor }}
css={css`
padding: ${theme.spacing(1.5)};
background-color: ${backgroundColor ?? theme.palette.warning.main};
display: flex;
align-items: center;
&.error {
background-color: ${colors.red[12]};
}
`}
>
{isPreview && <Pill text="Preview" type="info" lightBorder />}
<div
className={styles.centerContent}
style={{
color: readableForegroundColor(backgroundColor),
}}
css={css`
margin-right: auto;
margin-left: auto;
font-weight: 400;
color: ${readableForegroundColor(backgroundColor)};
& a {
color: inherit;
}
`}
>
<ReactMarkdown
allowedElements={markdownElementsAllowed}
@@ -52,36 +67,3 @@ export const ServiceBannerView: React.FC<ServiceBannerViewProps> = ({
</div>
);
};
const useStyles = makeStyles((theme) => ({
container: {
padding: theme.spacing(1.5),
backgroundColor: theme.palette.warning.main,
display: "flex",
alignItems: "center",
"&.error": {
backgroundColor: colors.red[12],
},
},
flex: {
display: "column",
},
centerContent: {
marginRight: "auto",
marginLeft: "auto",
fontWeight: 400,
"& a": {
color: "inherit",
},
},
}));
const readableForegroundColor = (backgroundColor: string): string => {
const rgb = hex.rgb(backgroundColor);
// Logic taken from here:
// https://github.com/casesandberg/react-color/blob/bc9a0e1dc5d11b06c511a8e02a95bd85c7129f4b/src/helpers/color.js#L56
// to be consistent with the color-picker label.
const yiq = (rgb[0] * 299 + rgb[1] * 587 + rgb[2] * 114) / 1000;
return yiq >= 128 ? "#000" : "#fff";
};
@@ -1,50 +1,91 @@
import { makeStyles } from "@mui/styles";
import { PropsWithChildren, FC } from "react";
import type { PropsWithChildren, FC } from "react";
import { MONOSPACE_FONT_FAMILY } from "theme/constants";
import { DisabledBadge, EnabledBadge } from "./Badges";
import Box, { BoxProps } from "@mui/material/Box";
import { combineClasses } from "utils/combineClasses";
import { useTheme } from "@mui/system";
import { DisabledBadge, EnabledBadge } from "./Badges";
import { css } from "@emotion/react";
export const OptionName: FC<PropsWithChildren> = ({ children }) => {
const styles = useStyles();
return <span className={styles.optionName}>{children}</span>;
export const OptionName: FC<PropsWithChildren> = (props) => {
const { children } = props;
return (
<span
css={{
display: "block",
}}
>
{children}
</span>
);
};
export const OptionDescription: FC<PropsWithChildren> = ({ children }) => {
const styles = useStyles();
return <span className={styles.optionDescription}>{children}</span>;
export const OptionDescription: FC<PropsWithChildren> = (props) => {
const { children } = props;
const theme = useTheme();
return (
<span
css={{
display: "block",
color: theme.palette.text.secondary,
fontSize: 14,
marginTop: theme.spacing(0.5),
}}
>
{children}
</span>
);
};
const NotSet: FC = () => {
const styles = useStyles();
interface OptionValueProps {
children?: boolean | number | string | string[];
}
return <span className={styles.optionValue}>Not set</span>;
};
export const OptionValue: FC<OptionValueProps> = (props) => {
const { children } = props;
const theme = useTheme();
export const OptionValue: FC<{ children?: unknown }> = ({ children }) => {
const styles = useStyles();
const optionStyles = css`
font-size: 14px;
font-family: ${MONOSPACE_FONT_FAMILY};
overflow-wrap: anywhere;
user-select: all;
& ul {
padding: ${theme.spacing(2)};
}
`;
if (typeof children === "boolean") {
return children ? <EnabledBadge /> : <DisabledBadge />;
}
if (typeof children === "number") {
return <span className={styles.optionValue}>{children}</span>;
return <span css={optionStyles}>{children}</span>;
}
if (!children || children.length === 0) {
return <span css={optionStyles}>Not set</span>;
}
if (typeof children === "string") {
return <span className={styles.optionValue}>{children}</span>;
return <span css={optionStyles}>{children}</span>;
}
if (Array.isArray(children)) {
if (children.length === 0) {
return <NotSet />;
}
return (
<ul className={styles.optionValueList}>
<ul
css={{
margin: 0,
padding: 0,
listStylePosition: "inside",
display: "flex",
flexDirection: "column",
gap: theme.spacing(0.5),
}}
>
{children.map((item) => (
<li key={item} className={styles.optionValue}>
<li key={item} css={optionStyles}>
{item}
</li>
))}
@@ -52,21 +93,24 @@ export const OptionValue: FC<{ children?: unknown }> = ({ children }) => {
);
}
if (children === "") {
return <NotSet />;
}
return <span className={styles.optionValue}>{JSON.stringify(children)}</span>;
return <span css={optionStyles}>{JSON.stringify(children)}</span>;
};
interface OptionConfigProps extends BoxProps {
source?: boolean;
}
// OptionalConfig takes a source bool to indicate if the Option is the source of the configured value.
export const OptionConfig = ({
source,
...boxProps
}: { source?: boolean } & BoxProps) => {
export const OptionConfig = (props: OptionConfigProps) => {
const { source, sx, ...attrs } = props;
const theme = useTheme();
const borderColor = source
? theme.palette.primary.main
: theme.palette.divider;
return (
<Box
{...boxProps}
{...attrs}
sx={{
fontSize: 13,
fontFamily: MONOSPACE_FONT_FAMILY,
@@ -79,68 +123,37 @@ export const OptionConfig = ({
alignItems: "center",
borderRadius: 0.25,
padding: (theme) => theme.spacing(0, 1),
border: (theme) =>
`1px solid ${
source ? theme.palette.primary.main : theme.palette.divider
}`,
"& .option-config-flag": {
backgroundColor: source ? "rgba(0, 0, 0, 0.7)" : undefined,
},
...boxProps.sx,
border: `1px solid ${borderColor}`,
...sx,
}}
/>
);
};
export const OptionConfigFlag = (props: BoxProps) => {
interface OptionConfigFlagProps extends BoxProps {
source?: boolean;
}
export const OptionConfigFlag = (props: OptionConfigFlagProps) => {
const { children, source, sx, ...attrs } = props;
return (
<Box
{...props}
className={combineClasses([props.className, "option-config-flag"])}
{...attrs}
sx={{
fontSize: 10,
fontWeight: 600,
margin: (theme) => theme.spacing(0, 0.75, 0, -0.5),
display: "block",
backgroundColor: (theme) => theme.palette.divider,
backgroundColor: (theme) =>
source ? "rgba(0, 0, 0, 0.7)" : theme.palette.divider,
lineHeight: 1,
padding: (theme) => theme.spacing(0.25, 0.5),
borderRadius: 0.25,
...props.sx,
...sx,
}}
/>
>
{children}
</Box>
);
};
const useStyles = makeStyles((theme) => ({
optionName: {
display: "block",
},
optionDescription: {
display: "block",
color: theme.palette.text.secondary,
fontSize: 14,
marginTop: theme.spacing(0.5),
},
optionValue: {
fontSize: 14,
fontFamily: MONOSPACE_FONT_FAMILY,
overflowWrap: "anywhere",
userSelect: "all",
"& ul": {
padding: theme.spacing(2),
},
},
optionValueList: {
margin: 0,
padding: 0,
listStylePosition: "inside",
display: "flex",
flexDirection: "column",
gap: theme.spacing(0.5),
},
}));
@@ -1,4 +1,3 @@
import { makeStyles } from "@mui/styles";
import Brush from "@mui/icons-material/Brush";
import LaunchOutlined from "@mui/icons-material/LaunchOutlined";
import ApprovalIcon from "@mui/icons-material/VerifiedUserOutlined";
@@ -9,24 +8,54 @@ import VpnKeyOutlined from "@mui/icons-material/VpnKeyOutlined";
import MonitorHeartOutlined from "@mui/icons-material/MonitorHeartOutlined";
import { GitIcon } from "components/Icons/GitIcon";
import { Stack } from "components/Stack/Stack";
import { ElementType, PropsWithChildren, ReactNode, FC } from "react";
import type { ElementType, FC, PropsWithChildren, ReactNode } from "react";
import { NavLink } from "react-router-dom";
import { combineClasses } from "utils/combineClasses";
import { useDashboard } from "components/Dashboard/DashboardProvider";
import { useTheme } from "@mui/system";
import { css } from "@emotion/css";
const SidebarNavItem: FC<
PropsWithChildren<{ href: string; icon: ReactNode }>
> = ({ children, href, icon }) => {
const styles = useStyles();
const theme = useTheme();
const activeStyles = css`
background-color: ${theme.palette.action.hover};
&::before {
content: "";
display: block;
width: 3px;
height: 100%;
position: absolute;
left: 0;
top: 0;
background-color: ${theme.palette.secondary.dark};
border-top-left-radius: ${theme.shape.borderRadius};
border-bottom-left-radius: ${theme.shape.borderRadius};
}
`;
return (
<NavLink
to={href}
className={({ isActive }) =>
combineClasses([
styles.sidebarNavItem,
isActive ? styles.sidebarNavItemActive : undefined,
])
}
className={({ isActive }) => css`
${isActive && activeStyles}
color: inherit;
display: block;
font-size: 14px;
text-decoration: none;
padding: ${theme.spacing(1.5, 1.5, 1.5, 2)};
border-radius: ${theme.shape.borderRadius / 2};
transition: background-color 0.15s ease-in-out;
margin-bottom: 1;
position: relative;
&:hover {
background-color: ${theme.palette.action.hover};
}
`}
>
<Stack alignItems="center" spacing={1.5} direction="row">
{icon}
@@ -37,16 +66,26 @@ const SidebarNavItem: FC<
};
const SidebarNavItemIcon: FC<{ icon: ElementType }> = ({ icon: Icon }) => {
const styles = useStyles();
return <Icon className={styles.sidebarNavItemIcon} />;
const theme = useTheme();
return (
<Icon
css={{
width: theme.spacing(2),
height: theme.spacing(2),
}}
/>
);
};
export const Sidebar: React.FC = () => {
const styles = useStyles();
const dashboard = useDashboard();
return (
<nav className={styles.sidebar}>
<nav
css={{
width: 245,
}}
>
<SidebarNavItem
href="general"
icon={<SidebarNavItemIcon icon={LaunchOutlined} />}
@@ -105,47 +144,3 @@ export const Sidebar: React.FC = () => {
</nav>
);
};
const useStyles = makeStyles((theme) => ({
sidebar: {
width: 245,
},
sidebarNavItem: {
color: "inherit",
display: "block",
fontSize: 14,
textDecoration: "none",
padding: theme.spacing(1.5, 1.5, 1.5, 2),
borderRadius: theme.shape.borderRadius / 2,
transition: "background-color 0.15s ease-in-out",
marginBottom: 1,
position: "relative",
"&:hover": {
backgroundColor: theme.palette.action.hover,
},
},
sidebarNavItemActive: {
backgroundColor: theme.palette.action.hover,
"&:before": {
content: '""',
display: "block",
width: 3,
height: "100%",
position: "absolute",
left: 0,
top: 0,
backgroundColor: theme.palette.secondary.dark,
borderTopLeftRadius: theme.shape.borderRadius,
borderBottomLeftRadius: theme.shape.borderRadius,
},
},
sidebarNavItemIcon: {
width: theme.spacing(2),
height: theme.spacing(2),
},
}));
+39 -38
View File
@@ -1,8 +1,7 @@
import Box from "@mui/material/Box";
import { makeStyles } from "@mui/styles";
import Typography from "@mui/material/Typography";
import { FC, ReactNode } from "react";
import { combineClasses } from "utils/combineClasses";
import type { FC, ReactNode } from "react";
import { useTheme } from "@emotion/react";
export interface EmptyStateProps {
/** Text Message to display, placed inside Typography component */
@@ -25,54 +24,56 @@ export interface EmptyStateProps {
export const EmptyState: FC<React.PropsWithChildren<EmptyStateProps>> = (
props,
) => {
const { message, description, cta, className, image, ...boxProps } = props;
const styles = useStyles();
const { message, description, cta, image, ...boxProps } = props;
const theme = useTheme();
return (
<Box className={combineClasses([styles.root, className])} {...boxProps}>
<Typography variant="h5" className={styles.title}>
<Box
css={{
overflow: "hidden",
display: "flex",
flexDirection: "column",
justifyContent: "center",
alignItems: "center",
textAlign: "center",
minHeight: 360,
padding: theme.spacing(10, 5),
position: "relative",
}}
{...boxProps}
>
<Typography
variant="h5"
css={{
fontSize: theme.spacing(3),
}}
>
{message}
</Typography>
{description && (
<Typography
variant="body2"
color="textSecondary"
className={styles.description}
css={{
marginTop: theme.spacing(1.5),
fontSize: theme.spacing(2),
lineHeight: "140%",
maxWidth: theme.spacing(60),
}}
>
{description}
</Typography>
)}
{cta && <div className={styles.cta}>{cta}</div>}
{cta && (
<div
css={{
marginTop: theme.spacing(4),
}}
>
{cta}
</div>
)}
{image}
</Box>
);
};
const useStyles = makeStyles((theme) => ({
root: {
overflow: "hidden",
display: "flex",
flexDirection: "column",
justifyContent: "center",
alignItems: "center",
textAlign: "center",
minHeight: 360,
padding: theme.spacing(10, 5),
position: "relative",
},
title: {
fontSize: theme.spacing(3),
},
description: {
marginTop: theme.spacing(1.5),
fontSize: theme.spacing(2),
lineHeight: "140%",
maxWidth: theme.spacing(60),
},
cta: {
marginTop: theme.spacing(4),
},
}));
+67 -69
View File
@@ -1,6 +1,4 @@
import { makeStyles } from "@mui/styles";
import { type FC, type PropsWithChildren, type ReactNode } from "react";
import { combineClasses } from "utils/combineClasses";
import { Stack } from "../Stack/Stack";
export interface PageHeaderProps {
@@ -13,16 +11,37 @@ export const PageHeader: FC<PropsWithChildren<PageHeaderProps>> = ({
actions,
className,
}) => {
const styles = useStyles({});
return (
<header
className={combineClasses([styles.root, className])}
className={className}
css={(theme) => ({
display: "flex",
alignItems: "center",
paddingTop: theme.spacing(6),
paddingBottom: theme.spacing(6),
gap: theme.spacing(4),
[theme.breakpoints.down("md")]: {
flexDirection: "column",
alignItems: "flex-start",
},
})}
data-testid="header"
>
<hgroup>{children}</hgroup>
{actions && (
<Stack direction="row" className={styles.actions}>
<Stack
direction="row"
css={(theme) => ({
marginLeft: "auto",
[theme.breakpoints.down("md")]: {
marginTop: theme.spacing(3),
marginLeft: "initial",
width: "100%",
},
})}
>
{actions}
</Stack>
)}
@@ -33,75 +52,54 @@ export const PageHeader: FC<PropsWithChildren<PageHeaderProps>> = ({
export const PageHeaderTitle: FC<PropsWithChildren<unknown>> = ({
children,
}) => {
const styles = useStyles({});
return <h1 className={styles.title}>{children}</h1>;
return (
<h1
css={(theme) => ({
fontSize: theme.spacing(3),
fontWeight: 400,
margin: 0,
display: "flex",
alignItems: "center",
lineHeight: "140%",
})}
>
{children}
</h1>
);
};
export const PageHeaderSubtitle: FC<
PropsWithChildren<{ condensed?: boolean }>
> = ({ children, condensed }) => {
const styles = useStyles({
condensed,
});
return <h2 className={styles.subtitle}>{children}</h2>;
return (
<h2
css={(theme) => ({
fontSize: theme.spacing(2),
color: theme.palette.text.secondary,
fontWeight: 400,
display: "block",
margin: 0,
marginTop: condensed ? theme.spacing(0.5) : theme.spacing(1),
lineHeight: "140%",
})}
>
{children}
</h2>
);
};
export const PageHeaderCaption: FC<PropsWithChildren> = ({ children }) => {
const styles = useStyles({});
return <span className={styles.caption}>{children}</span>;
return (
<span
css={(theme) => ({
fontSize: 12,
color: theme.palette.text.secondary,
fontWeight: 600,
textTransform: "uppercase",
letterSpacing: "0.1em",
})}
>
{children}
</span>
);
};
const useStyles = makeStyles((theme) => ({
root: {
display: "flex",
alignItems: "center",
paddingTop: theme.spacing(6),
paddingBottom: theme.spacing(6),
gap: theme.spacing(4),
[theme.breakpoints.down("md")]: {
flexDirection: "column",
alignItems: "flex-start",
},
},
title: {
fontSize: theme.spacing(3),
fontWeight: 400,
margin: 0,
display: "flex",
alignItems: "center",
lineHeight: "140%",
},
subtitle: {
fontSize: theme.spacing(2),
color: theme.palette.text.secondary,
fontWeight: 400,
display: "block",
margin: 0,
marginTop: ({ condensed }: { condensed?: boolean }) =>
condensed ? theme.spacing(0.5) : theme.spacing(1),
lineHeight: "140%",
},
actions: {
marginLeft: "auto",
[theme.breakpoints.down("md")]: {
marginTop: theme.spacing(3),
marginLeft: "initial",
width: "100%",
},
},
caption: {
fontSize: 12,
color: theme.palette.text.secondary,
fontWeight: 600,
textTransform: "uppercase",
letterSpacing: "0.1em",
},
}));
@@ -1,7 +1,6 @@
import { FC, PropsWithChildren, useState } from "react";
import { Section } from "components/SettingsLayout/Section";
import { TokensPageView } from "./TokensPageView";
import makeStyles from "@mui/styles/makeStyles";
import { useTokensData } from "./hooks";
import { ConfirmDeleteDialog } from "./ConfirmDeleteDialog";
import { Stack } from "components/Stack/Stack";
@@ -9,14 +8,19 @@ import Button from "@mui/material/Button";
import { Link as RouterLink } from "react-router-dom";
import AddIcon from "@mui/icons-material/AddOutlined";
import { APIKeyWithOwner } from "api/typesGenerated";
import { css } from "@emotion/react";
export const TokensPage: FC<PropsWithChildren<unknown>> = () => {
const styles = useStyles();
const cliCreateCommand = "coder tokens create";
const TokenActions = () => (
<Stack direction="row" justifyContent="end" className={styles.tokenActions}>
<Stack
direction="row"
justifyContent="end"
css={(theme) => ({
marginBottom: theme.spacing(1),
})}
>
<Button startIcon={<AddIcon />} component={RouterLink} to="new">
Add token
</Button>
@@ -43,7 +47,15 @@ export const TokensPage: FC<PropsWithChildren<unknown>> = () => {
<>
<Section
title="Tokens"
className={styles.section}
css={(theme) => css`
& code {
background: ${theme.palette.divider};
font-size: 12px;
padding: 2px 4px;
color: ${theme.palette.text.primary};
border-radius: 2px;
}
`}
description={
<>
Tokens are used to authenticate with the Coder API. You can create a
@@ -73,19 +85,4 @@ export const TokensPage: FC<PropsWithChildren<unknown>> = () => {
);
};
const useStyles = makeStyles((theme) => ({
section: {
"& code": {
background: theme.palette.divider,
fontSize: 12,
padding: "2px 4px",
color: theme.palette.text.primary,
borderRadius: 2,
},
},
tokenActions: {
marginBottom: theme.spacing(1),
},
}));
export default TokensPage;
@@ -9,7 +9,6 @@ import {
WorkspaceParametersForm,
} from "./WorkspaceParametersForm";
import { useNavigate } from "react-router-dom";
import { makeStyles } from "@mui/styles";
import { PageHeader, PageHeaderTitle } from "components/PageHeader/PageHeader";
import { FC } from "react";
import { isApiValidationError } from "api/errors";
@@ -74,11 +73,13 @@ export type WorkspaceParametersPageViewProps = {
export const WorkspaceParametersPageView: FC<
WorkspaceParametersPageViewProps
> = ({ data, submitError, isSubmitting, onSubmit, onCancel }) => {
const styles = useStyles();
return (
<>
<PageHeader className={styles.pageHeader}>
<PageHeader
css={{
paddingTop: 0,
}}
>
<PageHeaderTitle>Workspace parameters</PageHeaderTitle>
</PageHeader>
@@ -102,10 +103,4 @@ export const WorkspaceParametersPageView: FC<
);
};
const useStyles = makeStyles(() => ({
pageHeader: {
paddingTop: 0,
},
}));
export default WorkspaceParametersPage;
@@ -1,4 +1,3 @@
import { makeStyles } from "@mui/styles";
import { useMachine } from "@xstate/react";
import { Alert } from "components/Alert/Alert";
import { ConfirmDialog } from "components/Dialogs/ConfirmDialog/ConfirmDialog";
@@ -31,17 +30,7 @@ const getAutostart = (workspace: TypesGen.Workspace) =>
const getAutostop = (workspace: TypesGen.Workspace) =>
ttlMsToAutostop(workspace.ttl_ms);
const useStyles = makeStyles((theme) => ({
topMargin: {
marginTop: theme.spacing(3),
},
pageHeader: {
paddingTop: 0,
},
}));
export const WorkspaceSchedulePage: FC = () => {
const styles = useStyles();
const params = useParams() as { username: string; workspace: string };
const navigate = useNavigate();
const username = params.username.replace("@", "");
@@ -72,7 +61,11 @@ export const WorkspaceSchedulePage: FC = () => {
<Helmet>
<title>{pageTitle([workspaceName, "Schedule"])}</title>
</Helmet>
<PageHeader className={styles.pageHeader}>
<PageHeader
css={{
paddingTop: 0,
}}
>
<PageHeaderTitle>Workspace Schedule</PageHeaderTitle>
</PageHeader>
{(scheduleState.hasTag("loading") || !template) && <Loader />}
@@ -1,4 +1,3 @@
import { makeStyles } from "@mui/styles";
import { PageHeader, PageHeaderTitle } from "components/PageHeader/PageHeader";
import { ComponentProps, FC } from "react";
import { WorkspaceSettingsForm } from "./WorkspaceSettingsForm";
@@ -19,11 +18,13 @@ export const WorkspaceSettingsPageView: FC<WorkspaceSettingsPageViewProps> = ({
error,
workspace,
}) => {
const styles = useStyles();
return (
<>
<PageHeader className={styles.pageHeader}>
<PageHeader
css={{
paddingTop: 0,
}}
>
<PageHeaderTitle>Workspace Settings</PageHeaderTitle>
</PageHeader>
@@ -37,9 +38,3 @@ export const WorkspaceSettingsPageView: FC<WorkspaceSettingsPageViewProps> = ({
</>
);
};
const useStyles = makeStyles(() => ({
pageHeader: {
paddingTop: 0,
},
}));
@@ -15,7 +15,6 @@ import AddOutlined from "@mui/icons-material/AddOutlined";
import Button from "@mui/material/Button";
import { ChooseOne, Cond } from "components/Conditionals/ChooseOne";
import { Link as RouterLink, useNavigate } from "react-router-dom";
import { makeStyles } from "@mui/styles";
import { useClickableTableRow } from "hooks/useClickableTableRow";
import KeyboardArrowRight from "@mui/icons-material/KeyboardArrowRight";
import Box from "@mui/material/Box";
@@ -30,6 +29,8 @@ import Checkbox from "@mui/material/Checkbox";
import { AvatarDataSkeleton } from "components/AvatarData/AvatarDataSkeleton";
import Skeleton from "@mui/material/Skeleton";
import { InfoTooltip } from "components/InfoTooltip/InfoTooltip";
import { css } from "@emotion/react";
import { useTheme } from "@mui/system";
export interface WorkspacesTableProps {
workspaces?: Workspace[];
@@ -49,7 +50,7 @@ export const WorkspacesTable: FC<WorkspacesTableProps> = ({
onCheckChange,
canCheckWorkspaces,
}) => {
const styles = useStyles();
const theme = useTheme();
return (
<TableContainer>
@@ -100,7 +101,9 @@ export const WorkspacesTable: FC<WorkspacesTableProps> = ({
<Cond>
<TableEmpty
className={styles.withImage}
css={{
paddingBottom: 0,
}}
message="Create a workspace"
description="A workspace is your personal, customizable development environment in the cloud"
cta={
@@ -114,7 +117,19 @@ export const WorkspacesTable: FC<WorkspacesTableProps> = ({
</Button>
}
image={
<div className={styles.emptyImage}>
<div
css={css`
max-width: 50%;
height: ${theme.spacing(34)};
overflow: hidden;
margin-top: ${theme.spacing(6)};
opacity: 0.85;
& img {
max-width: 100%;
}
`}
>
<img src="/featured/workspaces.webp" alt="" />
</div>
}
@@ -321,20 +336,3 @@ const TableLoader = ({
const cantBeChecked = (workspace: Workspace) => {
return ["deleting", "pending"].includes(workspace.latest_build.status);
};
const useStyles = makeStyles((theme) => ({
withImage: {
paddingBottom: 0,
},
emptyImage: {
maxWidth: "50%",
height: theme.spacing(34),
overflow: "hidden",
marginTop: theme.spacing(6),
opacity: 0.85,
"& img": {
maxWidth: "100%",
},
},
}));
+12
View File
@@ -1,3 +1,5 @@
import { hex } from "color-convert";
/**
* Does not support shorthand hex strings (e.g., #fff), just to maximize
* compatibility with server, which also doesn't support shorthand
@@ -83,3 +85,13 @@ export function hslToHex(hsl: string): string {
};
return `#${f(0)}${f(8)}${f(4)}`;
}
export const readableForegroundColor = (backgroundColor: string): string => {
const rgb = hex.rgb(backgroundColor);
// Logic taken from here:
// https://github.com/casesandberg/react-color/blob/bc9a0e1dc5d11b06c511a8e02a95bd85c7129f4b/src/helpers/color.js#L56
// to be consistent with the color-picker label.
const yiq = (rgb[0] * 299 + rgb[1] * 587 + rgb[2] * 114) / 1000;
return yiq >= 128 ? "#000" : "#fff";
};