feat: add light theme (#11266)

This commit is contained in:
Kayla Washburn
2023-12-19 17:03:00 -07:00
committed by GitHub
parent e0d34ca6f7
commit 97f7a35a47
101 changed files with 1951 additions and 972 deletions
+1
View File
@@ -351,6 +351,7 @@ jobs:
- name: Install/Upgrade Helm chart
run: |
set -euo pipefail
helm dependency update --skip-refresh ./helm/coder
helm upgrade --install "pr${{ env.PR_NUMBER }}" ./helm/coder \
--namespace "pr${{ env.PR_NUMBER }}" \
--values ./pr-deploy-values.yaml \
+7 -1
View File
@@ -3,10 +3,16 @@ import turbosnap from "vite-plugin-turbosnap";
module.exports = {
stories: ["../src/**/*.stories.tsx"],
addons: [
{
name: "@storybook/addon-essentials",
options: {
backgrounds: false,
},
},
"@storybook/addon-links",
"@storybook/addon-essentials",
"@storybook/addon-mdx-gfm",
"@storybook/addon-actions",
"@storybook/addon-themes",
],
staticDirs: ["../static"],
framework: {
+41 -26
View File
@@ -4,24 +4,33 @@ import {
ThemeProvider as MuiThemeProvider,
} from "@mui/material/styles";
import { ThemeProvider as EmotionThemeProvider } from "@emotion/react";
import { DecoratorHelpers } from "@storybook/addon-themes";
import { withRouter } from "storybook-addon-react-router-v6";
import { HelmetProvider } from "react-helmet-async";
import theme from "theme";
import colors from "theme/tailwind";
import "theme/globalFonts";
import { QueryClient, QueryClientProvider } from "react-query";
import { HelmetProvider } from "react-helmet-async";
import themes from "theme";
import "theme/globalFonts";
DecoratorHelpers.initializeThemeState(Object.keys(themes), "dark");
export const decorators = [
(Story) => (
<StyledEngineProvider injectFirst>
<MuiThemeProvider theme={theme.dark}>
<EmotionThemeProvider theme={theme.dark}>
<CssBaseline />
<Story />
</EmotionThemeProvider>
</MuiThemeProvider>
</StyledEngineProvider>
),
(Story, context) => {
const selectedTheme = DecoratorHelpers.pluckThemeFromContext(context);
const { themeOverride } = DecoratorHelpers.useThemeParameters();
const selected = themeOverride || selectedTheme || "dark";
return (
<StyledEngineProvider injectFirst>
<MuiThemeProvider theme={themes[selected]}>
<EmotionThemeProvider theme={themes[selected]}>
<CssBaseline />
<Story />
</EmotionThemeProvider>
</MuiThemeProvider>
</StyledEngineProvider>
);
},
withRouter,
(Story) => {
return (
@@ -50,18 +59,12 @@ export const decorators = [
];
export const parameters = {
backgrounds: {
default: "dark",
values: [
{
name: "dark",
value: colors.gray[950],
},
{
name: "light",
value: colors.gray[50],
},
],
options: {
storySort: {
method: "alphabetical",
order: ["design", "pages", "components"],
locales: "en-US",
},
},
actions: {
argTypesRegex: "^(on|handler)[A-Z].*",
@@ -73,4 +76,16 @@ export const parameters = {
date: /Date$/,
},
},
viewport: {
viewports: {
ipad: {
name: "iPad Mini",
styles: {
height: "1024px",
width: "768px",
},
type: "tablet",
},
},
},
};
+1
View File
@@ -104,6 +104,7 @@
"@storybook/addon-essentials": "7.5.2",
"@storybook/addon-links": "7.5.2",
"@storybook/addon-mdx-gfm": "7.5.2",
"@storybook/addon-themes": "7.6.4",
"@storybook/react": "7.5.2",
"@storybook/react-vite": "7.5.2",
"@swc/core": "1.3.38",
+9
View File
@@ -233,6 +233,9 @@ devDependencies:
'@storybook/addon-mdx-gfm':
specifier: 7.5.2
version: 7.5.2
'@storybook/addon-themes':
specifier: 7.6.4
version: 7.6.4
'@storybook/react':
specifier: 7.5.2
version: 7.5.2(react-dom@18.2.0)(react@18.2.0)(typescript@5.2.2)
@@ -4087,6 +4090,12 @@ packages:
- '@types/react-dom'
dev: true
/@storybook/addon-themes@7.6.4:
resolution: {integrity: sha512-jz6/6LSRVgL9G5vknxROGOakXAsTIIl2sR7tkuC4gyGkCIGwvb9oO9jUaHkUHQ8rtcLXbVpFncQzUXmrwK6CGg==}
dependencies:
ts-dedent: 2.2.0
dev: true
/@storybook/addon-toolbars@7.5.2(@types/react-dom@18.2.4)(@types/react@18.2.6)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-BXzb5NOpILFOM7EOBxcF2Qj/q6BicWZ1AvAddORWGmqSa/MxMIa4X52oKXFUTHKBkrTO1X0XqHmoF88qm3TUFg==}
peerDependencies:
-5
View File
@@ -1,11 +1,6 @@
import type { PaletteColor, PaletteColorOptions } from "@mui/material/styles";
import type { NewTheme } from "theme/experimental";
declare module "@mui/material/styles" {
interface Theme {
experimental: NewTheme;
}
interface Palette {
neutral: PaletteColor;
}
+3 -3
View File
@@ -2,7 +2,7 @@ import { QueryClient, QueryClientProvider } from "react-query";
import type { FC, ReactNode } from "react";
import { HelmetProvider } from "react-helmet-async";
import { AppRouter } from "./AppRouter";
import { ThemeProviders } from "./contexts/ThemeProviders";
import { ThemeProvider } from "./contexts/ThemeProvider";
import { AuthProvider } from "./contexts/AuthProvider/AuthProvider";
import { ErrorBoundary } from "./components/ErrorBoundary/ErrorBoundary";
import { GlobalSnackbar } from "./components/GlobalSnackbar/GlobalSnackbar";
@@ -30,10 +30,10 @@ export const AppProviders: FC<AppProvidersProps> = ({
<HelmetProvider>
<QueryClientProvider client={queryClient}>
<AuthProvider>
<ThemeProviders>
<ThemeProvider>
{children}
<GlobalSnackbar />
</ThemeProviders>
</ThemeProvider>
</AuthProvider>
</QueryClientProvider>
</HelmetProvider>
@@ -96,6 +96,7 @@ export const ActiveUserChart: FC<ActiveUserChartProps> = ({
},
scales: {
y: {
grid: { color: theme.palette.divider },
suggestedMin: 0,
ticks: {
precision: 0,
@@ -103,6 +104,7 @@ export const ActiveUserChart: FC<ActiveUserChartProps> = ({
},
x: {
grid: { color: theme.palette.divider },
ticks: {
stepSize: data.length > 10 ? 2 : undefined,
},
@@ -124,11 +126,9 @@ export const ActiveUserChart: FC<ActiveUserChartProps> = ({
{
label: `${interval === "day" ? "Daily" : "Weekly"} Active Users`,
data: chartData,
pointBackgroundColor: theme.palette.info.light,
pointBorderColor: theme.palette.info.light,
borderColor: theme.palette.info.light,
backgroundColor: theme.palette.info.dark,
fill: "origin",
pointBackgroundColor: theme.experimental.roles.active.outline,
pointBorderColor: theme.experimental.roles.active.outline,
borderColor: theme.experimental.roles.active.outline,
},
],
}}
@@ -2,9 +2,15 @@ import type { Meta, StoryObj } from "@storybook/react";
import {
Badges,
AlphaBadge,
BetaBadge,
DisabledBadge,
EnabledBadge,
EntitledBadge,
EnterpriseBadge,
HealthyBadge,
NotHealthyBadge,
NotRegisteredBadge,
NotReachableBadge,
} from "./Badges";
const meta: Meta<typeof Badges> = {
@@ -26,11 +32,34 @@ export const Entitled: Story = {
children: <EntitledBadge />,
},
};
export const ProxyStatus: Story = {
args: {
children: (
<>
<HealthyBadge />
<HealthyBadge derpOnly />
<NotHealthyBadge />
<NotRegisteredBadge />
<NotReachableBadge />
</>
),
},
};
export const Disabled: Story = {
args: {
children: <DisabledBadge />,
},
};
export const Enterprise: Story = {
args: {
children: <EnterpriseBadge />,
},
};
export const Beta: Story = {
args: {
children: <BetaBadge />,
},
};
export const Alpha: Story = {
args: {
children: <AlphaBadge />,
+38 -18
View File
@@ -2,7 +2,6 @@ import type { PropsWithChildren, FC } from "react";
import Tooltip from "@mui/material/Tooltip";
import { type Interpolation, type Theme } from "@emotion/react";
import { Stack } from "components/Stack/Stack";
import colors from "theme/tailwind";
const styles = {
badge: {
@@ -22,14 +21,17 @@ const styles = {
enabledBadge: (theme) => ({
border: `1px solid ${theme.experimental.roles.success.outline}`,
backgroundColor: theme.experimental.roles.success.background,
color: theme.experimental.roles.success.text,
}),
errorBadge: (theme) => ({
border: `1px solid ${theme.experimental.roles.error.outline}`,
backgroundColor: theme.experimental.roles.error.background,
color: theme.experimental.roles.error.text,
}),
warnBadge: (theme) => ({
border: `1px solid ${theme.experimental.roles.warning.outline}`,
backgroundColor: theme.experimental.roles.warning.background,
color: theme.experimental.roles.warning.text,
}),
} satisfies Record<string, Interpolation<Theme>>;
@@ -42,10 +44,9 @@ export const EntitledBadge: FC = () => {
};
interface HealthyBadge {
derpOnly: boolean;
derpOnly?: boolean;
}
export const HealthyBadge: FC<HealthyBadge> = (props) => {
const { derpOnly } = props;
export const HealthyBadge: FC<HealthyBadge> = ({ derpOnly }) => {
return (
<span css={[styles.badge, styles.enabledBadge]}>
{derpOnly ? "Healthy (DERP only)" : "Healthy"}
@@ -79,8 +80,9 @@ export const DisabledBadge: FC = () => {
css={[
styles.badge,
(theme) => ({
border: `1px solid ${theme.palette.divider}`,
backgroundColor: theme.palette.background.paper,
border: `1px solid ${theme.experimental.l1.outline}`,
backgroundColor: theme.experimental.l1.background,
color: theme.experimental.l1.text,
}),
]}
>
@@ -95,8 +97,9 @@ export const EnterpriseBadge: FC = () => {
css={[
styles.badge,
(theme) => ({
backgroundColor: theme.palette.info.dark,
border: `1px solid ${theme.palette.info.light}`,
backgroundColor: theme.experimental.roles.info.background,
border: `1px solid ${theme.experimental.roles.info.outline}`,
color: theme.experimental.roles.info.text,
}),
]}
>
@@ -105,16 +108,33 @@ export const EnterpriseBadge: FC = () => {
);
};
export const BetaBadge: FC = () => {
return (
<span
css={[
styles.badge,
(theme) => ({
border: `1px solid ${theme.experimental.roles.preview.outline}`,
backgroundColor: theme.experimental.roles.preview.background,
color: theme.experimental.roles.preview.text,
}),
]}
>
Beta
</span>
);
};
export const AlphaBadge: FC = () => {
return (
<span
css={[
styles.badge,
{
border: `1px solid ${colors.violet[600]}`,
backgroundColor: colors.violet[950],
color: colors.violet[50],
},
(theme) => ({
border: `1px solid ${theme.experimental.roles.preview.outline}`,
backgroundColor: theme.experimental.roles.preview.background,
color: theme.experimental.roles.preview.text,
}),
]}
>
Alpha
@@ -127,11 +147,11 @@ export const DeprecatedBadge: FC = () => {
<span
css={[
styles.badge,
{
border: `1px solid ${colors.orange[600]}`,
backgroundColor: colors.orange[950],
color: colors.orange[50],
},
(theme) => ({
border: `1px solid ${theme.experimental.roles.danger.outline}`,
backgroundColor: theme.experimental.roles.danger.background,
color: theme.experimental.roles.danger.text,
}),
]}
>
Deprecated
@@ -1,22 +1,22 @@
import type { Meta, StoryObj } from "@storybook/react";
import { CodeExample } from "./CodeExample";
const sampleCode = `echo "Hello, world"`;
const meta: Meta<typeof CodeExample> = {
title: "components/CodeExample",
component: CodeExample,
argTypes: {
code: { control: "string", defaultValue: sampleCode },
args: {
code: `echo "hello, friend!"`,
},
};
export default meta;
type Story = StoryObj<typeof CodeExample>;
export const Example: Story = {
export const Example: Story = {};
export const Secret: Story = {
args: {
code: sampleCode,
secret: true,
},
};
+34 -34
View File
@@ -1,51 +1,51 @@
import { type FC } from "react";
import { useTheme } from "@emotion/react";
import { type Interpolation, type Theme } from "@emotion/react";
import { MONOSPACE_FONT_FAMILY } from "theme/constants";
import { CopyButton } from "../CopyButton/CopyButton";
export interface CodeExampleProps {
code: string;
password?: boolean;
secret?: boolean;
className?: string;
}
/**
* Component to show single-line code examples, with a copy button
*/
export const CodeExample: FC<CodeExampleProps> = (props) => {
const { code, password, className } = props;
const theme = useTheme();
export const CodeExample: FC<CodeExampleProps> = ({
code,
secret,
className,
}) => {
return (
<div
css={{
display: "flex",
flexDirection: "row",
alignItems: "center",
background: "rgb(0 0 0 / 30%)",
color: theme.palette.primary.contrastText,
fontFamily: MONOSPACE_FONT_FAMILY,
fontSize: 14,
borderRadius: 8,
padding: 8,
lineHeight: "150%",
border: `1px solid ${theme.palette.divider}`,
}}
className={className}
>
<code
css={{
padding: "0 8px",
width: "100%",
display: "flex",
alignItems: "center",
wordBreak: "break-all",
"-webkit-text-security": password ? "disc" : undefined,
}}
>
{code}
</code>
<div css={styles.container} className={className}>
<code css={[styles.code, secret && styles.secret]}>{code}</code>
<CopyButton text={code} />
</div>
);
};
const styles = {
container: (theme) => ({
display: "flex",
flexDirection: "row",
alignItems: "center",
color: theme.experimental.l1.text,
fontFamily: MONOSPACE_FONT_FAMILY,
fontSize: 14,
borderRadius: 8,
padding: 8,
lineHeight: "150%",
border: `1px solid ${theme.experimental.l1.outline}`,
}),
code: {
padding: "0 8px",
flexGrow: 1,
wordBreak: "break-all",
},
secret: {
"-webkit-text-security": "disc", // also supported by firefox
},
} satisfies Record<string, Interpolation<Theme>>;
@@ -378,7 +378,7 @@ const HealthIssue: FC<PropsWithChildren> = ({ children }) => {
<Stack direction="row" spacing={1} alignItems="center">
<ErrorIcon
css={{ width: 16, height: 16 }}
htmlColor={theme.colors.red[10]}
htmlColor={theme.experimental.roles.error.outline}
/>
{children}
</Stack>
@@ -426,13 +426,13 @@ const classNames = {
} satisfies Record<string, ClassName>;
const styles = {
statusBadge: css`
statusBadge: (theme) => css`
display: flex;
align-items: center;
justify-content: center;
padding: 0 12px;
height: 100%;
color: #fff;
color: ${theme.experimental.l1.text};
& svg {
width: 16px;
@@ -1,8 +1,10 @@
import type { Meta, StoryObj } from "@storybook/react";
import { chromatic } from "testHelpers/chromatic";
import { LicenseBannerView } from "./LicenseBannerView";
const meta: Meta<typeof LicenseBannerView> = {
title: "components/LicenseBannerView",
parameters: { chromatic },
component: LicenseBannerView,
};
@@ -47,11 +47,11 @@ export const LicenseBannerView: FC<LicenseBannerViewProps> = ({
display: flex;
align-items: center;
padding: 12px;
background-color: ${type === "error"
? theme.colors.red[10]
: theme.colors.orange[10]};
background-color: ${theme.experimental.roles[type].background};
`;
const textColor = theme.experimental.roles[type].text;
if (messages.length === 1) {
return (
<div css={containerStyles}>
@@ -59,7 +59,11 @@ export const LicenseBannerView: FC<LicenseBannerViewProps> = ({
<div css={styles.leftContent}>
<span>{messages[0]}</span>
&nbsp;
<Link color="white" fontWeight="medium" href="mailto:sales@coder.com">
<Link
color={textColor}
fontWeight="medium"
href="mailto:sales@coder.com"
>
{Language.upgrade}
</Link>
</div>
@@ -74,7 +78,11 @@ export const LicenseBannerView: FC<LicenseBannerViewProps> = ({
<div>
{Language.exceeded}
&nbsp;
<Link color="white" fontWeight="medium" href="mailto:sales@coder.com">
<Link
color={textColor}
fontWeight="medium"
href="mailto:sales@coder.com"
>
{Language.upgrade}
</Link>
</div>
@@ -1,9 +1,11 @@
import type { Meta, StoryObj } from "@storybook/react";
import { chromaticWithTablet } from "testHelpers/chromatic";
import { MockUser, MockUser2 } from "testHelpers/entities";
import { NavbarView } from "./NavbarView";
const meta: Meta<typeof NavbarView> = {
title: "components/NavbarView",
parameters: { chromatic: chromaticWithTablet, layout: "fullscreen" },
component: NavbarView,
args: {
user: MockUser,
@@ -23,12 +25,3 @@ export const ForMember: Story = {
canViewAllUsers: false,
},
};
export const SmallViewport: Story = {
parameters: {
viewport: {
defaultViewport: "tablet",
},
chromatic: { viewports: [420] },
},
};
@@ -456,7 +456,7 @@ const styles = {
},
link: (theme) => css`
align-items: center;
color: ${theme.colors.gray[6]};
color: ${theme.palette.text.secondary};
display: flex;
flex: 1;
font-size: 16px;
@@ -470,7 +470,7 @@ const styles = {
}
&:hover {
background-color: ${theme.palette.action.hover};
background-color: ${theme.experimental.l2.hover.background};
}
${theme.breakpoints.up("md")} {
@@ -45,7 +45,7 @@ export const UserDropdown: FC<UserDropdownProps> = ({
/>
</Badge>
<DropdownArrow
color={theme.colors.gray[6]}
color={theme.experimental.l2.fill}
close={popover.isOpen}
/>
</div>
@@ -1,6 +1,7 @@
import { css, type Interpolation, type Theme } from "@emotion/react";
import { type FC } from "react";
import { InlineMarkdown } from "components/Markdown/Markdown";
import { Pill } from "components/Pill/Pill";
import ReactMarkdown from "react-markdown";
import { css, useTheme } from "@emotion/react";
import { readableForegroundColor } from "utils/colors";
export interface ServiceBannerViewProps {
@@ -9,58 +10,39 @@ export interface ServiceBannerViewProps {
isPreview: boolean;
}
export const ServiceBannerView: React.FC<ServiceBannerViewProps> = ({
export const ServiceBannerView: FC<ServiceBannerViewProps> = ({
message,
backgroundColor,
isPreview,
}) => {
const theme = useTheme();
// We don't want anything funky like an image or a heading in the service
// banner.
const markdownElementsAllowed = [
"text",
"a",
"pre",
"ul",
"strong",
"emphasis",
"italic",
"link",
"em",
];
return (
<div
css={css`
padding: 12px;
background-color: ${backgroundColor ?? theme.palette.warning.main};
display: flex;
align-items: center;
&.error {
background-color: ${theme.colors.red[12]};
}
`}
>
<div css={[styles.banner, { backgroundColor }]}>
{isPreview && <Pill text="Preview" type="info" />}
<div
css={css`
margin-right: auto;
margin-left: auto;
font-weight: 400;
color: ${readableForegroundColor(backgroundColor)};
& a {
color: inherit;
}
`}
css={[
styles.wrapper,
{ color: readableForegroundColor(backgroundColor) },
]}
>
<ReactMarkdown
allowedElements={markdownElementsAllowed}
unwrapDisallowed
>
{message}
</ReactMarkdown>
<InlineMarkdown>{message}</InlineMarkdown>
</div>
</div>
);
};
const styles = {
banner: css`
padding: 12px;
display: flex;
align-items: center;
`,
wrapper: css`
margin-right: auto;
margin-left: auto;
font-weight: 400;
& a {
color: inherit;
}
`,
} satisfies Record<string, Interpolation<Theme>>;
+10 -10
View File
@@ -58,7 +58,7 @@ export const DialogActionButtons: React.FC<DialogActionButtonsProps> = ({
disabled={disabled}
type="submit"
css={[
type === "delete" && styles.warningButton,
type === "delete" && styles.dangerButton,
type === "success" && styles.successButton,
]}
>
@@ -70,26 +70,26 @@ export const DialogActionButtons: React.FC<DialogActionButtonsProps> = ({
};
const styles = {
warningButton: (theme) => ({
dangerButton: (theme) => ({
"&.MuiButton-contained": {
backgroundColor: theme.palette.warning.main,
borderColor: theme.palette.warning.main,
backgroundColor: theme.experimental.roles.danger.fill,
borderColor: theme.experimental.roles.danger.outline,
"&:not(.MuiLoadingButton-loading)": {
color: theme.palette.text.primary,
color: theme.experimental.roles.danger.text,
},
"&:hover:not(:disabled)": {
backgroundColor: theme.palette.warning.main,
borderColor: theme.palette.warning.main,
backgroundColor: theme.experimental.roles.danger.disabled.fill,
borderColor: theme.experimental.roles.danger.disabled.outline,
},
"&.Mui-disabled": {
backgroundColor: theme.palette.warning.dark,
borderColor: theme.palette.warning.dark,
backgroundColor: theme.experimental.roles.danger.disabled.background,
borderColor: theme.experimental.roles.danger.disabled.outline,
"&:not(.MuiLoadingButton-loading)": {
color: theme.palette.warning.main,
color: theme.experimental.roles.danger.disabled.text,
},
},
},
@@ -0,0 +1,36 @@
import { type Interpolation, type Theme } from "@emotion/react";
import { type FC, type ImgHTMLAttributes } from "react";
interface ExternalIconProps extends ImgHTMLAttributes<HTMLImageElement> {
size?: number;
}
export const ExternalIcon: FC<ExternalIconProps> = ({
size = 36,
...attrs
}) => {
return (
<div css={[styles.container, { height: size, width: size }]}>
<img
alt=""
aria-hidden
css={[
styles.icon,
{ height: size, width: size, padding: Math.ceil(size / 6) },
]}
{...attrs}
/>
</div>
);
};
const styles = {
container: {
borderRadius: 9999,
overflow: "clip",
},
icon: {
backgroundColor: "#000",
objectFit: "contain",
},
} satisfies Record<string, Interpolation<Theme>>;
@@ -206,6 +206,7 @@ const styles = {
display: "flex",
alignItems: "center",
...(theme.typography.body2 as CSSObject),
color: theme.experimental.roles.active.fill,
}),
linkIcon: {
+1
View File
@@ -16,6 +16,7 @@ export const Loader: FC<LoaderProps> = ({ size = 26, ...attrs }) => {
justifyContent: "center",
}}
data-testid="loader"
data-chromatic="ignore"
{...attrs}
>
<CircularProgress size={size} />
+19 -4
View File
@@ -121,12 +121,19 @@ export const Markdown: FC<MarkdownProps> = (props) => {
);
};
interface MarkdownInlineProps {
interface InlineMarkdownProps {
/**
* The Markdown text to parse and render
*/
children: string;
/**
* Additional element types to allow.
* Allows italic, bold, links, and inline code snippets by default.
* eg. `["ol", "ul", "li"]` to support lists.
*/
allowedElements?: readonly string[];
className?: string;
/**
@@ -138,13 +145,21 @@ interface MarkdownInlineProps {
/**
* Supports a strict subset of Markdown that behaves well as inline/confined content.
*/
export const InlineMarkdown: FC<MarkdownInlineProps> = (props) => {
const { children, className, components = {} } = props;
export const InlineMarkdown: FC<InlineMarkdownProps> = (props) => {
const { children, allowedElements = [], className, components = {} } = props;
return (
<ReactMarkdown
className={className}
allowedElements={["p", "em", "strong", "a", "pre", "code"]}
allowedElements={[
"p",
"em",
"strong",
"a",
"pre",
"code",
...allowedElements,
]}
unwrapDisallowed
components={{
p: ({ children }) => <>{children}</>,
@@ -1,4 +1,4 @@
import { PropsWithChildren } from "react";
import { type FC, type PropsWithChildren } from "react";
import Button from "@mui/material/Button";
import { useTheme } from "@emotion/react";
@@ -11,13 +11,13 @@ type NumberedPageButtonProps = {
disabled?: boolean;
};
export function NumberedPageButton({
export const NumberedPageButton: FC<NumberedPageButtonProps> = ({
pageNumber,
totalPages,
onClick,
highlighted = false,
disabled = false,
}: NumberedPageButtonProps) {
}) => {
return (
<BasePageButton
name="Page button"
@@ -29,16 +29,16 @@ export function NumberedPageButton({
{pageNumber}
</BasePageButton>
);
}
};
type PlaceholderPageButtonProps = PropsWithChildren<{
pagesOmitted: number;
}>;
export function PlaceholderPageButton({
export const PlaceholderPageButton: FC<PlaceholderPageButtonProps> = ({
pagesOmitted,
children = <>&hellip;</>,
}: PlaceholderPageButtonProps) {
}) => {
return (
<BasePageButton
disabled
@@ -48,7 +48,7 @@ export function PlaceholderPageButton({
{children}
</BasePageButton>
);
}
};
type BasePageButtonProps = PropsWithChildren<{
name: string;
@@ -59,22 +59,29 @@ type BasePageButtonProps = PropsWithChildren<{
disabled?: boolean;
}>;
function BasePageButton({
const BasePageButton: FC<BasePageButtonProps> = ({
children,
onClick,
name,
"aria-label": ariaLabel,
highlighted = false,
disabled = false,
}: BasePageButtonProps) {
}) => {
const theme = useTheme();
return (
<Button
css={
highlighted && {
borderColor: `${theme.palette.info.main}`,
backgroundColor: `${theme.palette.info.dark}`,
borderColor: theme.experimental.roles.active.outline,
backgroundColor: theme.experimental.roles.active.background,
// Override the hover state with active colors, but not hover
// colors because clicking won't do anything.
"&:hover": {
borderColor: theme.experimental.roles.active.outline,
backgroundColor: theme.experimental.roles.active.background,
},
}
}
aria-label={ariaLabel}
@@ -85,7 +92,7 @@ function BasePageButton({
{children}
</Button>
);
}
};
function getNumberedButtonLabel(
page: number,
@@ -1,6 +1,6 @@
import useMediaQuery from "@mui/material/useMediaQuery";
import { useTheme } from "@emotion/react";
import { type FC } from "react";
import { PlaceholderPageButton, NumberedPageButton } from "./PageButtons";
import { buildPagedList } from "./utils";
import { PaginationNavButton } from "./PaginationNavButton";
@@ -17,14 +17,14 @@ export type PaginationWidgetBaseProps = {
hasNextPage?: boolean;
};
export const PaginationWidgetBase = ({
export const PaginationWidgetBase: FC<PaginationWidgetBaseProps> = ({
currentPage,
pageSize,
totalRecords,
onPageChange,
hasPreviousPage,
hasNextPage,
}: PaginationWidgetBaseProps) => {
}) => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down("md"));
const totalPages = Math.ceil(totalRecords / pageSize);
@@ -99,11 +99,11 @@ type PaginationRowProps = {
onChange: (newPage: number) => void;
};
function PaginationRow({
const PaginationRow: FC<PaginationRowProps> = ({
currentPage,
totalPages,
onChange,
}: PaginationRowProps) {
}) => {
const pageInfo = buildPagedList(totalPages, currentPage);
const pagesOmitted = totalPages - pageInfo.length - 1;
@@ -131,4 +131,4 @@ function PaginationRow({
})}
</>
);
}
};
+5 -4
View File
@@ -1,5 +1,5 @@
import { type FC, type ReactNode, useMemo, forwardRef } from "react";
import { css, type Interpolation, type Theme } from "@emotion/react";
import { css, useTheme, type Interpolation, type Theme } from "@emotion/react";
import type { ThemeRole } from "theme/experimental";
export type PillType = ThemeRole | keyof typeof themeOverrides;
@@ -14,8 +14,8 @@ export interface PillProps {
const themeOverrides = {
neutral: (theme) => ({
backgroundColor: theme.colors.gray[13],
borderColor: theme.colors.gray[6],
backgroundColor: theme.experimental.l1.background,
borderColor: theme.experimental.l1.outline,
}),
} satisfies Record<string, Interpolation<Theme>>;
@@ -30,6 +30,7 @@ const themeStyles = (type: ThemeRole) => (theme: Theme) => {
export const Pill: FC<PillProps> = forwardRef<HTMLDivElement, PillProps>(
(props, ref) => {
const { icon, text = null, type = "neutral", ...attrs } = props;
const theme = useTheme();
const typeStyles = useMemo(() => {
if (type in themeOverrides) {
@@ -50,7 +51,7 @@ export const Pill: FC<PillProps> = forwardRef<HTMLDivElement, PillProps>(
borderStyle: "solid",
borderRadius: 99999,
fontSize: 12,
color: "#FFF",
color: theme.experimental.l1.text,
height: 24,
paddingLeft: icon ? 6 : 12,
paddingRight: 12,
@@ -1,4 +1,4 @@
import { useTheme } from "@mui/material/styles";
import { useTheme } from "@emotion/react";
import HelpOutline from "@mui/icons-material/HelpOutline";
import Tooltip from "@mui/material/Tooltip";
import { type FC } from "react";
@@ -1,6 +1,5 @@
import { type Theme, useTheme } from "@emotion/react";
import { type FC } from "react";
import { useTheme } from "@emotion/react";
import { Theme } from "@mui/material/styles";
import type { WorkspaceAgent, DERPRegion } from "api/typesGenerated";
import {
HelpTooltipText,
@@ -31,7 +30,11 @@ const getDisplayLatency = (theme: Theme, agent: WorkspaceAgent) => {
};
};
export const AgentLatency: FC<{ agent: WorkspaceAgent }> = ({ agent }) => {
interface AgentLatencyProps {
agent: WorkspaceAgent;
}
export const AgentLatency: FC<AgentLatencyProps> = ({ agent }) => {
const theme = useTheme();
const latency = getDisplayLatency(theme, agent);
@@ -95,7 +95,7 @@ export interface AgentMetadataViewProps {
export const AgentMetadataView: FC<AgentMetadataViewProps> = ({ metadata }) => {
if (metadata.length === 0) {
return <></>;
return null;
}
return (
<div css={styles.root}>
@@ -131,7 +131,7 @@ export const AgentMetadata: FC<AgentMetadataProps> = ({
return;
}
let timeout: NodeJS.Timeout | undefined = undefined;
let timeout: ReturnType<typeof setTimeout> | undefined = undefined;
const connect = (): (() => void) => {
const source = watchAgentMetadata(agent.id);
@@ -259,7 +259,9 @@ const styles = {
},
metadataValueSuccess: (theme) => ({
color: theme.palette.success.light,
// color: theme.palette.success.light,
color: theme.experimental.roles.success.fill,
// color: theme.experimental.roles.success.text,
}),
metadataValueError: (theme) => ({
@@ -1,3 +1,5 @@
import type { Meta, StoryObj } from "@storybook/react";
import { chromatic } from "testHelpers/chromatic";
import {
MockPrimaryWorkspaceProxy,
MockWorkspaceProxies,
@@ -21,7 +23,6 @@ import {
} from "testHelpers/entities";
import { AgentRow, LineWithID } from "./AgentRow";
import { ProxyContext, getPreferredProxy } from "contexts/ProxyContext";
import type { Meta, StoryObj } from "@storybook/react";
const defaultAgentMetadata = [
{
@@ -102,6 +103,7 @@ const storybookLogs: LineWithID[] = [
const meta: Meta<typeof AgentRow> = {
title: "components/AgentRow",
parameters: { chromatic },
component: AgentRow,
args: {
storybookLogs,
+10 -31
View File
@@ -426,28 +426,14 @@ export const AgentRow: FC<AgentRowProps> = ({
</AutoSizer>
</Collapse>
<div css={styles.logsPanelButtons}>
{showLogs ? (
<button
css={[styles.logsPanelButton, styles.toggleLogsButton]}
onClick={() => {
setShowLogs((v) => !v);
}}
>
<DropdownArrow close />
Hide logs
</button>
) : (
<button
css={[styles.logsPanelButton, styles.toggleLogsButton]}
onClick={() => {
setShowLogs((v) => !v);
}}
>
<DropdownArrow />
Show logs
</button>
)}
<div css={{ display: "flex" }}>
<button
css={styles.logsPanelButton}
onClick={() => setShowLogs((v) => !v)}
>
<DropdownArrow close={showLogs} />
{showLogs ? "Hide" : "Show"} logs
</button>
</div>
</div>
)}
@@ -673,10 +659,6 @@ const styles = {
borderTop: `1px solid ${theme.palette.divider}`,
}),
logsPanelButtons: {
display: "flex",
},
logsPanelButton: (theme) => ({
textAlign: "left",
background: "transparent",
@@ -689,10 +671,11 @@ const styles = {
alignItems: "center",
gap: 8,
whiteSpace: "nowrap",
width: "100%",
"&:hover": {
color: theme.palette.text.primary,
backgroundColor: theme.colors.gray[14],
backgroundColor: theme.experimental.l2.hover.background,
},
"& svg": {
@@ -700,10 +683,6 @@ const styles = {
},
}),
toggleLogsButton: {
width: "100%",
},
buttonSkeleton: {
borderRadius: 4,
},
@@ -1,43 +1,31 @@
import { PortForwardPopoverView } from "./PortForwardButton";
import { PortForwardButton } from "./PortForwardButton";
import type { Meta, StoryObj } from "@storybook/react";
import {
MockListeningPortsResponse,
MockWorkspaceAgent,
} from "testHelpers/entities";
const meta: Meta<typeof PortForwardPopoverView> = {
title: "components/PortForwardPopoverView",
component: PortForwardPopoverView,
decorators: [
(Story) => (
<div
css={(theme) => ({
width: 304,
border: `1px solid ${theme.palette.divider}`,
borderRadius: 8,
backgroundColor: theme.palette.background.paper,
})}
>
<Story />
</div>
),
],
const meta: Meta<typeof PortForwardButton> = {
title: "components/PortForwardButton",
component: PortForwardButton,
args: {
agent: MockWorkspaceAgent,
},
};
export default meta;
type Story = StoryObj<typeof PortForwardPopoverView>;
type Story = StoryObj<typeof PortForwardButton>;
export const WithPorts: Story = {
export const Example: Story = {
args: {
ports: MockListeningPortsResponse.ports,
storybook: {
portsQueryData: MockListeningPortsResponse,
},
},
};
export const Empty: Story = {
export const Loading: Story = {
args: {
ports: [],
storybook: {},
},
};
@@ -10,6 +10,7 @@ import { getAgentListeningPorts } from "api/api";
import type {
WorkspaceAgent,
WorkspaceAgentListeningPort,
WorkspaceAgentListeningPortsResponse,
} from "api/typesGenerated";
import { portForwardURL } from "utils/portForward";
import { type ClassName, useClassName } from "hooks/useClassName";
@@ -32,34 +33,43 @@ export interface PortForwardButtonProps {
username: string;
workspaceName: string;
agent: WorkspaceAgent;
/**
* Only for use in Storybook
*/
storybook?: {
portsQueryData?: WorkspaceAgentListeningPortsResponse;
};
}
export const PortForwardButton: FC<PortForwardButtonProps> = (props) => {
const { agent } = props;
const { agent, storybook } = props;
const paper = useClassName(classNames.paper, []);
const portsQuery = useQuery({
queryKey: ["portForward", agent.id],
queryFn: () => getAgentListeningPorts(agent.id),
enabled: agent.status === "connected",
enabled: !storybook && agent.status === "connected",
refetchInterval: 5_000,
});
const data = storybook ? storybook.portsQueryData : portsQuery.data;
return (
<Popover>
<PopoverTrigger>
<AgentButton disabled={!portsQuery.data}>
<AgentButton disabled={!data}>
{DisplayAppNameMap["port_forwarding_helper"]}
{portsQuery.data ? (
<div css={styles.portCount}>{portsQuery.data.ports.length}</div>
{data ? (
<div css={styles.portCount}>{data.ports.length}</div>
) : (
<CircularProgress size={10} css={{ marginLeft: 8 }} />
)}
</AgentButton>
</PopoverTrigger>
<PopoverContent horizontal="right" classes={{ paper }}>
<PortForwardPopoverView {...props} ports={portsQuery.data?.ports} />
<PortForwardPopoverView {...props} ports={data?.ports} />
</PopoverContent>
</Popover>
);
@@ -204,7 +214,7 @@ const styles = {
display: "flex",
alignItems: "center",
justifyContent: "center",
backgroundColor: theme.colors.gray[11],
backgroundColor: theme.experimental.l2.background,
marginLeft: 8,
}),
@@ -0,0 +1,43 @@
import { PortForwardPopoverView } from "./PortForwardButton";
import type { Meta, StoryObj } from "@storybook/react";
import {
MockListeningPortsResponse,
MockWorkspaceAgent,
} from "testHelpers/entities";
const meta: Meta<typeof PortForwardPopoverView> = {
title: "components/PortForwardPopoverView",
component: PortForwardPopoverView,
decorators: [
(Story) => (
<div
css={(theme) => ({
width: 304,
border: `1px solid ${theme.palette.divider}`,
borderRadius: 8,
backgroundColor: theme.palette.background.paper,
})}
>
<Story />
</div>
),
],
args: {
agent: MockWorkspaceAgent,
},
};
export default meta;
type Story = StoryObj<typeof PortForwardPopoverView>;
export const WithPorts: Story = {
args: {
ports: MockListeningPortsResponse.ports,
},
};
export const Empty: Story = {
args: {
ports: [],
},
};
@@ -1,15 +1,13 @@
import { type FC } from "react";
import type { WorkspaceResource } from "api/typesGenerated";
import { Avatar, AvatarIcon } from "components/Avatar/Avatar";
import { FC } from "react";
import { WorkspaceResource } from "api/typesGenerated";
const FALLBACK_ICON = "/icon/widgets.svg";
// These resources (i.e. docker_image, kubernetes_deployment) map to Terraform
// resource types. These are the most used ones and are based on user usage.
// We may want to update from time-to-time.
const BUILT_IN_ICON_PATHS: {
[resourceType: WorkspaceResource["type"]]: string;
} = {
const BUILT_IN_ICON_PATHS: Record<string, string> = {
docker_volume: "/icon/database.svg",
docker_container: "/icon/memory.svg",
docker_image: "/icon/container.svg",
@@ -19,24 +17,16 @@ const BUILT_IN_ICON_PATHS: {
google_compute_instance: "/icon/memory.svg",
aws_instance: "/icon/memory.svg",
kubernetes_deployment: "/icon/memory.svg",
null_resource: FALLBACK_ICON,
};
export const getIconPathResource = (resourceType: string): string => {
if (resourceType in BUILT_IN_ICON_PATHS) {
return BUILT_IN_ICON_PATHS[resourceType];
}
return FALLBACK_ICON;
return BUILT_IN_ICON_PATHS[resourceType] ?? FALLBACK_ICON;
};
export type ResourceAvatarProps = { resource: WorkspaceResource };
export const ResourceAvatar: FC<ResourceAvatarProps> = ({ resource }) => {
const hasIcon = resource.icon && resource.icon !== "";
const avatarSrc = hasIcon
? resource.icon
: getIconPathResource(resource.type);
const avatarSrc = resource.icon || getIconPathResource(resource.type);
return (
<Avatar background>
@@ -1,9 +1,11 @@
import { TemplateVersionParameter } from "api/typesGenerated";
import { RichParameterInput } from "./RichParameterInput";
import type { Meta, StoryObj } from "@storybook/react";
import type { TemplateVersionParameter } from "api/typesGenerated";
import { chromatic } from "testHelpers/chromatic";
import { RichParameterInput } from "./RichParameterInput";
const meta: Meta<typeof RichParameterInput> = {
title: "components/RichParameterInput",
parameters: { chromatic },
component: RichParameterInput,
};
@@ -5,13 +5,20 @@ import { useCoderTheme } from "./coderTheme";
loader.config({ monaco });
export const SyntaxHighlighter: FC<{
interface SyntaxHighlighterProps {
value: string;
language: string;
editorProps?: ComponentProps<typeof Editor> &
ComponentProps<typeof DiffEditor>;
compareWith?: string;
}> = ({ value, compareWith, language, editorProps }) => {
}
export const SyntaxHighlighter: FC<SyntaxHighlighterProps> = ({
value,
compareWith,
language,
editorProps,
}) => {
const hasDiff = compareWith && value !== compareWith;
const coderTheme = useCoderTheme();
const commonProps = {
@@ -1,222 +1,6 @@
import { useMonaco } from "@monaco-editor/react";
import { useTheme } from "@emotion/react";
import { useEffect, useState } from "react";
import { editor } from "monaco-editor";
import { type Theme, useTheme } from "@emotion/react";
// Theme based on https://github.com/brijeshb42/monaco-themes/blob/master/themes/Dracula.json
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- The theme is not typed
export const coderTheme = (theme: Theme): editor.IStandaloneThemeData => ({
base: "vs-dark",
inherit: true,
rules: [
{
background: "282a36",
token: "",
},
{
foreground: "6272a4",
token: "comment",
},
{
foreground: "f1fa8c",
token: "string",
},
{
foreground: "bd93f9",
token: "constant.numeric",
},
{
foreground: "bd93f9",
token: "constant.language",
},
{
foreground: "bd93f9",
token: "constant.character",
},
{
foreground: "bd93f9",
token: "constant.other",
},
{
foreground: "ffb86c",
token: "variable.other.readwrite.instance",
},
{
foreground: "ff79c6",
token: "constant.character.escaped",
},
{
foreground: "ff79c6",
token: "constant.character.escape",
},
{
foreground: "ff79c6",
token: "string source",
},
{
foreground: "ff79c6",
token: "string source.ruby",
},
{
foreground: "ff79c6",
token: "keyword",
},
{
foreground: "ff79c6",
token: "storage",
},
{
foreground: "8be9fd",
fontStyle: "italic",
token: "storage.type",
},
{
foreground: "50fa7b",
fontStyle: "underline",
token: "entity.name.class",
},
{
foreground: "50fa7b",
fontStyle: "italic underline",
token: "entity.other.inherited-class",
},
{
foreground: "50fa7b",
token: "entity.name.function",
},
{
foreground: "ffb86c",
fontStyle: "italic",
token: "variable.parameter",
},
{
foreground: "ff79c6",
token: "entity.name.tag",
},
{
foreground: "50fa7b",
token: "entity.other.attribute-name",
},
{
foreground: "8be9fd",
token: "support.function",
},
{
foreground: "6be5fd",
token: "support.constant",
},
{
foreground: "66d9ef",
fontStyle: " italic",
token: "support.type",
},
{
foreground: "66d9ef",
fontStyle: " italic",
token: "support.class",
},
{
foreground: "f8f8f0",
background: "ff79c6",
token: "invalid",
},
{
foreground: "f8f8f0",
background: "bd93f9",
token: "invalid.deprecated",
},
{
foreground: "cfcfc2",
token: "meta.structure.dictionary.json string.quoted.double.json",
},
{
foreground: "6272a4",
token: "meta.diff",
},
{
foreground: "6272a4",
token: "meta.diff.header",
},
{
foreground: "ff79c6",
token: "markup.deleted",
},
{
foreground: "50fa7b",
token: "markup.inserted",
},
{
foreground: "e6db74",
token: "markup.changed",
},
{
foreground: "bd93f9",
token: "constant.numeric.line-number.find-in-files - match",
},
{
foreground: "e6db74",
token: "entity.name.filename",
},
{
foreground: "f83333",
token: "message.error",
},
{
foreground: "eeeeee",
token:
"punctuation.definition.string.begin.json - meta.structure.dictionary.value.json",
},
{
foreground: "eeeeee",
token:
"punctuation.definition.string.end.json - meta.structure.dictionary.value.json",
},
{
foreground: "8be9fd",
token: "meta.structure.dictionary.json string.quoted.double.json",
},
{
foreground: "f1fa8c",
token: "meta.structure.dictionary.value.json string.quoted.double.json",
},
{
foreground: "50fa7b",
token:
"meta meta meta meta meta meta meta.structure.dictionary.value string",
},
{
foreground: "ffb86c",
token: "meta meta meta meta meta meta.structure.dictionary.value string",
},
{
foreground: "ff79c6",
token: "meta meta meta meta meta.structure.dictionary.value string",
},
{
foreground: "bd93f9",
token: "meta meta meta meta.structure.dictionary.value string",
},
{
foreground: "50fa7b",
token: "meta meta meta.structure.dictionary.value string",
},
{
foreground: "ffb86c",
token: "meta meta.structure.dictionary.value string",
},
],
colors: {
"editor.foreground": theme.palette.text.primary,
"editor.background": theme.palette.background.default,
"editor.selectionBackground": theme.palette.action.hover,
"editor.lineHighlightBackground": theme.palette.background.paper,
"editorCursor.foreground": "#f8f8f0",
"editorWhitespace.foreground": "#3B3A32",
"editorIndentGuide.activeBackground": "#9D550FB0",
"editor.selectionHighlightBorder": "#222218",
},
});
export const useCoderTheme = (): { isLoading: boolean; name: string } => {
const [isLoading, setIsLoading] = useState(true);
@@ -226,7 +10,7 @@ export const useCoderTheme = (): { isLoading: boolean; name: string } => {
useEffect(() => {
if (monaco) {
monaco.editor.defineTheme(name, coderTheme(theme));
monaco.editor.defineTheme(name, theme.monaco);
setIsLoading(false);
}
}, [monaco, theme]);
File diff suppressed because one or more lines are too long
@@ -37,11 +37,17 @@ const languageByExtension: Record<AllowedExtension, string> = {
protobuf: "protobuf",
};
export const TemplateFiles: FC<{
interface TemplateFilesProps {
currentFiles: TemplateVersionFiles;
previousFiles?: TemplateVersionFiles;
tab: UseTabResult;
}> = ({ currentFiles, previousFiles, tab }) => {
}
export const TemplateFiles: FC<TemplateFilesProps> = ({
currentFiles,
previousFiles,
tab,
}) => {
const filenames = Object.keys(currentFiles);
const selectedFilename = filenames[Number(tab.value)];
const currentFile = currentFiles[selectedFilename];
@@ -5,6 +5,8 @@ import { type FC, type ReactNode, useMemo } from "react";
import AnsiToHTML from "ansi-to-html";
import { MONOSPACE_FONT_FAMILY } from "theme/constants";
const convert = new AnsiToHTML();
export interface Line {
time: string;
output: string;
@@ -16,9 +18,10 @@ export interface LogsProps {
lines: Line[];
hideTimestamps?: boolean;
className?: string;
children?: ReactNode;
}
export const Logs: FC<React.PropsWithChildren<LogsProps>> = ({
export const Logs: FC<LogsProps> = ({
hideTimestamps,
lines,
className = "",
@@ -50,16 +53,23 @@ export const Logs: FC<React.PropsWithChildren<LogsProps>> = ({
export const logLineHeight = 20;
const convert = new AnsiToHTML();
export const LogLine: FC<{
interface LogLineProps {
line: Line;
hideTimestamp?: boolean;
number?: number;
style?: React.CSSProperties;
sourceIcon?: ReactNode;
maxNumber?: number;
}> = ({ line, hideTimestamp, number, maxNumber, sourceIcon, style }) => {
}
export const LogLine: FC<LogLineProps> = ({
line,
hideTimestamp,
number,
maxNumber,
sourceIcon,
style,
}) => {
const output = useMemo(() => {
return convert.toHtml(line.output.split(/\r/g).pop() as string);
}, [line.output]);
@@ -120,15 +130,15 @@ const styles = {
padding: "0 32px",
"&.error": {
backgroundColor: theme.palette.error.dark,
backgroundColor: theme.experimental.roles.error.background,
},
"&.debug": {
backgroundColor: theme.palette.background.paper,
backgroundColor: theme.experimental.roles.info.background,
},
"&.warn": {
backgroundColor: theme.palette.warning.dark,
backgroundColor: theme.experimental.roles.warning.background,
},
}),
space: {
@@ -1,9 +1,11 @@
import { Meta, StoryObj } from "@storybook/react";
import { WorkspaceBuildLogs } from "./WorkspaceBuildLogs";
import type { Meta, StoryObj } from "@storybook/react";
import { chromatic } from "testHelpers/chromatic";
import { MockWorkspaceBuildLogs } from "testHelpers/entities";
import { WorkspaceBuildLogs } from "./WorkspaceBuildLogs";
const meta: Meta<typeof WorkspaceBuildLogs> = {
title: "components/WorkspaceBuildLogs",
parameters: { chromatic },
component: WorkspaceBuildLogs,
};
@@ -69,7 +69,7 @@ export const DormantStatusBadge: FC<DormantStatusBadgeProps> = ({
className,
}) => {
if (!workspace.dormant_at) {
return <></>;
return null;
}
const formatDate = (dateStr: string): string => {
@@ -7,15 +7,19 @@ import {
import {
type FC,
type PropsWithChildren,
type ReactNode,
useContext,
useEffect,
useMemo,
useState,
} from "react";
import themes, { DEFAULT_THEME } from "theme";
import themes, { DEFAULT_THEME, type Theme } from "theme";
import { AuthContext } from "./AuthProvider/AuthProvider";
export const ThemeProviders: FC<PropsWithChildren> = ({ children }) => {
/**
*
*/
export const ThemeProvider: FC<PropsWithChildren> = ({ children }) => {
// We need to use the `AuthContext` directly, rather than the `useAuth` hook,
// because Storybook and many tests depend on this component, but do not provide
// an `AuthProvider`, and `useAuth` will throw in that case.
@@ -56,12 +60,23 @@ export const ThemeProviders: FC<PropsWithChildren> = ({ children }) => {
return (
<StyledEngineProvider injectFirst>
<MuiThemeProvider theme={theme}>
<EmotionThemeProvider theme={theme}>
<CssBaseline enableColorScheme />
{children}
</EmotionThemeProvider>
</MuiThemeProvider>
<ThemeOverride theme={theme}>{children}</ThemeOverride>
</StyledEngineProvider>
);
};
interface ThemeOverrideProps {
theme: Theme;
children?: ReactNode;
}
export const ThemeOverride: FC<ThemeOverrideProps> = ({ theme, children }) => {
return (
<MuiThemeProvider theme={theme}>
<EmotionThemeProvider theme={theme}>
<CssBaseline enableColorScheme />
{children}
</EmotionThemeProvider>
</MuiThemeProvider>
);
};
@@ -1,12 +1,16 @@
import { FC } from "react";
import { type FC } from "react";
import { AuditLog } from "api/typesGenerated";
import { Link as RouterLink } from "react-router-dom";
import Link from "@mui/material/Link";
import { BuildAuditDescription } from "./BuildAuditDescription";
export const AuditLogDescription: FC<{ auditLog: AuditLog }> = ({
interface AuditLogDescriptionProps {
auditLog: AuditLog;
}
export const AuditLogDescription: FC<AuditLogDescriptionProps> = ({
auditLog,
}): JSX.Element => {
}) => {
let target = auditLog.resource_target.trim();
const user = auditLog.user?.username.trim();
@@ -4,6 +4,8 @@ import TableCell from "@mui/material/TableCell";
import TableContainer from "@mui/material/TableContainer";
import TableHead from "@mui/material/TableHead";
import TableRow from "@mui/material/TableRow";
import type { Meta, StoryObj } from "@storybook/react";
import { chromatic } from "testHelpers/chromatic";
import {
MockAuditLog,
MockAuditLog2,
@@ -12,7 +14,6 @@ import {
MockAuditLogGitSSH,
} from "testHelpers/entities";
import { AuditLogRow } from "./AuditLogRow";
import type { Meta, StoryObj } from "@storybook/react";
const meta: Meta<typeof AuditLogRow> = {
title: "pages/AuditPage/AuditLogRow",
@@ -48,6 +49,7 @@ export const NoDiff: Story = {
};
export const WithDiff: Story = {
parameters: { chromatic },
args: {
auditLog: MockAuditLog2,
defaultIsDiffOpen: true,
@@ -55,6 +57,7 @@ export const WithDiff: Story = {
};
export const WithLongDiffRow: Story = {
parameters: { chromatic },
args: {
auditLog: {
...MockAuditLog2,
@@ -1,12 +1,13 @@
import { Meta, StoryObj } from "@storybook/react";
import { type ComponentProps } from "react";
import { chromaticWithTablet } from "testHelpers/chromatic";
import { MockAuditLog, MockAuditLog2, MockUser } from "testHelpers/entities";
import { AuditPageView } from "./AuditPageView";
import { ComponentProps } from "react";
import {
mockInitialRenderResult,
mockSuccessResult,
} from "components/PaginationWidget/PaginationContainer.mocks";
import { type UsePaginatedQueryResult } from "hooks/usePaginatedQuery";
import { AuditPageView } from "./AuditPageView";
import {
MockMenu,
@@ -43,6 +44,7 @@ export default meta;
type Story = StoryObj<typeof AuditPageView>;
export const AuditPage: Story = {
parameters: { chromatic: chromaticWithTablet },
args: {
auditsQuery: mockSuccessResult,
},
@@ -84,12 +86,3 @@ export const NotVisible: Story = {
auditsQuery: mockInitialRenderResult,
},
};
export const AuditPageSmallViewport: Story = {
args: {
auditsQuery: mockSuccessResult,
},
parameters: {
chromatic: { viewports: [600] },
},
};
+6 -2
View File
@@ -13,8 +13,12 @@ export const AuditPaywall: FC = () => {
description="Audit Logs allows Auditors to monitor user operations in their deployment. To use this feature, you need an Enterprise license."
cta={
<Stack direction="row" alignItems="center">
<Link href={docs("/admin/upgrade")} target="_blank" rel="noreferrer">
<Button size="small" startIcon={<ArrowRightAltOutlined />}>
<Link target="_blank" rel="noreferrer">
<Button
href={docs("/admin/upgrade")}
size="small"
startIcon={<ArrowRightAltOutlined />}
>
See how to upgrade
</Button>
</Link>
@@ -32,17 +32,13 @@ export const CliAuthPageView: FC<CliAuthPageViewProps> = ({ sessionToken }) => {
}}
>
Copy the session token below and{" "}
<strong
css={{
whiteSpace: "nowrap",
}}
>
<strong css={{ whiteSpace: "nowrap" }}>
paste it in your terminal
</strong>
.
</p>
<CodeExample code={sessionToken} password />
<CodeExample code={sessionToken} secret />
<div
css={{
@@ -1,4 +1,5 @@
import { Meta, StoryObj } from "@storybook/react";
import { chromatic } from "testHelpers/chromatic";
import {
mockApiError,
MockTemplate,
@@ -11,6 +12,7 @@ import { CreateWorkspacePageView } from "./CreateWorkspacePageView";
const meta: Meta<typeof CreateWorkspacePageView> = {
title: "pages/CreateWorkspacePage",
parameters: { chromatic },
component: CreateWorkspacePageView,
args: {
defaultName: "",
@@ -9,7 +9,7 @@ const meta: Meta<typeof ExternalAuth> = {
export default meta;
type Story = StoryObj<typeof ExternalAuth>;
export const GithubNotAuthenticated: Story = {
export const Github: Story = {
args: {
displayIcon: "/icon/github.svg",
displayName: "GitHub",
@@ -17,6 +17,24 @@ export const GithubNotAuthenticated: Story = {
},
};
export const GithubTimeout: Story = {
args: {
displayIcon: "/icon/github.svg",
displayName: "GitHub",
authenticated: false,
externalAuthPollingState: "abandoned",
},
};
export const GithubFailed: Story = {
args: {
displayIcon: "/icon/github.svg",
displayName: "GitHub",
authenticated: false,
error: "Github doesn't like you",
},
};
export const GithubAuthenticated: Story = {
args: {
displayIcon: "/icon/github.svg",
@@ -25,7 +43,7 @@ export const GithubAuthenticated: Story = {
},
};
export const GitlabNotAuthenticated: Story = {
export const Gitlab: Story = {
args: {
displayIcon: "/icon/gitlab.svg",
displayName: "GitLab",
@@ -41,7 +59,7 @@ export const GitlabAuthenticated: Story = {
},
};
export const AzureDevOpsNotAuthenticated: Story = {
export const AzureDevOps: Story = {
args: {
displayIcon: "/icon/azure-devops.svg",
displayName: "Azure DevOps",
@@ -57,7 +75,7 @@ export const AzureDevOpsAuthenticated: Story = {
},
};
export const BitbucketNotAuthenticated: Story = {
export const Bitbucket: Story = {
args: {
displayIcon: "/icon/bitbucket.svg",
displayName: "Bitbucket",
@@ -16,70 +16,82 @@ export interface ExternalAuthProps {
startPollingExternalAuth: () => void;
error?: string;
message?: string;
fullWidth?: boolean;
}
export const ExternalAuth: FC<ExternalAuthProps> = (props) => {
const {
displayName,
displayIcon,
authenticated,
authenticateURL,
externalAuthPollingState,
startPollingExternalAuth,
error,
message,
} = props;
export const ExternalAuth: FC<ExternalAuthProps> = ({
displayName,
displayIcon,
authenticated,
authenticateURL,
externalAuthPollingState,
startPollingExternalAuth,
error,
message,
fullWidth = true,
}) => {
const messageContent =
message ??
(authenticated
? `Authenticated with ${displayName}`
: `Login with ${displayName}`);
return (
<Tooltip
title={authenticated && `${displayName} has already been connected.`}
>
<Stack alignItems="center" spacing={1}>
<LoadingButton
loadingPosition="start"
loading={externalAuthPollingState === "polling"}
href={authenticateURL}
variant="contained"
size="large"
startIcon={
displayIcon && (
<img
src={displayIcon}
alt={`${displayName} Icon`}
width={16}
height={16}
/>
)
}
disabled={authenticated}
css={{ height: 52 }}
color={error ? "error" : undefined}
fullWidth
onClick={(event) => {
event.preventDefault();
// If the user is already authenticated, we don't want to redirect them
if (authenticated || authenticateURL === "") {
return;
}
window.open(authenticateURL, "_blank", "width=900,height=600");
startPollingExternalAuth();
}}
>
{messageContent}
</LoadingButton>
{externalAuthPollingState === "abandoned" && (
<Button variant="text" onClick={startPollingExternalAuth}>
<ReplayIcon /> Check again
</Button>
)}
{error && <FormHelperText error>{error}</FormHelperText>}
</Stack>
</Tooltip>
return (
<>
<Tooltip
title={authenticated && `${displayName} has already been connected.`}
>
<Stack
alignItems="center"
spacing={1}
css={!fullWidth && { display: "inline-block" }}
>
<LoadingButton
loadingPosition="start"
loading={externalAuthPollingState === "polling"}
href={authenticateURL}
variant="contained"
size="large"
startIcon={
displayIcon && (
<img
src={displayIcon}
alt={`${displayName} Icon`}
width={16}
height={16}
/>
)
}
disabled={authenticated}
css={{ height: 42 }}
fullWidth={fullWidth}
onClick={(event) => {
event.preventDefault();
// If the user is already authenticated, we don't want to redirect them
if (authenticated || authenticateURL === "") {
return;
}
window.open(authenticateURL, "_blank", "width=900,height=600");
startPollingExternalAuth();
}}
>
{messageContent}
</LoadingButton>
{externalAuthPollingState === "abandoned" && (
<Button variant="text" onClick={startPollingExternalAuth}>
<ReplayIcon /> Check again
</Button>
)}
</Stack>
</Tooltip>
{error && (
<FormHelperText
css={(theme) => ({ color: theme.experimental.roles.error.text })}
>
{error}
</FormHelperText>
)}
</>
);
};
@@ -19,6 +19,7 @@ import {
import { Fieldset } from "components/DeploySettingsLayout/Fieldset";
import { Stack } from "components/Stack/Stack";
import { getFormHelpers } from "utils/formUtils";
import colors from "theme/tailwind";
export type AppearanceSettingsPageViewProps = {
appearance: UpdateAppearanceConfig;
@@ -29,11 +30,12 @@ export type AppearanceSettingsPageViewProps = {
) => void;
};
const fallbackBgColor = colors.neutral[500];
export const AppearanceSettingsPageView: FC<
AppearanceSettingsPageViewProps
> = ({ appearance, isEntitled, onSaveAppearance }) => {
const theme = useTheme();
const fallbackBgColor = theme.colors.blue[7];
const applicationNameForm = useFormik<{
application_name: string;
@@ -1,8 +1,10 @@
import LicensesSettingsPageView from "./LicensesSettingsPageView";
import { chromatic } from "testHelpers/chromatic";
import { MockLicenseResponse } from "testHelpers/entities";
import LicensesSettingsPageView from "./LicensesSettingsPageView";
export default {
title: "pages/DeploySettingsPage/LicensesSettingsPageView",
parameters: { chromatic },
component: LicensesSettingsPageView,
};
+2 -1
View File
@@ -59,6 +59,7 @@ export const GroupsPageView: FC<GroupsPageViewProps> = ({
rel="noreferrer"
startIcon={<ArrowRightAltOutlined />}
variant="contained"
color="primary"
>
Learn about Enterprise
</Button>
@@ -68,7 +69,7 @@ export const GroupsPageView: FC<GroupsPageViewProps> = ({
target="_blank"
rel="noreferrer"
>
Read the docs
Read the documentation
</Link>
</Stack>
}
+16 -15
View File
@@ -1,4 +1,19 @@
import Tooltip from "@mui/material/Tooltip";
import CodeOutlined from "@mui/icons-material/CodeOutlined";
import TagOutlined from "@mui/icons-material/TagOutlined";
import ArrowBackOutlined from "@mui/icons-material/ArrowBackOutlined";
import { useTheme } from "@emotion/react";
import { type FC } from "react";
import { Helmet } from "react-helmet-async";
import { Link, useOutletContext, useParams } from "react-router-dom";
import type {
HealthMessage,
HealthSeverity,
HealthcheckReport,
} from "api/typesGenerated";
import { getLatencyColor } from "utils/latency";
import { Alert } from "components/Alert/Alert";
import { pageTitle } from "utils/page";
import {
Header,
HeaderTitle,
@@ -8,22 +23,8 @@ import {
Logs,
HealthyDot,
} from "./Content";
import {
HealthMessage,
HealthSeverity,
HealthcheckReport,
} from "api/typesGenerated";
import CodeOutlined from "@mui/icons-material/CodeOutlined";
import TagOutlined from "@mui/icons-material/TagOutlined";
import Tooltip from "@mui/material/Tooltip";
import { useTheme } from "@mui/material/styles";
import ArrowBackOutlined from "@mui/icons-material/ArrowBackOutlined";
import { getLatencyColor } from "utils/latency";
import { Alert } from "components/Alert/Alert";
import { Helmet } from "react-helmet-async";
import { pageTitle } from "utils/page";
export const DERPRegionPage = () => {
export const DERPRegionPage: FC = () => {
const theme = useTheme();
const healthStatus = useOutletContext<HealthcheckReport>();
const params = useParams() as { regionId: string };
@@ -122,6 +122,7 @@ export function HealthLayout() {
<div css={{ display: "flex", flexDirection: "column" }}>
<span css={{ fontWeight: 500 }}>Last check</span>
<span
data-chromatic="ignore"
css={{
color: theme.palette.text.secondary,
lineHeight: "150%",
@@ -134,6 +135,7 @@ export function HealthLayout() {
<div css={{ display: "flex", flexDirection: "column" }}>
<span css={{ fontWeight: 500 }}>Version</span>
<span
data-chromatic="ignore"
css={{
color: theme.palette.text.secondary,
lineHeight: "150%",
+3 -1
View File
@@ -1,9 +1,11 @@
import type { Meta } from "@storybook/react";
import { useQueryClient } from "react-query";
import {
reactRouterParameters,
reactRouterOutlet,
RouteDefinition,
} from "storybook-addon-react-router-v6";
import { chromatic } from "testHelpers/chromatic";
import {
MockBuildInfo,
MockEntitlements,
@@ -11,7 +13,6 @@ import {
MockHealth,
MockHealthSettings,
} from "testHelpers/entities";
import { Meta } from "@storybook/react";
import { HEALTH_QUERY_KEY, HEALTH_QUERY_SETTINGS_KEY } from "api/queries/debug";
import { DashboardProvider } from "components/Dashboard/DashboardProvider";
import { HealthLayout } from "./HealthLayout";
@@ -26,6 +27,7 @@ export const generateMeta = ({ element, path, params }: MetaOptions): Meta => {
return {
render: HealthLayout,
parameters: {
chromatic,
layout: "fullscreen",
reactRouter: reactRouterParameters({
location: { pathParams: params },
@@ -1,9 +1,11 @@
import { SetupPageView } from "./SetupPageView";
import { mockApiError } from "testHelpers/entities";
import type { Meta, StoryObj } from "@storybook/react";
import { chromatic } from "testHelpers/chromatic";
import { mockApiError } from "testHelpers/entities";
import { SetupPageView } from "./SetupPageView";
const meta: Meta<typeof SetupPageView> = {
title: "pages/SetupPage",
parameters: { chromatic },
component: SetupPageView,
};
@@ -1,10 +1,10 @@
import { type FC } from "react";
import { Helmet } from "react-helmet-async";
import { Loader } from "components/Loader/Loader";
import { TemplateFiles } from "components/TemplateFiles/TemplateFiles";
import { useTemplateLayoutContext } from "pages/TemplatePage/TemplateLayout";
import { FC } from "react";
import { Helmet } from "react-helmet-async";
import { getTemplatePageTitle } from "../utils";
import { useFileTab, useTemplateFiles } from "components/TemplateFiles/hooks";
import { useTemplateLayoutContext } from "pages/TemplatePage/TemplateLayout";
import { getTemplatePageTitle } from "../utils";
const TemplateFilesPage: FC = () => {
const { template, activeVersion } = useTemplateLayoutContext();
@@ -1,9 +1,11 @@
import type { Meta, StoryObj } from "@storybook/react";
import { TemplateInsightsPageView } from "./TemplateInsightsPage";
import { chromatic } from "testHelpers/chromatic";
import { MockEntitlementsWithUserLimit } from "testHelpers/entities";
import { TemplateInsightsPageView } from "./TemplateInsightsPage";
const meta: Meta<typeof TemplateInsightsPageView> = {
title: "pages/TemplatePage/TemplateInsightsPageView",
parameters: { chromatic },
component: TemplateInsightsPageView,
};
@@ -18,10 +18,10 @@ import type {
} from "api/typesGenerated";
import { useTheme } from "@emotion/react";
import {
PropsWithChildren,
type FC,
type HTMLAttributes,
type PropsWithChildren,
type ReactNode,
HTMLAttributes,
useId,
} from "react";
import chroma from "chroma-js";
@@ -39,7 +39,6 @@ export const VersionRow: FC<VersionRowProps> = ({
<TimelineEntry
data-testid={`version-${version.id}`}
{...clickableProps}
css={[styles.row]}
className={clickableProps.className}
>
<TableCell css={styles.versionCell}>
@@ -127,16 +126,6 @@ export const VersionRow: FC<VersionRowProps> = ({
};
const styles = {
row: (theme) => ({
"&:hover $promoteButton": {
color: theme.palette.text.primary,
borderColor: theme.colors.gray[11],
"&:hover": {
borderColor: theme.palette.text.primary,
},
},
}),
promoteButton: (theme) => ({
color: theme.palette.text.secondary,
transition: "none",
@@ -56,7 +56,7 @@ export const TemplatePermissionsPage: FC<
</Button>
</Link>
<Link href={docs("/admin/rbac")} target="_blank" rel="noreferrer">
Read the docs
Read the documentation
</Link>
</Stack>
}
@@ -92,8 +92,8 @@ export const FileTreeView: FC<FileTreeViewProps> = ({
}
&.Mui-selected {
color: ${theme.palette.text.primary};
background: ${theme.colors.gray[14]};
color: ${theme.experimental.roles.active.text};
background: ${theme.experimental.roles.active.background};
}
&.Mui-focused {
@@ -133,16 +133,13 @@ export const FileTreeView: FC<FileTreeViewProps> = ({
} as CSSProperties
}
>
{isFolder ? (
{isFolder &&
Object.keys(content)
.sort(sortFileTree(content))
.map((filename) => {
const child = content[filename];
return buildTreeItems(filename, child, currentPath);
})
) : (
<></>
)}
})}
</TreeItem>
);
};
@@ -1,16 +1,22 @@
import { useTheme } from "@emotion/react";
import Editor, { loader } from "@monaco-editor/react";
import * as monaco from "monaco-editor";
import { FC, useMemo } from "react";
import { type FC, useEffect, useMemo } from "react";
import { MONOSPACE_FONT_FAMILY } from "theme/constants";
loader.config({ monaco });
export const MonacoEditor: FC<{
interface MonacoEditorProps {
value?: string;
path?: string;
onChange?: (value: string) => void;
}> = ({ onChange, value, path }) => {
}
export const MonacoEditor: FC<MonacoEditorProps> = ({
onChange,
value,
path,
}) => {
const theme = useTheme();
const language = useMemo(() => {
@@ -31,6 +37,20 @@ export const MonacoEditor: FC<{
}
}, [path]);
useEffect(() => {
document.fonts.ready
.then(() => {
// Ensures that all text is measured properly.
// If this isn't done, there can be weird selection issues.
monaco.editor.remeasureFonts();
})
.catch(() => {
// Not a biggie!
});
monaco.editor.defineTheme("min", theme.monaco);
}, [theme]);
return (
<Editor
value={value}
@@ -52,62 +72,16 @@ export const MonacoEditor: FC<{
onChange(newValue);
}
}}
onMount={(editor, monaco) => {
onMount={(editor) => {
// This jank allows for Ctrl + Enter to work outside the editor.
// We use this keybind to trigger a build.
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Private type in Monaco!
(editor as any)._standaloneKeybindingService.addDynamicKeybinding(
`-editor.action.insertLineAfter`,
monaco.KeyMod.CtrlCmd | monaco.KeyCode.Enter,
() => {
//
},
() => {},
);
document.fonts.ready
.then(() => {
// Ensures that all text is measured properly.
// If this isn't done, there can be weird selection issues.
monaco.editor.remeasureFonts();
})
.catch(() => {
// Not a biggie!
});
monaco.editor.defineTheme("min", {
base: "vs-dark",
inherit: true,
rules: [
{
token: "comment",
foreground: "6B737C",
},
{
token: "type",
foreground: "B392F0",
},
{
token: "string",
foreground: "9DB1C5",
},
{
token: "variable",
foreground: "BBBBBB",
},
{
token: "identifier",
foreground: "B392F0",
},
{
token: "delimiter.curly",
foreground: "EBB325",
},
],
colors: {
"editor.foreground": theme.palette.text.primary,
"editor.background": theme.palette.background.paper,
},
});
editor.updateOptions({
theme: "min",
});
@@ -1,3 +1,5 @@
import type { Meta, StoryObj } from "@storybook/react";
import { chromatic } from "testHelpers/chromatic";
import {
MockFailedProvisionerJob,
MockRunningProvisionerJob,
@@ -14,19 +16,19 @@ import {
MockWorkspaceVolumeResource,
} from "testHelpers/entities";
import { TemplateVersionEditor } from "./TemplateVersionEditor";
import type { Meta, StoryObj } from "@storybook/react";
const meta: Meta<typeof TemplateVersionEditor> = {
title: "pages/TemplateVersionEditorPage",
title: "pages/TemplateVersionEditor",
parameters: {
chromatic,
layout: "fullscreen",
},
component: TemplateVersionEditor,
args: {
template: MockTemplate,
templateVersion: MockTemplateVersion,
defaultFileTree: MockTemplateVersionFileTree,
},
parameters: {
layout: "fullscreen",
},
};
export default meta;
@@ -1,3 +1,5 @@
import type { Meta, StoryObj } from "@storybook/react";
import { chromaticWithTablet } from "testHelpers/chromatic";
import {
mockApiError,
MockTemplate,
@@ -5,10 +7,10 @@ import {
MockTemplateExample2,
} from "testHelpers/entities";
import { TemplatesPageView } from "./TemplatesPageView";
import type { Meta, StoryObj } from "@storybook/react";
const meta: Meta<typeof TemplatesPageView> = {
title: "pages/TemplatesPage",
parameters: { chromatic: chromaticWithTablet },
component: TemplatesPageView,
};
@@ -65,15 +67,6 @@ export const WithTemplates: Story = {
},
};
export const WithTemplatesSmallViewPort: Story = {
args: {
...WithTemplates.args,
},
parameters: {
chromatic: { viewports: [600] },
},
};
export const EmptyCanCreate: Story = {
args: {
canCreateTemplates: true,
@@ -44,7 +44,7 @@ import { ErrorAlert } from "components/Alert/ErrorAlert";
import { docs } from "utils/docs";
import Skeleton from "@mui/material/Skeleton";
import { AvatarDataSkeleton } from "components/AvatarData/AvatarDataSkeleton";
import { Pill } from "components/Pill/Pill";
import { DeprecatedBadge } from "components/Badges/Badges";
export const Language = {
developerCount: (activeCount: number): string => {
@@ -127,7 +127,7 @@ const TemplateRow: FC<TemplateRowProps> = ({ template }) => {
<TableCell css={styles.actionCell}>
{template.deprecated ? (
<Pill text="Deprecated" type="warning" />
<DeprecatedBadge />
) : (
<Button
size="small"
@@ -278,15 +278,15 @@ const styles = {
}),
tableRow: (theme) => ({
"&:hover .actionButton": {
color: theme.palette.text.primary,
borderColor: theme.colors.gray[11],
"&:hover": {
borderColor: theme.palette.text.primary,
},
color: theme.experimental.l2.hover.text,
borderColor: theme.experimental.l2.hover.outline,
},
}),
actionButton: (theme) => ({
color: theme.palette.text.secondary,
transition: "none",
color: theme.palette.text.secondary,
"&:hover": {
borderColor: theme.palette.text.primary,
},
}),
} satisfies Record<string, Interpolation<Theme>>;
@@ -18,7 +18,6 @@ type Story = StoryObj<typeof AppearanceForm>;
export const Example: Story = {
args: {
enableAuto: true,
initialValues: { theme_preference: "" },
},
};
@@ -1,20 +1,18 @@
import { visuallyHidden } from "@mui/utils";
import { type Interpolation } from "@emotion/react";
import { type FC, useMemo } from "react";
import { type FC } from "react";
import type { UpdateUserAppearanceSettingsRequest } from "api/typesGenerated";
import themes, { DEFAULT_THEME, type Theme } from "theme";
import { ErrorAlert } from "components/Alert/ErrorAlert";
import { Stack } from "components/Stack/Stack";
import { BetaBadge } from "components/Badges/Badges";
import { ThemeOverride } from "contexts/ThemeProvider";
export interface AppearanceFormProps {
isUpdating?: boolean;
error?: unknown;
initialValues: UpdateUserAppearanceSettingsRequest;
onSubmit: (values: UpdateUserAppearanceSettingsRequest) => Promise<unknown>;
// temporary, so that storybook can test the right thing without showing
// a semi-broken auto theme to users. will be removed when light mode is done.
enableAuto?: boolean;
}
export const AppearanceForm: FC<AppearanceFormProps> = ({
@@ -22,7 +20,6 @@ export const AppearanceForm: FC<AppearanceFormProps> = ({
error,
onSubmit,
initialValues,
enableAuto,
}) => {
const currentTheme = initialValues.theme_preference || DEFAULT_THEME;
@@ -39,14 +36,13 @@ export const AppearanceForm: FC<AppearanceFormProps> = ({
{Boolean(error) && <ErrorAlert error={error} />}
<Stack direction="row" wrap="wrap">
{enableAuto && (
<AutoThemePreviewButton
displayName="Auto"
active={currentTheme === "auto"}
themes={[themes.dark, themes.light]}
onSelect={() => onChangeTheme("auto")}
/>
)}
<AutoThemePreviewButton
displayName="Auto"
active={currentTheme === "auto"}
themes={[themes.dark, themes.light]}
onSelect={() => onChangeTheme("auto")}
/>
<ThemePreviewButton
displayName="Dark"
active={currentTheme === "dark"}
@@ -59,21 +55,26 @@ export const AppearanceForm: FC<AppearanceFormProps> = ({
theme={themes.darkBlue}
onSelect={() => onChangeTheme("darkBlue")}
/>
<ThemePreviewButton
displayName="Light"
beta
active={currentTheme === "light"}
theme={themes.light}
onSelect={() => onChangeTheme("light")}
/>
</Stack>
</form>
);
};
interface AutoThemePreviewButtonProps {
active?: boolean;
className?: string;
displayName: string;
interface AutoThemePreviewButtonProps extends Omit<ThemePreviewProps, "theme"> {
themes: [Theme, Theme];
onSelect?: () => void;
}
const AutoThemePreviewButton: FC<AutoThemePreviewButtonProps> = ({
active,
beta,
className,
displayName,
themes,
@@ -101,11 +102,13 @@ const AutoThemePreviewButton: FC<AutoThemePreviewButtonProps> = ({
clipPath: "polygon(-5% -5%, 50% -5%, 50% 105%, -5% 105%)",
}}
active={active}
beta={beta}
displayName={displayName}
theme={leftTheme}
/>
<ThemePreview
active={active}
beta={beta}
displayName={displayName}
theme={rightTheme}
/>
@@ -114,16 +117,13 @@ const AutoThemePreviewButton: FC<AutoThemePreviewButtonProps> = ({
);
};
interface ThemePreviewButtonProps {
active?: boolean;
className?: string;
displayName: string;
theme: Theme;
interface ThemePreviewButtonProps extends ThemePreviewProps {
onSelect?: () => void;
}
const ThemePreviewButton: FC<ThemePreviewButtonProps> = ({
active,
beta,
className,
displayName,
theme,
@@ -141,7 +141,12 @@ const ThemePreviewButton: FC<ThemePreviewButtonProps> = ({
css={{ ...visuallyHidden }}
/>
<label htmlFor={displayName} className={className}>
<ThemePreview active={active} displayName={displayName} theme={theme} />
<ThemePreview
active={active}
beta={beta}
displayName={displayName}
theme={theme}
/>
</label>
</>
);
@@ -149,6 +154,7 @@ const ThemePreviewButton: FC<ThemePreviewButtonProps> = ({
interface ThemePreviewProps {
active?: boolean;
beta?: boolean;
className?: string;
displayName: string;
theme: Theme;
@@ -156,141 +162,146 @@ interface ThemePreviewProps {
const ThemePreview: FC<ThemePreviewProps> = ({
active,
beta,
className,
displayName,
theme,
}) => {
const styles = useMemo(
() =>
({
container: {
backgroundColor: theme.palette.background.default,
border: `1px solid ${theme.palette.divider}`,
width: 220,
color: theme.palette.text.primary,
borderRadius: 6,
overflow: "clip",
userSelect: "none",
},
containerActive: {
outline: `2px solid ${theme.experimental.roles.active.outline}`,
},
page: {
backgroundColor: theme.palette.background.default,
color: theme.palette.text.primary,
},
header: {
backgroundColor: theme.palette.background.paper,
display: "flex",
alignItems: "center",
justifyContent: "space-between",
padding: "6px 10px",
marginBottom: 8,
borderBottom: `1px solid ${theme.palette.divider}`,
},
headerLinks: {
display: "flex",
alignItems: "center",
gap: 6,
},
headerLink: {
backgroundColor: theme.palette.text.secondary,
height: 6,
width: 20,
borderRadius: 3,
},
activeHeaderLink: {
backgroundColor: theme.palette.text.primary,
},
proxy: {
backgroundColor: theme.palette.success.light,
height: 6,
width: 12,
borderRadius: 3,
},
user: {
backgroundColor: theme.palette.text.primary,
height: 8,
width: 8,
borderRadius: 4,
float: "right",
},
body: {
width: 120,
margin: "auto",
},
title: {
backgroundColor: theme.palette.text.primary,
height: 8,
width: 45,
borderRadius: 4,
marginBottom: 6,
},
table: {
border: `1px solid ${theme.palette.divider}`,
borderBottom: "none",
borderTopLeftRadius: 3,
borderTopRightRadius: 3,
overflow: "clip",
},
tableHeader: {
backgroundColor: theme.palette.background.paper,
height: 10,
margin: -1,
},
label: {
borderTop: `1px solid ${theme.palette.divider}`,
padding: "4px 12px",
fontSize: 14,
},
workspace: {
borderTop: `1px solid ${theme.palette.divider}`,
height: 15,
"&::after": {
content: '""',
display: "block",
marginTop: 4,
marginLeft: 4,
backgroundColor: theme.palette.text.disabled,
height: 6,
width: 30,
borderRadius: 3,
},
},
}) satisfies Record<string, Interpolation<never>>,
[theme],
);
return (
<div
css={[styles.container, active && styles.containerActive]}
className={className}
>
<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>
<ThemeOverride theme={theme}>
<div
css={[styles.container, active && styles.containerActive]}
className={className}
>
<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>
<div css={styles.headerLinks}>
<div css={styles.proxy}></div>
<div css={styles.user}></div>
</div>
</div>
<div css={styles.headerLinks}>
<div css={styles.proxy}></div>
<div css={styles.user}></div>
<div css={styles.body}>
<div css={styles.title}></div>
<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>
</div>
</div>
<div css={styles.body}>
<div css={styles.title}></div>
<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>
<div css={styles.label}>
<span>{displayName}</span>
{beta && <BetaBadge />}
</div>
</div>
<div css={styles.label}>{displayName}</div>
</div>
</ThemeOverride>
);
};
const styles = {
container: (theme) => ({
backgroundColor: theme.palette.background.default,
border: `1px solid ${theme.palette.divider}`,
width: 220,
color: theme.palette.text.primary,
borderRadius: 6,
overflow: "clip",
userSelect: "none",
}),
containerActive: (theme) => ({
outline: `2px solid ${theme.experimental.roles.active.outline}`,
}),
page: (theme) => ({
backgroundColor: theme.palette.background.default,
color: theme.palette.text.primary,
}),
header: (theme) => ({
backgroundColor: theme.palette.background.paper,
display: "flex",
alignItems: "center",
justifyContent: "space-between",
padding: "6px 10px",
marginBottom: 8,
borderBottom: `1px solid ${theme.palette.divider}`,
}),
headerLinks: {
display: "flex",
alignItems: "center",
gap: 6,
},
headerLink: (theme) => ({
backgroundColor: theme.palette.text.secondary,
height: 6,
width: 20,
borderRadius: 3,
}),
activeHeaderLink: (theme) => ({
backgroundColor: theme.palette.text.primary,
}),
proxy: (theme) => ({
backgroundColor: theme.palette.success.light,
height: 6,
width: 12,
borderRadius: 3,
}),
user: (theme) => ({
backgroundColor: theme.palette.text.primary,
height: 8,
width: 8,
borderRadius: 4,
float: "right",
}),
body: {
width: 120,
margin: "auto",
},
title: (theme) => ({
backgroundColor: theme.palette.text.primary,
height: 8,
width: 45,
borderRadius: 4,
marginBottom: 6,
}),
table: (theme) => ({
border: `1px solid ${theme.palette.divider}`,
borderBottom: "none",
borderTopLeftRadius: 3,
borderTopRightRadius: 3,
overflow: "clip",
}),
tableHeader: (theme) => ({
backgroundColor: theme.palette.background.paper,
height: 10,
margin: -1,
}),
label: (theme) => ({
display: "flex",
alignItems: "center",
justifyContent: "space-between",
borderTop: `1px solid ${theme.palette.divider}`,
padding: "4px 12px",
fontSize: 14,
}),
workspace: (theme) => ({
borderTop: `1px solid ${theme.palette.divider}`,
height: 15,
"&::after": {
content: '""',
display: "block",
marginTop: 4,
marginLeft: 4,
backgroundColor: theme.palette.text.disabled,
height: 6,
width: 30,
borderRadius: 3,
},
}),
} satisfies Record<string, Interpolation<Theme>>;
@@ -6,7 +6,7 @@ import { AppearancePage } from "./AppearancePage";
import { MockUser } from "testHelpers/entities";
describe("appearance page", () => {
it("changes theme to dark", async () => {
it("does nothing when selecting current theme", async () => {
renderWithAuth(<AppearancePage />);
jest.spyOn(API, "updateAppearanceSettings").mockResolvedValueOnce({
@@ -18,10 +18,7 @@ describe("appearance page", () => {
await userEvent.click(dark);
// Check if the API was called correctly
expect(API.updateAppearanceSettings).toBeCalledTimes(1);
expect(API.updateAppearanceSettings).toHaveBeenCalledWith("me", {
theme_preference: "dark",
});
expect(API.updateAppearanceSettings).toBeCalledTimes(0);
});
it("changes theme to dark blue", async () => {
@@ -41,4 +38,22 @@ describe("appearance page", () => {
theme_preference: "darkBlue",
});
});
it("changes theme to light", async () => {
renderWithAuth(<AppearancePage />);
jest.spyOn(API, "updateAppearanceSettings").mockResolvedValueOnce({
...MockUser,
theme_preference: "light",
});
const light = await screen.findByText("Light");
await userEvent.click(light);
// Check if the API was called correctly
expect(API.updateAppearanceSettings).toBeCalledTimes(1);
expect(API.updateAppearanceSettings).toHaveBeenCalledWith("me", {
theme_preference: "light",
});
});
});
@@ -26,6 +26,16 @@ type Story = StoryObj<typeof ExternalAuthPageView>;
export const NoProviders: Story = {};
export const NoIcon: Story = {
args: {
...meta.args,
auths: {
providers: [{ ...MockGithubExternalProvider, display_icon: "" }],
links: [MockGithubAuthLink],
},
},
};
export const Authenticated: Story = {
args: {
...meta.args,
@@ -36,7 +46,7 @@ export const Authenticated: Story = {
},
};
export const UnAuthenticated: Story = {
export const Unauthenticated: Story = {
args: {
...meta.args,
auths: {
@@ -131,9 +131,8 @@ const ExternalAuthRow: FC<ExternalAuthRowProps> = ({
<TableCell>
<AvatarData
title={app.display_name || app.id}
// subtitle={template.description}
avatar={
app.display_icon !== "" && (
app.display_icon && (
<Avatar src={app.display_icon} variant="square" fitImage />
)
}
@@ -150,6 +149,7 @@ const ExternalAuthRow: FC<ExternalAuthRowProps> = ({
message={authenticated ? "Authenticated" : "Click to Login"}
externalAuthPollingState={externalAuthPollingState}
startPollingExternalAuth={startPollingExternalAuth}
fullWidth={false}
/>
</TableCell>
<TableCell>
@@ -11,7 +11,8 @@ const meta: Meta<typeof SSHKeysPageView> = {
user_id: "test-user-id",
created_at: "2022-07-28T07:45:50.795918897Z",
updated_at: "2022-07-28T07:45:50.795919142Z",
public_key: "SSH-Key",
public_key:
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICnKzATuWwmmt5+CKTPuRGN0R1PBemA+6/SStpLiyX+L",
},
},
};
@@ -14,18 +14,17 @@
* users like that, though, know that it will be painful
*/
import { useTheme } from "@emotion/react";
import { type User, type Role } from "api/typesGenerated";
import { EditRolesButton } from "./EditRolesButton";
import { Pill } from "components/Pill/Pill";
import TableCell from "@mui/material/TableCell";
import Stack from "@mui/material/Stack";
import { type FC } from "react";
import { type User, type Role } from "api/typesGenerated";
import { Pill } from "components/Pill/Pill";
import {
Popover,
PopoverTrigger,
PopoverContent,
} from "components/Popover/Popover";
import { EditRolesButton } from "./EditRolesButton";
type UserRoleCellProps = {
canEditUsers: boolean;
@@ -36,14 +35,14 @@ type UserRoleCellProps = {
onUserRolesUpdate: (user: User, newRoleNames: string[]) => void;
};
export function UserRoleCell({
export const UserRoleCell: FC<UserRoleCellProps> = ({
canEditUsers,
allAvailableRoles,
user,
isLoading,
oidcRoleSyncEnabled,
onUserRolesUpdate,
}: UserRoleCellProps) {
}) => {
const theme = useTheme();
const [mainDisplayRole = fallbackRole, ...extraRoles] =
@@ -75,11 +74,11 @@ export function UserRoleCell({
text={mainDisplayRole.display_name}
css={{
backgroundColor: hasOwnerRole
? theme.palette.info.dark
: theme.palette.background.paper,
? theme.experimental.roles.info.background
: theme.experimental.l2.background,
borderColor: hasOwnerRole
? theme.palette.info.light
: theme.palette.divider,
? theme.experimental.roles.info.outline
: theme.experimental.l2.outline,
}}
/>
@@ -87,13 +86,13 @@ export function UserRoleCell({
</Stack>
</TableCell>
);
}
};
type OverflowRolePillProps = {
roles: readonly Role[];
};
function OverflowRolePill({ roles }: OverflowRolePillProps) {
const OverflowRolePill: FC<OverflowRolePillProps> = ({ roles }) => {
const theme = useTheme();
return (
@@ -144,7 +143,7 @@ function OverflowRolePill({ roles }: OverflowRolePillProps) {
</PopoverContent>
</Popover>
);
}
};
const fallbackRole: Role = {
name: "member",
@@ -4,6 +4,10 @@ import {
MockAssignableSiteRoles,
MockAuthMethodsPasswordOnly,
MockGroup,
MockUserAdminRole,
MockTemplateAdminRole,
MockMemberRole,
MockAuditorRole,
} from "testHelpers/entities";
import { UsersTable } from "./UsersTable";
import type { Meta, StoryObj } from "@storybook/react";
@@ -43,7 +47,12 @@ export const Editable: Story = {
...MockUser,
username: "John Doe",
email: "john.doe@coder.com",
roles: [],
roles: [
MockUserAdminRole,
MockTemplateAdminRole,
MockMemberRole,
MockAuditorRole,
],
status: "dormant",
},
{
@@ -32,7 +32,7 @@ export const SidebarItem: FC<SidebarItemProps> = ({
return (
<button
css={(theme) => ({
background: active ? theme.colors.gray[13] : "none",
background: active ? theme.experimental.l2.background : "none",
border: "none",
fontSize: 14,
width: "100%",
@@ -173,18 +173,18 @@ const styles = {
orphanContainer: (theme) => ({
marginTop: 24,
display: "flex",
backgroundColor: theme.colors.orange[15],
backgroundColor: theme.experimental.roles.danger.background,
justifyContent: "space-between",
border: `1px solid ${theme.colors.orange[11]}`,
border: `1px solid ${theme.experimental.roles.danger.outline}`,
borderRadius: 8,
padding: 12,
gap: 8,
lineHeight: "18px",
"& .option": {
color: theme.colors.orange[11],
color: theme.experimental.roles.danger.fill,
"&.Mui-checked": {
color: theme.colors.orange[11],
color: theme.experimental.roles.danger.fill,
},
},
@@ -25,13 +25,13 @@ import {
defaultSchedule,
emptySchedule,
} from "pages/WorkspaceSettingsPage/WorkspaceSchedulePage/schedule";
import { ChangeEvent, FC } from "react";
import { type ChangeEvent, type FC } from "react";
import * as Yup from "yup";
import { getFormHelpers } from "utils/formUtils";
import { timeZones } from "utils/timeZones";
import { Pill } from "components/Pill/Pill";
import Tooltip from "@mui/material/Tooltip";
import { formatDuration, intervalToDuration } from "date-fns";
import { DisabledBadge } from "components/Badges/Badges";
// REMARK: some plugins depend on utc, so it's listed first. Otherwise they're
// sorted alphabetically.
@@ -290,7 +290,7 @@ export const WorkspaceScheduleForm: FC<
</div>
{!enableAutoStart && (
<Tooltip title="This option can be enabled in the template settings">
<Pill text="Disabled" />
<DisabledBadge />
</Tooltip>
)}
</>
@@ -378,7 +378,7 @@ export const WorkspaceScheduleForm: FC<
</div>
{!enableAutoStop && (
<Tooltip title="This option can be enabled in the template settings">
<Pill text="Disabled" />
<DisabledBadge />
</Tooltip>
)}
</>
+13
View File
@@ -0,0 +1,13 @@
export const chromatic = {
modes: {
dark: { theme: "dark" },
light: { theme: "light" },
},
};
export const chromaticWithTablet = {
modes: {
"dark desktop": { theme: "dark" },
"light tablet": { theme: "light", viewport: "ipad" },
},
};
+8 -8
View File
@@ -2933,7 +2933,7 @@ export const MockHealth: TypesGen.HealthcheckReport = {
created_at: "2023-05-01T19:15:56.606593Z",
updated_at: "2023-12-05T14:13:36.647535Z",
deleted: false,
version: "v2.4.0-devel+5fad61102",
version: "v2.5.0-devel+5fad61102",
},
{
id: "9d786ce0-55b1-4ace-8acc-a4672ff8d41f",
@@ -2956,7 +2956,7 @@ export const MockHealth: TypesGen.HealthcheckReport = {
created_at: "2023-05-01T20:34:11.114005Z",
updated_at: "2023-12-05T14:13:45.941716Z",
deleted: false,
version: "v2.4.0-devel+5fad61102",
version: "v2.5.0-devel+5fad61102",
},
{
id: "2e209786-73b1-4838-ba78-e01c9334450a",
@@ -2979,7 +2979,7 @@ export const MockHealth: TypesGen.HealthcheckReport = {
created_at: "2023-05-01T20:41:02.76448Z",
updated_at: "2023-12-05T14:13:41.968568Z",
deleted: false,
version: "v2.4.0-devel+5fad61102",
version: "v2.5.0-devel+5fad61102",
},
{
id: "c272e80c-0cce-49d6-9782-1b5cf90398e8",
@@ -3050,7 +3050,7 @@ export const MockHealth: TypesGen.HealthcheckReport = {
created_at: "2023-12-01T09:21:15.996267Z",
updated_at: "2023-12-05T14:13:59.663174Z",
deleted: false,
version: "v2.4.0-devel+5fad61102",
version: "v2.5.0-devel+5fad61102",
},
{
id: "72649dc9-03c7-46a8-bc95-96775e93ddc1",
@@ -3073,7 +3073,7 @@ export const MockHealth: TypesGen.HealthcheckReport = {
created_at: "2023-12-01T09:23:44.505529Z",
updated_at: "2023-12-05T14:13:55.769058Z",
deleted: false,
version: "v2.4.0-devel+5fad61102",
version: "v2.5.0-devel+5fad61102",
},
{
id: "1f78398f-e5ae-4c38-aa89-30222181d443",
@@ -3096,12 +3096,12 @@ export const MockHealth: TypesGen.HealthcheckReport = {
created_at: "2023-12-01T09:36:00.231252Z",
updated_at: "2023-12-05T14:13:47.015031Z",
deleted: false,
version: "v2.4.0-devel+5fad61102",
version: "v2.5.0-devel+5fad61102",
},
],
},
},
coder_version: "v0.27.1-devel+c575292",
coder_version: "v2.5.0-devel+5fad61102",
};
export const MockListeningPortsResponse: TypesGen.WorkspaceAgentListeningPortsResponse =
@@ -3184,7 +3184,7 @@ export const DeploymentHealthUnhealthy: TypesGen.HealthcheckReport = {
created_at: "2023-11-23T15:37:25.513213Z",
updated_at: "2023-11-23T18:09:19.734747Z",
deleted: false,
version: "v2.4.0-devel+89bae7eff",
version: "v2.5.0-devel+89bae7eff",
},
],
},
+2 -2
View File
@@ -7,7 +7,7 @@ import {
import { type ReactNode, useState } from "react";
import { QueryClient } from "react-query";
import { AppProviders } from "App";
import { ThemeProviders } from "contexts/ThemeProviders";
import { ThemeProvider } from "contexts/ThemeProvider";
import { DashboardLayout } from "components/Dashboard/DashboardLayout";
import { RequireAuth } from "components/RequireAuth/RequireAuth";
import { TemplateSettingsLayout } from "pages/TemplateSettingsPage/TemplateSettingsLayout";
@@ -265,6 +265,6 @@ export const waitForLoaderToBeRemoved = async (): Promise<void> => {
export const renderComponent = (component: React.ReactElement) => {
return tlRender(component, {
wrapper: ({ children }) => <ThemeProviders>{children}</ThemeProviders>,
wrapper: ({ children }) => <ThemeProvider>{children}</ThemeProvider>,
});
};
+24 -18
View File
@@ -3,41 +3,41 @@ import colors from "../tailwind";
export default {
l1: {
background: colors.gray[950],
outline: colors.gray[700],
fill: colors.gray[600],
background: colors.zinc[950],
outline: colors.zinc[700],
fill: colors.zinc[600],
text: colors.white,
},
l2: {
background: colors.gray[900],
outline: colors.gray[700],
fill: "#f00",
text: colors.white,
background: colors.zinc[900],
outline: colors.zinc[700],
fill: colors.zinc[500],
text: colors.zinc[50],
disabled: {
background: "#f00",
outline: "#f00",
fill: "#f00",
text: colors.gray[200],
fill: colors.zinc[500],
text: colors.zinc[200],
},
hover: {
background: "#f00",
outline: "#f00",
background: colors.zinc[800],
outline: colors.zinc[600],
fill: "#f00",
text: colors.white,
},
},
l3: {
background: colors.gray[800],
outline: colors.gray[700],
fill: colors.gray[600],
background: colors.zinc[800],
outline: colors.zinc[700],
fill: colors.zinc[600],
text: colors.white,
disabled: {
background: "#f00",
outline: "#f00",
fill: "#f00",
text: colors.gray[200],
text: colors.zinc[200],
},
hover: {
background: "#f00",
@@ -51,7 +51,7 @@ export default {
danger: {
background: colors.orange[950],
outline: colors.orange[500],
fill: colors.orange[600],
fill: colors.orange[700],
text: colors.orange[50],
disabled: {
background: colors.orange[950],
@@ -68,8 +68,8 @@ export default {
},
error: {
background: colors.red[950],
outline: colors.red[500],
fill: colors.red[600],
outline: colors.red[600],
fill: colors.red[400],
text: colors.red[50],
},
warning: {
@@ -126,5 +126,11 @@ export default {
text: colors.white,
},
},
preview: {
background: colors.violet[950],
outline: colors.violet[500],
fill: colors.violet[400],
text: colors.violet[50],
},
},
} satisfies NewTheme;
+2
View File
@@ -1,9 +1,11 @@
import colors from "./colors";
import experimental from "./experimental";
import monaco from "./monaco";
import muiTheme from "./mui";
export default {
...muiTheme,
colors,
experimental,
monaco,
};
+37
View File
@@ -0,0 +1,37 @@
import muiTheme from "./mui";
import type * as monaco from "monaco-editor";
export default {
base: "vs-dark",
inherit: true,
rules: [
{
token: "comment",
foreground: "6B737C",
},
{
token: "type",
foreground: "B392F0",
},
{
token: "string",
foreground: "9DB1C5",
},
{
token: "variable",
foreground: "DDDDDD",
},
{
token: "identifier",
foreground: "B392F0",
},
{
token: "delimiter.curly",
foreground: "EBB325",
},
],
colors: {
"editor.foreground": muiTheme.palette.text.primary,
"editor.background": muiTheme.palette.background.paper,
},
} satisfies monaco.editor.IStandaloneThemeData as monaco.editor.IStandaloneThemeData;
+9 -7
View File
@@ -1,4 +1,5 @@
import colors from "./colors";
// eslint-disable-next-line no-restricted-imports -- We need MUI here
import { alertClasses } from "@mui/material/Alert";
import { createTheme, type ThemeOptions } from "@mui/material/styles";
import {
BODY_FONT_FAMILY,
@@ -8,17 +9,17 @@ import {
BUTTON_SM_HEIGHT,
BUTTON_XL_HEIGHT,
} from "../constants";
// eslint-disable-next-line no-restricted-imports -- We need MUI here
import { alertClasses } from "@mui/material/Alert";
import tw from "../tailwind";
import colors from "./colors";
let muiTheme = createTheme({
palette: {
mode: "dark",
primary: {
main: colors.blue[7],
contrastText: colors.blue[1],
light: colors.blue[6],
dark: colors.blue[9],
main: tw.sky[500],
contrastText: tw.sky[50],
light: tw.sky[300],
dark: tw.sky[400],
},
secondary: {
main: colors.gray[11],
@@ -489,6 +490,7 @@ muiTheme = createTheme(muiTheme, {
lineHeight: "150%",
borderRadius: 4,
background: muiTheme.palette.divider,
padding: "8px 16px",
},
},
},
+11 -5
View File
@@ -12,17 +12,17 @@ export default {
l2: {
background: colors.gray[900],
outline: colors.gray[700],
fill: "#f00",
text: colors.white,
fill: colors.gray[500],
text: colors.gray[50],
disabled: {
background: "#f00",
outline: "#f00",
fill: "#f00",
fill: colors.gray[500],
text: colors.gray[200],
},
hover: {
background: "#f00",
outline: "#f00",
outline: colors.gray[600],
fill: "#f00",
text: colors.white,
},
@@ -50,7 +50,7 @@ export default {
roles: {
danger: {
background: colors.orange[950],
outline: colors.orange[500],
outline: colors.orange[600],
fill: colors.orange[600],
text: colors.orange[50],
disabled: {
@@ -126,5 +126,11 @@ export default {
text: colors.white,
},
},
preview: {
background: colors.violet[950],
outline: colors.violet[500],
fill: colors.violet[400],
text: colors.violet[50],
},
},
} satisfies NewTheme;
+2
View File
@@ -1,9 +1,11 @@
import colors from "./colors";
import experimental from "./experimental";
import monaco from "./monaco";
import muiTheme from "./mui";
export default {
...muiTheme,
colors,
experimental,
monaco,
};
+37
View File
@@ -0,0 +1,37 @@
import muiTheme from "./mui";
import type * as monaco from "monaco-editor";
export default {
base: "vs-dark",
inherit: true,
rules: [
{
token: "comment",
foreground: "6B737C",
},
{
token: "type",
foreground: "B392F0",
},
{
token: "string",
foreground: "9DB1C5",
},
{
token: "variable",
foreground: "DDDDDD",
},
{
token: "identifier",
foreground: "B392F0",
},
{
token: "delimiter.curly",
foreground: "EBB325",
},
],
colors: {
"editor.foreground": muiTheme.palette.text.primary,
"editor.background": muiTheme.palette.background.paper,
},
} satisfies monaco.editor.IStandaloneThemeData as monaco.editor.IStandaloneThemeData;
+1
View File
@@ -489,6 +489,7 @@ muiTheme = createTheme(muiTheme, {
lineHeight: "150%",
borderRadius: 4,
background: muiTheme.palette.divider,
padding: "8px 16px",
},
},
},
+1
View File
@@ -19,6 +19,7 @@ export interface NewTheme {
info: Role; // just sharing :)
success: InteractiveRole; // yay!! it's working!!
active: InteractiveRole; // selected items, focused inputs, in progress
preview: Role; // experiments, alpha/beta features
};
}
+5 -2
View File
@@ -1,20 +1,23 @@
import type { Theme as MuiTheme } from "@mui/material/styles";
import type * as monaco from "monaco-editor";
import dark from "./dark";
import darkBlue from "./darkBlue";
import light from "./light";
import type { NewTheme } from "./experimental";
import type { Colors } from "./colors";
export interface Theme extends MuiTheme {
colors: Colors;
experimental: NewTheme;
monaco: monaco.editor.IStandaloneThemeData;
}
export const DEFAULT_THEME = "auto";
export const DEFAULT_THEME = "dark";
const theme = {
dark,
darkBlue,
light: dark,
light,
} satisfies Record<string, Theme>;
export default theme;
+62
View File
@@ -0,0 +1,62 @@
import tw from "../tailwind";
export default {
white: "#fff",
gray: {
17: tw.zinc[950],
16: tw.zinc[900],
14: tw.zinc[800],
13: tw.zinc[700],
12: tw.zinc[600],
11: tw.zinc[500],
9: tw.zinc[400],
6: tw.zinc[300],
4: tw.zinc[200],
2: tw.zinc[100],
1: tw.zinc[50],
},
red: {
15: tw.red[950],
12: tw.red[800],
10: tw.red[700],
9: tw.red[600],
8: tw.red[500],
6: tw.red[400],
2: tw.red[50],
},
orange: {
15: tw.amber[950],
14: tw.amber[900],
12: tw.amber[800],
11: tw.amber[700],
10: tw.amber[600],
9: tw.amber[500],
7: tw.amber[400],
},
yellow: {
5: tw.yellow[300],
},
green: {
15: tw.green[950],
13: tw.green[700],
12: tw.green[600],
11: tw.green[500],
9: tw.green[400],
8: tw.green[300],
},
blue: {
14: tw.blue[950],
9: tw.blue[600],
8: tw.blue[500],
7: tw.blue[400],
6: tw.blue[300],
3: tw.blue[200],
1: tw.blue[50],
},
};
+136
View File
@@ -0,0 +1,136 @@
import { type NewTheme } from "../experimental";
import colors from "../tailwind";
export default {
l1: {
background: colors.gray[50],
outline: colors.gray[300],
fill: colors.gray[700],
text: colors.black,
},
l2: {
background: colors.gray[100],
outline: colors.gray[500],
fill: colors.gray[500],
text: colors.gray[950],
disabled: {
background: "#f00",
outline: "#f00",
fill: colors.gray[500],
text: colors.gray[200],
},
hover: {
background: colors.gray[200],
outline: colors.gray[700],
fill: "#f00",
text: colors.black,
},
},
l3: {
background: colors.gray[200],
outline: colors.gray[700],
fill: colors.gray[600],
text: colors.black,
disabled: {
background: "#f00",
outline: "#f00",
fill: "#f00",
text: colors.gray[200],
},
hover: {
background: "#f00",
outline: "#f00",
fill: "#f00",
text: colors.black,
},
},
roles: {
danger: {
background: colors.orange[50],
outline: colors.orange[400],
fill: colors.orange[600],
text: colors.orange[950],
disabled: {
background: colors.orange[50],
outline: colors.orange[800],
fill: colors.orange[800],
text: colors.orange[800],
},
hover: {
background: colors.orange[100],
outline: colors.orange[500],
fill: colors.orange[500],
text: colors.black,
},
},
error: {
background: colors.red[100],
outline: colors.red[500],
fill: colors.red[600],
text: colors.red[950],
},
warning: {
background: colors.amber[50],
outline: colors.amber[300],
fill: "#f00",
text: colors.amber[950],
},
notice: {
background: colors.yellow[50],
outline: colors.yellow[600],
fill: colors.yellow[500],
text: colors.yellow[950],
},
info: {
background: colors.blue[50],
outline: colors.blue[400],
fill: colors.blue[600],
text: colors.blue[950],
},
success: {
background: colors.green[50],
outline: colors.green[500],
fill: colors.green[600],
text: colors.green[950],
disabled: {
background: colors.green[50],
outline: colors.green[800],
fill: colors.green[800],
text: colors.green[800],
},
hover: {
background: colors.green[100],
outline: colors.green[500],
fill: colors.green[500],
text: colors.black,
},
},
active: {
background: colors.sky[100],
outline: colors.sky[500],
fill: colors.sky[600],
text: colors.sky[950],
disabled: {
background: colors.sky[50],
outline: colors.sky[800],
fill: colors.sky[800],
text: colors.sky[200],
},
hover: {
background: colors.sky[200],
outline: colors.sky[400],
fill: colors.sky[500],
text: colors.black,
},
},
preview: {
background: colors.violet[50],
outline: colors.violet[500],
fill: colors.violet[600],
text: colors.violet[950],
},
},
} satisfies NewTheme;
+11
View File
@@ -0,0 +1,11 @@
import colors from "./colors";
import experimental from "./experimental";
import monaco from "./monaco";
import muiTheme from "./mui";
export default {
...muiTheme,
colors,
experimental,
monaco,
};
+37
View File
@@ -0,0 +1,37 @@
import muiTheme from "./mui";
import type * as monaco from "monaco-editor";
export default {
base: "vs",
inherit: true,
rules: [
{
token: "comment",
foreground: "6B737C",
},
{
token: "type",
foreground: "682CD7",
},
{
token: "string",
foreground: "1766B4",
},
{
token: "variable",
foreground: "444444",
},
{
token: "identifier",
foreground: "682CD7",
},
{
token: "delimiter.curly",
foreground: "EBB325",
},
],
colors: {
"editor.foreground": muiTheme.palette.text.primary,
"editor.background": muiTheme.palette.background.paper,
},
} satisfies monaco.editor.IStandaloneThemeData as monaco.editor.IStandaloneThemeData;
+576
View File
@@ -0,0 +1,576 @@
// eslint-disable-next-line no-restricted-imports -- We need MUI here
import { alertClasses } from "@mui/material/Alert";
import { createTheme, type ThemeOptions } from "@mui/material/styles";
import {
BODY_FONT_FAMILY,
borderRadius,
BUTTON_LG_HEIGHT,
BUTTON_MD_HEIGHT,
BUTTON_SM_HEIGHT,
BUTTON_XL_HEIGHT,
} from "../constants";
import tw from "../tailwind";
let muiTheme = createTheme({
palette: {
mode: "light",
primary: {
main: tw.sky[600],
contrastText: tw.sky[50],
light: tw.sky[400],
dark: tw.sky[500],
},
secondary: {
main: tw.zinc[500],
contrastText: tw.zinc[800],
dark: tw.zinc[600],
},
background: {
default: tw.zinc[50],
paper: tw.zinc[100],
},
text: {
primary: tw.zinc[950],
secondary: tw.zinc[700],
disabled: tw.zinc[600],
},
divider: tw.zinc[200],
warning: {
light: tw.amber[500],
main: tw.amber[800],
dark: tw.amber[950],
},
success: {
main: tw.green[500],
dark: tw.green[600],
},
info: {
light: tw.blue[400],
main: tw.blue[600],
dark: tw.blue[950],
contrastText: tw.zinc[200],
},
error: {
light: tw.red[400],
main: tw.red[500],
dark: tw.red[950],
contrastText: tw.zinc[800],
},
action: {
hover: tw.zinc[100],
},
neutral: {
main: tw.zinc[950],
},
},
typography: {
fontFamily: BODY_FONT_FAMILY,
body1: {
fontSize: "1rem" /* 16px at default scaling */,
lineHeight: "160%",
},
body2: {
fontSize: "0.875rem" /* 14px at default scaling */,
lineHeight: "160%",
},
},
shape: {
borderRadius,
},
});
muiTheme = createTheme(muiTheme, {
components: {
MuiCssBaseline: {
styleOverrides: `
html, body, #root, #storybook-root {
height: 100%;
}
button, input {
font-family: ${BODY_FONT_FAMILY};
}
input:-webkit-autofill,
input:-webkit-autofill:hover,
input:-webkit-autofill:focus,
input:-webkit-autofill:active {
-webkit-box-shadow: 0 0 0 100px ${muiTheme.palette.background.default} inset !important;
}
::placeholder {
color: ${muiTheme.palette.text.disabled};
}
`,
},
MuiAvatar: {
styleOverrides: {
root: {
width: 36,
height: 36,
fontSize: 18,
"& .MuiSvgIcon-root": {
width: "50%",
},
},
colorDefault: {
backgroundColor: tw.zinc[700],
},
},
},
// Button styles are based on
// https://tailwindui.com/components/application-ui/elements/buttons
MuiButtonBase: {
defaultProps: {
disableRipple: true,
},
},
MuiButton: {
defaultProps: {
variant: "outlined",
color: "neutral",
},
styleOverrides: {
root: {
textTransform: "none",
letterSpacing: "normal",
fontWeight: 500,
height: BUTTON_MD_HEIGHT,
padding: "8px 16px",
borderRadius: "6px",
fontSize: 14,
whiteSpace: "nowrap",
":focus-visible": {
outline: `2px solid ${muiTheme.palette.primary.main}`,
},
"& .MuiLoadingButton-loadingIndicator": {
width: 14,
height: 14,
},
"& .MuiLoadingButton-loadingIndicator .MuiCircularProgress-root": {
width: "inherit !important",
height: "inherit !important",
},
},
sizeSmall: {
height: BUTTON_SM_HEIGHT,
},
sizeLarge: {
height: BUTTON_LG_HEIGHT,
},
sizeXlarge: {
height: BUTTON_XL_HEIGHT,
},
outlined: {
boxShadow: "0 1px 4px #0001",
":hover": {
boxShadow: "0 1px 4px #0001",
border: `1px solid ${tw.zinc[500]}`,
},
"&.Mui-disabled": {
boxShadow: "none !important",
},
},
outlinedNeutral: {
borderColor: tw.zinc[300],
"&.Mui-disabled": {
borderColor: tw.zinc[200],
color: tw.zinc[500],
"& > .MuiLoadingButton-loadingIndicator": {
color: tw.zinc[500],
},
},
},
contained: {
boxShadow: "0 1px 4px #0001",
"&.Mui-disabled": {
boxShadow: "none !important",
},
":hover": {
boxShadow: "0 1px 4px #0001",
},
},
containedNeutral: {
backgroundColor: tw.zinc[100],
border: `1px solid ${tw.zinc[200]}`,
"&.Mui-disabled": {
backgroundColor: tw.zinc[50],
border: `1px solid ${tw.zinc[100]}`,
},
"&:hover": {
backgroundColor: tw.zinc[200],
border: `1px solid ${tw.zinc[300]}`,
},
},
iconSizeMedium: {
"& > .MuiSvgIcon-root": {
fontSize: 14,
},
},
iconSizeSmall: {
"& > .MuiSvgIcon-root": {
fontSize: 13,
},
},
startIcon: {
marginLeft: "-2px",
},
},
},
MuiButtonGroup: {
styleOverrides: {
root: {
">button:hover+button": {
// The !important is unfortunate, but necessary for the border.
borderLeftColor: `${tw.zinc[300]} !important`,
},
},
},
},
MuiLoadingButton: {
defaultProps: {
variant: "outlined",
color: "neutral",
},
},
MuiTableContainer: {
styleOverrides: {
root: {
borderRadius,
border: `1px solid ${muiTheme.palette.divider}`,
},
},
},
MuiTable: {
styleOverrides: {
root: ({ theme }) => ({
borderCollapse: "unset",
border: "none",
boxShadow: `0 0 0 1px ${muiTheme.palette.background.default} inset`,
overflow: "hidden",
"& td": {
paddingTop: 16,
paddingBottom: 16,
background: "transparent",
},
[theme.breakpoints.down("md")]: {
minWidth: 1000,
},
}),
},
},
MuiTableCell: {
styleOverrides: {
head: {
fontSize: 14,
color: muiTheme.palette.text.secondary,
fontWeight: 600,
background: muiTheme.palette.background.paper,
},
root: {
fontSize: 16,
background: muiTheme.palette.background.paper,
borderBottom: `1px solid ${muiTheme.palette.divider}`,
padding: "12px 8px",
// This targets the first+last td elements, and also the first+last elements
// of a TableCellLink.
"&:not(:only-child):first-of-type, &:not(:only-child):first-of-type > a":
{
paddingLeft: 32,
},
"&:not(:only-child):last-child, &:not(:only-child):last-child > a": {
paddingRight: 32,
},
},
},
},
MuiTableRow: {
styleOverrides: {
root: {
"&:last-child .MuiTableCell-body": {
borderBottom: 0,
},
},
},
},
MuiLink: {
defaultProps: {
underline: "hover",
},
},
MuiPaper: {
defaultProps: {
elevation: 0,
},
styleOverrides: {
root: {
border: `1px solid ${muiTheme.palette.divider}`,
backgroundImage: "none",
},
},
},
MuiSkeleton: {
styleOverrides: {
root: {
backgroundColor: muiTheme.palette.divider,
},
},
},
MuiLinearProgress: {
styleOverrides: {
root: {
borderRadius: 999,
},
},
},
MuiChip: {
styleOverrides: {
root: {
backgroundColor: tw.zinc[400],
},
},
},
MuiMenu: {
defaultProps: {
anchorOrigin: {
vertical: "bottom",
horizontal: "right",
},
transformOrigin: {
vertical: "top",
horizontal: "right",
},
},
styleOverrides: {
paper: {
marginTop: 8,
borderRadius: 4,
padding: "4px 0",
minWidth: 160,
},
root: {
// It should be the same as the menu padding
"& .MuiDivider-root": {
marginTop: `4px !important`,
marginBottom: `4px !important`,
},
},
},
},
MuiMenuItem: {
styleOverrides: {
root: {
gap: 12,
"& .MuiSvgIcon-root": {
fontSize: 20,
},
},
},
},
MuiSnackbar: {
styleOverrides: {
anchorOriginBottomRight: {
bottom: `${24 + 36}px !important`, // 36 is the bottom bar height
},
},
},
MuiSnackbarContent: {
styleOverrides: {
root: {
borderRadius: "4px !important",
},
},
},
MuiTextField: {
defaultProps: {
InputLabelProps: {
shrink: true,
},
},
},
MuiInputBase: {
defaultProps: {
color: "primary",
},
styleOverrides: {
root: {
height: BUTTON_LG_HEIGHT,
},
sizeSmall: {
height: BUTTON_MD_HEIGHT,
fontSize: 14,
},
multiline: {
height: "auto",
},
colorPrimary: {
// Same as button
"& .MuiOutlinedInput-notchedOutline": {
borderColor: tw.zinc[300],
},
// The default outlined input color is white, which seemed jarring.
"&:hover:not(.Mui-error):not(.Mui-focused) .MuiOutlinedInput-notchedOutline":
{
borderColor: tw.zinc[500],
},
},
},
},
MuiFormHelperText: {
defaultProps: {
sx: {
marginLeft: 0,
marginTop: 1,
},
},
},
MuiRadio: {
defaultProps: {
disableRipple: true,
},
},
MuiCheckbox: {
styleOverrides: {
root: {
/**
* Adds focus styling to checkboxes (which doesn't exist normally, for
* some reason?).
*
* The checkbox component is a root span with a checkbox input inside
* it. MUI does not allow you to use selectors like (& input) to
* target the inner checkbox (even though you can use & td to style
* tables). Tried every combination of selector possible (including
* lots of !important), and the main issue seems to be that the
* styling just never gets processed for it to get injected into the
* CSSOM.
*
* Had to settle for adding styling to the span itself (which does
* make the styling more obvious, even if there's not much room for
* customization).
*/
"&.Mui-focusVisible": {
boxShadow: `0 0 0 2px ${tw.blue[600]}`,
},
"&.Mui-disabled": {
color: tw.zinc[500],
},
},
},
},
MuiSwitch: {
defaultProps: { color: "primary" },
styleOverrides: {
root: {
".Mui-focusVisible .MuiSwitch-thumb": {
// Had to thicken outline to make sure that the focus color didn't
// bleed into the thumb and was still easily-visible
boxShadow: `0 0 0 3px ${tw.blue[600]}`,
},
},
},
},
MuiAutocomplete: {
styleOverrides: {
root: {
// Not sure why but since the input has padding we don't need it here
"& .MuiInputBase-root": {
padding: 0,
},
},
},
},
MuiList: {
defaultProps: {
disablePadding: true,
},
},
MuiTabs: {
defaultProps: {
textColor: "primary",
indicatorColor: "primary",
},
},
MuiTooltip: {
styleOverrides: {
tooltip: {
lineHeight: "150%",
borderRadius: 4,
background: muiTheme.palette.background.paper,
color: muiTheme.palette.secondary.contrastText,
border: `1px solid ${muiTheme.palette.divider}`,
padding: "8px 16px",
boxShadow: "0 1px 4px #0001",
},
},
},
MuiAlert: {
defaultProps: {
variant: "outlined",
},
styleOverrides: {
root: ({ theme }) => ({
background: theme.palette.background.paper,
}),
action: {
paddingTop: 2, // Idk why it is not aligned as expected
},
icon: {
fontSize: 16,
marginTop: "4px", // The size of text is 24 so (24 - 16)/2 = 4
},
message: ({ theme }) => ({
color: theme.palette.text.primary,
}),
outlinedWarning: {
[`& .${alertClasses.icon}`]: {
color: muiTheme.palette.warning.light,
},
},
outlinedInfo: {
[`& .${alertClasses.icon}`]: {
color: muiTheme.palette.primary.light,
},
},
outlinedError: {
[`& .${alertClasses.icon}`]: {
color: muiTheme.palette.error.light,
},
},
},
},
MuiAlertTitle: {
styleOverrides: {
root: {
fontSize: "inherit",
marginBottom: 0,
},
},
},
MuiIconButton: {
styleOverrides: {
root: {
"&.Mui-focusVisible": {
boxShadow: `0 0 0 2px ${tw.blue[600]}`,
},
},
},
},
},
} as ThemeOptions);
export default muiTheme;
+4 -4
View File
@@ -1,16 +1,16 @@
import { Theme } from "@mui/material/styles";
import type { Theme } from "@emotion/react";
export const getLatencyColor = (theme: Theme, latency?: number) => {
if (!latency) {
return theme.palette.text.secondary;
}
let color = theme.palette.success.light;
let color = theme.experimental.roles.success.fill;
if (latency >= 150 && latency < 300) {
color = theme.palette.warning.light;
color = theme.experimental.roles.warning.fill;
} else if (latency >= 300) {
color = theme.palette.error.light;
color = theme.experimental.roles.error.fill;
}
return color;
};

Some files were not shown because too many files have changed in this diff Show More