mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
refactor: start using emotion for styling (#9909)
This commit is contained in:
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
import type { Theme as MuiTheme } from "@mui/system";
|
||||
|
||||
declare module "@emotion/react" {
|
||||
interface Theme extends MuiTheme {}
|
||||
}
|
||||
+18
-12
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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),
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -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),
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -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;
|
||||
|
||||
+5
-10
@@ -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;
|
||||
|
||||
+5
-12
@@ -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%",
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -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";
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user