diff --git a/.github/workflows/pr-deploy.yaml b/.github/workflows/pr-deploy.yaml index 0aaf6664372f9..9c657b43ba699 100644 --- a/.github/workflows/pr-deploy.yaml +++ b/.github/workflows/pr-deploy.yaml @@ -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 \ diff --git a/site/.storybook/main.js b/site/.storybook/main.js index f955a9a2d9011..e5ede7b46b2f8 100644 --- a/site/.storybook/main.js +++ b/site/.storybook/main.js @@ -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: { diff --git a/site/.storybook/preview.jsx b/site/.storybook/preview.jsx index 2a687e27cdb01..05d5a340747c5 100644 --- a/site/.storybook/preview.jsx +++ b/site/.storybook/preview.jsx @@ -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 { QueryClient, QueryClientProvider } from "react-query"; import { HelmetProvider } from "react-helmet-async"; -import theme from "theme"; -import colors from "theme/tailwind"; +import themes from "theme"; import "theme/globalFonts"; -import { QueryClient, QueryClientProvider } from "react-query"; + +DecoratorHelpers.initializeThemeState(Object.keys(themes), "dark"); export const decorators = [ - (Story) => ( - - - - - - - - - ), + (Story, context) => { + const selectedTheme = DecoratorHelpers.pluckThemeFromContext(context); + const { themeOverride } = DecoratorHelpers.useThemeParameters(); + + const selected = themeOverride || selectedTheme || "dark"; + + return ( + + + + + + + + + ); + }, 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", + }, + }, + }, }; diff --git a/site/package.json b/site/package.json index 95614f5cc0606..92fc67de02cfb 100644 --- a/site/package.json +++ b/site/package.json @@ -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", diff --git a/site/pnpm-lock.yaml b/site/pnpm-lock.yaml index 23c835d6f0ea5..5bf7c2544766a 100644 --- a/site/pnpm-lock.yaml +++ b/site/pnpm-lock.yaml @@ -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: diff --git a/site/src/@types/mui.d.ts b/site/src/@types/mui.d.ts index 4017af0f1b6e8..255981fdf60fc 100644 --- a/site/src/@types/mui.d.ts +++ b/site/src/@types/mui.d.ts @@ -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; } diff --git a/site/src/App.tsx b/site/src/App.tsx index 8266aeb1bea12..a311c074430de 100644 --- a/site/src/App.tsx +++ b/site/src/App.tsx @@ -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 = ({ - + {children} - + diff --git a/site/src/components/ActiveUserChart/ActiveUserChart.tsx b/site/src/components/ActiveUserChart/ActiveUserChart.tsx index 3a4ca5d163c9c..797abd7f7ed75 100644 --- a/site/src/components/ActiveUserChart/ActiveUserChart.tsx +++ b/site/src/components/ActiveUserChart/ActiveUserChart.tsx @@ -96,6 +96,7 @@ export const ActiveUserChart: FC = ({ }, scales: { y: { + grid: { color: theme.palette.divider }, suggestedMin: 0, ticks: { precision: 0, @@ -103,6 +104,7 @@ export const ActiveUserChart: FC = ({ }, x: { + grid: { color: theme.palette.divider }, ticks: { stepSize: data.length > 10 ? 2 : undefined, }, @@ -124,11 +126,9 @@ export const ActiveUserChart: FC = ({ { 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, }, ], }} diff --git a/site/src/components/Badges/Badges.stories.tsx b/site/src/components/Badges/Badges.stories.tsx index e699b6317f0e7..fd31d446e0770 100644 --- a/site/src/components/Badges/Badges.stories.tsx +++ b/site/src/components/Badges/Badges.stories.tsx @@ -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 = { @@ -26,11 +32,34 @@ export const Entitled: Story = { children: , }, }; +export const ProxyStatus: Story = { + args: { + children: ( + <> + + + + + + + ), + }, +}; +export const Disabled: Story = { + args: { + children: , + }, +}; export const Enterprise: Story = { args: { children: , }, }; +export const Beta: Story = { + args: { + children: , + }, +}; export const Alpha: Story = { args: { children: , diff --git a/site/src/components/Badges/Badges.tsx b/site/src/components/Badges/Badges.tsx index e1ca731f749bf..1a60e949b0831 100644 --- a/site/src/components/Badges/Badges.tsx +++ b/site/src/components/Badges/Badges.tsx @@ -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>; @@ -42,10 +44,9 @@ export const EntitledBadge: FC = () => { }; interface HealthyBadge { - derpOnly: boolean; + derpOnly?: boolean; } -export const HealthyBadge: FC = (props) => { - const { derpOnly } = props; +export const HealthyBadge: FC = ({ derpOnly }) => { return ( {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 ( + ({ + border: `1px solid ${theme.experimental.roles.preview.outline}`, + backgroundColor: theme.experimental.roles.preview.background, + color: theme.experimental.roles.preview.text, + }), + ]} + > + Beta + + ); +}; + export const AlphaBadge: FC = () => { return ( ({ + 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 = () => { ({ + border: `1px solid ${theme.experimental.roles.danger.outline}`, + backgroundColor: theme.experimental.roles.danger.background, + color: theme.experimental.roles.danger.text, + }), ]} > Deprecated diff --git a/site/src/components/CodeExample/CodeExample.stories.tsx b/site/src/components/CodeExample/CodeExample.stories.tsx index 35c3da065f467..1af88cd98dafd 100644 --- a/site/src/components/CodeExample/CodeExample.stories.tsx +++ b/site/src/components/CodeExample/CodeExample.stories.tsx @@ -1,22 +1,22 @@ import type { Meta, StoryObj } from "@storybook/react"; import { CodeExample } from "./CodeExample"; -const sampleCode = `echo "Hello, world"`; - const meta: Meta = { title: "components/CodeExample", component: CodeExample, - argTypes: { - code: { control: "string", defaultValue: sampleCode }, + args: { + code: `echo "hello, friend!"`, }, }; export default meta; type Story = StoryObj; -export const Example: Story = { +export const Example: Story = {}; + +export const Secret: Story = { args: { - code: sampleCode, + secret: true, }, }; diff --git a/site/src/components/CodeExample/CodeExample.tsx b/site/src/components/CodeExample/CodeExample.tsx index 7f6dfc62cefbf..9014f04a58ce5 100644 --- a/site/src/components/CodeExample/CodeExample.tsx +++ b/site/src/components/CodeExample/CodeExample.tsx @@ -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 = (props) => { - const { code, password, className } = props; - const theme = useTheme(); - +export const CodeExample: FC = ({ + code, + secret, + className, +}) => { return ( -
- - {code} - +
+ {code}
); }; + +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>; diff --git a/site/src/components/Dashboard/DeploymentBanner/DeploymentBannerView.tsx b/site/src/components/Dashboard/DeploymentBanner/DeploymentBannerView.tsx index 1cb105118ebf8..46efd3712ca55 100644 --- a/site/src/components/Dashboard/DeploymentBanner/DeploymentBannerView.tsx +++ b/site/src/components/Dashboard/DeploymentBanner/DeploymentBannerView.tsx @@ -378,7 +378,7 @@ const HealthIssue: FC = ({ children }) => { {children} @@ -426,13 +426,13 @@ const classNames = { } satisfies Record; 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; diff --git a/site/src/components/Dashboard/LicenseBanner/LicenseBannerView.stories.tsx b/site/src/components/Dashboard/LicenseBanner/LicenseBannerView.stories.tsx index cc37d18488085..ffd396bb0fba6 100644 --- a/site/src/components/Dashboard/LicenseBanner/LicenseBannerView.stories.tsx +++ b/site/src/components/Dashboard/LicenseBanner/LicenseBannerView.stories.tsx @@ -1,8 +1,10 @@ import type { Meta, StoryObj } from "@storybook/react"; +import { chromatic } from "testHelpers/chromatic"; import { LicenseBannerView } from "./LicenseBannerView"; const meta: Meta = { title: "components/LicenseBannerView", + parameters: { chromatic }, component: LicenseBannerView, }; diff --git a/site/src/components/Dashboard/LicenseBanner/LicenseBannerView.tsx b/site/src/components/Dashboard/LicenseBanner/LicenseBannerView.tsx index 7e3889fcebdd8..31c9f4269e48d 100644 --- a/site/src/components/Dashboard/LicenseBanner/LicenseBannerView.tsx +++ b/site/src/components/Dashboard/LicenseBanner/LicenseBannerView.tsx @@ -47,11 +47,11 @@ export const LicenseBannerView: FC = ({ 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 (
@@ -59,7 +59,11 @@ export const LicenseBannerView: FC = ({
{messages[0]}   - + {Language.upgrade}
@@ -74,7 +78,11 @@ export const LicenseBannerView: FC = ({
{Language.exceeded}   - + {Language.upgrade}
diff --git a/site/src/components/Dashboard/Navbar/NavbarView.stories.tsx b/site/src/components/Dashboard/Navbar/NavbarView.stories.tsx index 189cd82d65a88..93afc9c618622 100644 --- a/site/src/components/Dashboard/Navbar/NavbarView.stories.tsx +++ b/site/src/components/Dashboard/Navbar/NavbarView.stories.tsx @@ -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 = { 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] }, - }, -}; diff --git a/site/src/components/Dashboard/Navbar/NavbarView.tsx b/site/src/components/Dashboard/Navbar/NavbarView.tsx index 56b5bdbf7398e..8b37d41ba1d53 100644 --- a/site/src/components/Dashboard/Navbar/NavbarView.tsx +++ b/site/src/components/Dashboard/Navbar/NavbarView.tsx @@ -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")} { diff --git a/site/src/components/Dashboard/Navbar/UserDropdown/UserDropdown.tsx b/site/src/components/Dashboard/Navbar/UserDropdown/UserDropdown.tsx index 3f5661e8df5e5..a5e83c0d50f1b 100644 --- a/site/src/components/Dashboard/Navbar/UserDropdown/UserDropdown.tsx +++ b/site/src/components/Dashboard/Navbar/UserDropdown/UserDropdown.tsx @@ -45,7 +45,7 @@ export const UserDropdown: FC = ({ />
diff --git a/site/src/components/Dashboard/ServiceBanner/ServiceBannerView.tsx b/site/src/components/Dashboard/ServiceBanner/ServiceBannerView.tsx index cc389f191ff09..b2b30ecb46d70 100644 --- a/site/src/components/Dashboard/ServiceBanner/ServiceBannerView.tsx +++ b/site/src/components/Dashboard/ServiceBanner/ServiceBannerView.tsx @@ -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 = ({ +export const ServiceBannerView: FC = ({ 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 ( -
+
{isPreview && }
- - {message} - + {message}
); }; + +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>; diff --git a/site/src/components/Dialogs/Dialog.tsx b/site/src/components/Dialogs/Dialog.tsx index ed379576305b9..7e429752ddaa2 100644 --- a/site/src/components/Dialogs/Dialog.tsx +++ b/site/src/components/Dialogs/Dialog.tsx @@ -58,7 +58,7 @@ export const DialogActionButtons: React.FC = ({ 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 = ({ }; 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, }, }, }, diff --git a/site/src/components/ExternalIcon/ExternalIcon.tsx b/site/src/components/ExternalIcon/ExternalIcon.tsx new file mode 100644 index 0000000000000..91eace58bec64 --- /dev/null +++ b/site/src/components/ExternalIcon/ExternalIcon.tsx @@ -0,0 +1,36 @@ +import { type Interpolation, type Theme } from "@emotion/react"; +import { type FC, type ImgHTMLAttributes } from "react"; + +interface ExternalIconProps extends ImgHTMLAttributes { + size?: number; +} + +export const ExternalIcon: FC = ({ + size = 36, + ...attrs +}) => { + return ( +
+ +
+ ); +}; + +const styles = { + container: { + borderRadius: 9999, + overflow: "clip", + }, + icon: { + backgroundColor: "#000", + objectFit: "contain", + }, +} satisfies Record>; diff --git a/site/src/components/Form/Form.tsx b/site/src/components/Form/Form.tsx index e8e2160054bcb..a7e902a3deb59 100644 --- a/site/src/components/Form/Form.tsx +++ b/site/src/components/Form/Form.tsx @@ -120,13 +120,7 @@ export const FormSection: FC< }} className={classes.sectionInfo} > -

+

{title} {alpha && } {deprecated && } @@ -154,14 +148,11 @@ const styles = { fontWeight: 400, margin: 0, marginBottom: 8, - }), - - formSectionInfoTitleAlpha: { display: "flex", flexDirection: "row", alignItems: "center", gap: 12, - }, + }), formSectionInfoDescription: (theme) => ({ fontSize: 14, diff --git a/site/src/components/HelpTooltip/HelpTooltip.tsx b/site/src/components/HelpTooltip/HelpTooltip.tsx index 76144ca608ab5..a6b17e60df397 100644 --- a/site/src/components/HelpTooltip/HelpTooltip.tsx +++ b/site/src/components/HelpTooltip/HelpTooltip.tsx @@ -206,6 +206,7 @@ const styles = { display: "flex", alignItems: "center", ...(theme.typography.body2 as CSSObject), + color: theme.experimental.roles.active.fill, }), linkIcon: { diff --git a/site/src/components/Loader/Loader.tsx b/site/src/components/Loader/Loader.tsx index d225b5dc2621f..5b7142bef4ae9 100644 --- a/site/src/components/Loader/Loader.tsx +++ b/site/src/components/Loader/Loader.tsx @@ -16,6 +16,7 @@ export const Loader: FC = ({ size = 26, ...attrs }) => { justifyContent: "center", }} data-testid="loader" + data-chromatic="ignore" {...attrs} > diff --git a/site/src/components/Markdown/Markdown.tsx b/site/src/components/Markdown/Markdown.tsx index a3a54f0dd6241..e6defa3733288 100644 --- a/site/src/components/Markdown/Markdown.tsx +++ b/site/src/components/Markdown/Markdown.tsx @@ -121,12 +121,19 @@ export const Markdown: FC = (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 = (props) => { - const { children, className, components = {} } = props; +export const InlineMarkdown: FC = (props) => { + const { children, allowedElements = [], className, components = {} } = props; return ( <>{children}, diff --git a/site/src/components/PaginationWidget/PageButtons.tsx b/site/src/components/PaginationWidget/PageButtons.tsx index ceef484aa13b3..003cb7bf4be34 100644 --- a/site/src/components/PaginationWidget/PageButtons.tsx +++ b/site/src/components/PaginationWidget/PageButtons.tsx @@ -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 = ({ pageNumber, totalPages, onClick, highlighted = false, disabled = false, -}: NumberedPageButtonProps) { +}) => { return ( ); -} +}; type PlaceholderPageButtonProps = PropsWithChildren<{ pagesOmitted: number; }>; -export function PlaceholderPageButton({ +export const PlaceholderPageButton: FC = ({ pagesOmitted, children = <>…, -}: PlaceholderPageButtonProps) { +}) => { return ( ); -} +}; type BasePageButtonProps = PropsWithChildren<{ name: string; @@ -59,22 +59,29 @@ type BasePageButtonProps = PropsWithChildren<{ disabled?: boolean; }>; -function BasePageButton({ +const BasePageButton: FC = ({ children, onClick, name, "aria-label": ariaLabel, highlighted = false, disabled = false, -}: BasePageButtonProps) { +}) => { const theme = useTheme(); return ( - ) : ( - - )} +
+

)} @@ -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, }, diff --git a/site/src/components/Resources/PortForwardButton.stories.tsx b/site/src/components/Resources/PortForwardButton.stories.tsx index 1f4c710e711a1..b2fc7ccfe704d 100644 --- a/site/src/components/Resources/PortForwardButton.stories.tsx +++ b/site/src/components/Resources/PortForwardButton.stories.tsx @@ -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 = { - title: "components/PortForwardPopoverView", - component: PortForwardPopoverView, - decorators: [ - (Story) => ( -
({ - width: 304, - border: `1px solid ${theme.palette.divider}`, - borderRadius: 8, - backgroundColor: theme.palette.background.paper, - })} - > - -
- ), - ], +const meta: Meta = { + title: "components/PortForwardButton", + component: PortForwardButton, args: { agent: MockWorkspaceAgent, }, }; export default meta; -type Story = StoryObj; +type Story = StoryObj; -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: {}, }, }; diff --git a/site/src/components/Resources/PortForwardButton.tsx b/site/src/components/Resources/PortForwardButton.tsx index a1935d282c56f..2b284586eaf45 100644 --- a/site/src/components/Resources/PortForwardButton.tsx +++ b/site/src/components/Resources/PortForwardButton.tsx @@ -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 = (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 ( - + {DisplayAppNameMap["port_forwarding_helper"]} - {portsQuery.data ? ( -
{portsQuery.data.ports.length}
+ {data ? ( +
{data.ports.length}
) : ( )}
- +
); @@ -204,7 +214,7 @@ const styles = { display: "flex", alignItems: "center", justifyContent: "center", - backgroundColor: theme.colors.gray[11], + backgroundColor: theme.experimental.l2.background, marginLeft: 8, }), diff --git a/site/src/components/Resources/PortForwardPopoverView.stories.tsx b/site/src/components/Resources/PortForwardPopoverView.stories.tsx new file mode 100644 index 0000000000000..1f4c710e711a1 --- /dev/null +++ b/site/src/components/Resources/PortForwardPopoverView.stories.tsx @@ -0,0 +1,43 @@ +import { PortForwardPopoverView } from "./PortForwardButton"; +import type { Meta, StoryObj } from "@storybook/react"; +import { + MockListeningPortsResponse, + MockWorkspaceAgent, +} from "testHelpers/entities"; + +const meta: Meta = { + title: "components/PortForwardPopoverView", + component: PortForwardPopoverView, + decorators: [ + (Story) => ( +
({ + width: 304, + border: `1px solid ${theme.palette.divider}`, + borderRadius: 8, + backgroundColor: theme.palette.background.paper, + })} + > + +
+ ), + ], + args: { + agent: MockWorkspaceAgent, + }, +}; + +export default meta; +type Story = StoryObj; + +export const WithPorts: Story = { + args: { + ports: MockListeningPortsResponse.ports, + }, +}; + +export const Empty: Story = { + args: { + ports: [], + }, +}; diff --git a/site/src/components/Resources/ResourceAvatar.tsx b/site/src/components/Resources/ResourceAvatar.tsx index b74a75b754c80..2951cb5af210a 100644 --- a/site/src/components/Resources/ResourceAvatar.tsx +++ b/site/src/components/Resources/ResourceAvatar.tsx @@ -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 = { 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 = ({ resource }) => { - const hasIcon = resource.icon && resource.icon !== ""; - const avatarSrc = hasIcon - ? resource.icon - : getIconPathResource(resource.type); + const avatarSrc = resource.icon || getIconPathResource(resource.type); return ( diff --git a/site/src/components/RichParameterInput/RichParameterInput.stories.tsx b/site/src/components/RichParameterInput/RichParameterInput.stories.tsx index cd654ba7f2abc..8d1d5212fa59a 100644 --- a/site/src/components/RichParameterInput/RichParameterInput.stories.tsx +++ b/site/src/components/RichParameterInput/RichParameterInput.stories.tsx @@ -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 = { title: "components/RichParameterInput", + parameters: { chromatic }, component: RichParameterInput, }; diff --git a/site/src/components/SyntaxHighlighter/SyntaxHighlighter.tsx b/site/src/components/SyntaxHighlighter/SyntaxHighlighter.tsx index 0fb184fb1d6bb..e34659cef104d 100644 --- a/site/src/components/SyntaxHighlighter/SyntaxHighlighter.tsx +++ b/site/src/components/SyntaxHighlighter/SyntaxHighlighter.tsx @@ -5,13 +5,20 @@ import { useCoderTheme } from "./coderTheme"; loader.config({ monaco }); -export const SyntaxHighlighter: FC<{ +interface SyntaxHighlighterProps { value: string; language: string; editorProps?: ComponentProps & ComponentProps; compareWith?: string; -}> = ({ value, compareWith, language, editorProps }) => { +} + +export const SyntaxHighlighter: FC = ({ + value, + compareWith, + language, + editorProps, +}) => { const hasDiff = compareWith && value !== compareWith; const coderTheme = useCoderTheme(); const commonProps = { diff --git a/site/src/components/SyntaxHighlighter/coderTheme.ts b/site/src/components/SyntaxHighlighter/coderTheme.ts index 377651fd02f9e..d543a04111a1d 100644 --- a/site/src/components/SyntaxHighlighter/coderTheme.ts +++ b/site/src/components/SyntaxHighlighter/coderTheme.ts @@ -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]); diff --git a/site/src/components/TemplateFiles/TemplateFiles.stories.tsx b/site/src/components/TemplateFiles/TemplateFiles.stories.tsx new file mode 100644 index 0000000000000..ce565ed3d0a48 --- /dev/null +++ b/site/src/components/TemplateFiles/TemplateFiles.stories.tsx @@ -0,0 +1,31 @@ +import { action } from "@storybook/addon-actions"; +import type { Meta, StoryObj } from "@storybook/react"; +import { chromatic } from "testHelpers/chromatic"; +import { TemplateFiles } from "./TemplateFiles"; + +const exampleFiles = { + "README.md": + "---\nname: Develop in Docker\ndescription: Develop inside Docker containers using your local daemon\ntags: [local, docker]\nicon: /icon/docker.png\n---\n\n# docker\n\nTo get started, run `coder templates init`. When prompted, select this template.\nFollow the on-screen instructions to proceed.\n\n## Editing the image\n\nEdit the `Dockerfile` and run `coder templates push` to update workspaces.\n\n## code-server\n\n`code-server` is installed via the `startup_script` argument in the `coder_agent`\nresource block. The `coder_app` resource is defined to access `code-server` through\nthe dashboard UI over `localhost:13337`.\n\n## Extending this template\n\nSee the [kreuzwerker/docker](https://registry.terraform.io/providers/kreuzwerker/docker) Terraform provider documentation to\nadd the following features to your Coder template:\n\n- SSH/TCP docker host\n- Registry authentication\n- Build args\n- Volume mounts\n- Custom container spec\n- More\n\nWe also welcome contributions!\n", + "build/Dockerfile": + 'FROM ubuntu\n\nRUN apt-get update \\\n\t&& apt-get install -y \\\n\tcurl \\\n\tgit \\\n\tgolang \\\n\tsudo \\\n\tvim \\\n\twget \\\n\t&& rm -rf /var/lib/apt/lists/*\n\nARG USER=coder\nRUN useradd --groups sudo --no-create-home --shell /bin/bash ${USER} \\\n\t&& echo "${USER} ALL=(ALL) NOPASSWD:ALL" >/etc/sudoers.d/${USER} \\\n\t&& chmod 0440 /etc/sudoers.d/${USER}\nUSER ${USER}\nWORKDIR /home/${USER}\n', + "main.tf": + 'terraform {\n required_providers {\n coder = {\n source = "coder/coder"\n }\n docker = {\n source = "kreuzwerker/docker"\n }\n }\n}\n\nlocals {\n username = data.coder_workspace.me.owner\n}\n\ndata "coder_provisioner" "me" {\n}\n\nprovider "docker" {\n}\n\ndata "coder_workspace" "me" {\n}\n\nresource "coder_agent" "main" {\n arch = data.coder_provisioner.me.arch\n os = "linux"\n startup_script_timeout = 180\n startup_script = <<-EOT\n set -e\n\n # install and start code-server\n curl -fsSL https://code-server.dev/install.sh | sh -s -- --method=standalone --prefix=/tmp/code-server --version 4.11.0\n /tmp/code-server/bin/code-server --auth none --port 13337 >/tmp/code-server.log 2>&1 &\n EOT\n\n # These environment variables allow you to make Git commits right away after creating a\n # workspace. Note that they take precedence over configuration defined in ~/.gitconfig!\n # You can remove this block if you\'d prefer to configure Git manually or using\n # dotfiles. (see docs/dotfiles.md)\n env = {\n GIT_AUTHOR_NAME = "${data.coder_workspace.me.owner}"\n GIT_COMMITTER_NAME = "${data.coder_workspace.me.owner}"\n GIT_AUTHOR_EMAIL = "${data.coder_workspace.me.owner_email}"\n GIT_COMMITTER_EMAIL = "${data.coder_workspace.me.owner_email}"\n }\n\n # The following metadata blocks are optional. They are used to display\n # information about your workspace in the dashboard. You can remove them\n # if you don\'t want to display any information.\n # For basic resources, you can use the `coder stat` command.\n # If you need more control, you can write your own script.\n metadata {\n display_name = "CPU Usage"\n key = "0_cpu_usage"\n script = "coder stat cpu"\n interval = 10\n timeout = 1\n }\n\n metadata {\n display_name = "RAM Usage"\n key = "1_ram_usage"\n script = "coder stat mem"\n interval = 10\n timeout = 1\n }\n\n metadata {\n display_name = "Home Disk"\n key = "3_home_disk"\n script = "coder stat disk --path $${HOME}"\n interval = 60\n timeout = 1\n }\n\n metadata {\n display_name = "CPU Usage (Host)"\n key = "4_cpu_usage_host"\n script = "coder stat cpu --host"\n interval = 10\n timeout = 1\n }\n\n metadata {\n display_name = "Memory Usage (Host)"\n key = "5_mem_usage_host"\n script = "coder stat mem --host"\n interval = 10\n timeout = 1\n }\n\n metadata {\n display_name = "Load Average (Host)"\n key = "6_load_host"\n # get load avg scaled by number of cores\n script = < = { + title: "components/TemplateFiles", + parameters: { chromatic }, + component: TemplateFiles, + args: { + currentFiles: exampleFiles, + previousFiles: exampleFiles, + tab: { value: "0", set: action("change tab") }, + }, +}; + +export default meta; +type Story = StoryObj; + +const Example: Story = {}; + +export { Example as TemplateFiles }; diff --git a/site/src/components/TemplateFiles/TemplateFiles.tsx b/site/src/components/TemplateFiles/TemplateFiles.tsx index 7acce3a1b0ee1..da1cc9bf07356 100644 --- a/site/src/components/TemplateFiles/TemplateFiles.tsx +++ b/site/src/components/TemplateFiles/TemplateFiles.tsx @@ -37,11 +37,17 @@ const languageByExtension: Record = { protobuf: "protobuf", }; -export const TemplateFiles: FC<{ +interface TemplateFilesProps { currentFiles: TemplateVersionFiles; previousFiles?: TemplateVersionFiles; tab: UseTabResult; -}> = ({ currentFiles, previousFiles, tab }) => { +} + +export const TemplateFiles: FC = ({ + currentFiles, + previousFiles, + tab, +}) => { const filenames = Object.keys(currentFiles); const selectedFilename = filenames[Number(tab.value)]; const currentFile = currentFiles[selectedFilename]; diff --git a/site/src/components/WorkspaceBuildLogs/Logs.tsx b/site/src/components/WorkspaceBuildLogs/Logs.tsx index 585adf0f9788b..40c419b600dc8 100644 --- a/site/src/components/WorkspaceBuildLogs/Logs.tsx +++ b/site/src/components/WorkspaceBuildLogs/Logs.tsx @@ -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> = ({ +export const Logs: FC = ({ hideTimestamps, lines, className = "", @@ -50,16 +53,23 @@ export const Logs: FC> = ({ 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 = ({ + 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: { diff --git a/site/src/components/WorkspaceBuildLogs/WorkspaceBuildLogs.stories.tsx b/site/src/components/WorkspaceBuildLogs/WorkspaceBuildLogs.stories.tsx index 5044a9c4e07bd..39d966b890fa7 100644 --- a/site/src/components/WorkspaceBuildLogs/WorkspaceBuildLogs.stories.tsx +++ b/site/src/components/WorkspaceBuildLogs/WorkspaceBuildLogs.stories.tsx @@ -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 = { title: "components/WorkspaceBuildLogs", + parameters: { chromatic }, component: WorkspaceBuildLogs, }; diff --git a/site/src/components/WorkspaceStatusBadge/WorkspaceStatusBadge.tsx b/site/src/components/WorkspaceStatusBadge/WorkspaceStatusBadge.tsx index b889e9cd1dbb9..00aced41ad3f6 100644 --- a/site/src/components/WorkspaceStatusBadge/WorkspaceStatusBadge.tsx +++ b/site/src/components/WorkspaceStatusBadge/WorkspaceStatusBadge.tsx @@ -69,7 +69,7 @@ export const DormantStatusBadge: FC = ({ className, }) => { if (!workspace.dormant_at) { - return <>; + return null; } const formatDate = (dateStr: string): string => { diff --git a/site/src/contexts/ThemeProviders.tsx b/site/src/contexts/ThemeProvider.tsx similarity index 76% rename from site/src/contexts/ThemeProviders.tsx rename to site/src/contexts/ThemeProvider.tsx index 33a3357da5c9d..488c2906022cc 100644 --- a/site/src/contexts/ThemeProviders.tsx +++ b/site/src/contexts/ThemeProvider.tsx @@ -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 = ({ children }) => { +/** + * + */ +export const ThemeProvider: FC = ({ 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 = ({ children }) => { return ( - - - - {children} - - + {children} ); }; + +interface ThemeOverrideProps { + theme: Theme; + children?: ReactNode; +} + +export const ThemeOverride: FC = ({ theme, children }) => { + return ( + + + + {children} + + + ); +}; diff --git a/site/src/pages/AuditPage/AuditLogRow/AuditLogDescription/AuditLogDescription.tsx b/site/src/pages/AuditPage/AuditLogRow/AuditLogDescription/AuditLogDescription.tsx index f583bd694aa2e..9e59cceb4e3df 100644 --- a/site/src/pages/AuditPage/AuditLogRow/AuditLogDescription/AuditLogDescription.tsx +++ b/site/src/pages/AuditPage/AuditLogRow/AuditLogDescription/AuditLogDescription.tsx @@ -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 = ({ auditLog, -}): JSX.Element => { +}) => { let target = auditLog.resource_target.trim(); const user = auditLog.user?.username.trim(); diff --git a/site/src/pages/AuditPage/AuditLogRow/AuditLogRow.stories.tsx b/site/src/pages/AuditPage/AuditLogRow/AuditLogRow.stories.tsx index 9c7c681d4e59a..352b6b8d578aa 100644 --- a/site/src/pages/AuditPage/AuditLogRow/AuditLogRow.stories.tsx +++ b/site/src/pages/AuditPage/AuditLogRow/AuditLogRow.stories.tsx @@ -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 = { 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, diff --git a/site/src/pages/AuditPage/AuditPageView.stories.tsx b/site/src/pages/AuditPage/AuditPageView.stories.tsx index e64e54b48699d..69aa64977e92a 100644 --- a/site/src/pages/AuditPage/AuditPageView.stories.tsx +++ b/site/src/pages/AuditPage/AuditPageView.stories.tsx @@ -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; 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] }, - }, -}; diff --git a/site/src/pages/AuditPage/AuditPaywall.tsx b/site/src/pages/AuditPage/AuditPaywall.tsx index 9fab0c4899156..8de74dbdc251c 100644 --- a/site/src/pages/AuditPage/AuditPaywall.tsx +++ b/site/src/pages/AuditPage/AuditPaywall.tsx @@ -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={ - - diff --git a/site/src/pages/CliAuthPage/CliAuthPageView.tsx b/site/src/pages/CliAuthPage/CliAuthPageView.tsx index 88180b954a2f0..0c88a394b77ea 100644 --- a/site/src/pages/CliAuthPage/CliAuthPageView.tsx +++ b/site/src/pages/CliAuthPage/CliAuthPageView.tsx @@ -32,17 +32,13 @@ export const CliAuthPageView: FC = ({ sessionToken }) => { }} > Copy the session token below and{" "} - + paste it in your terminal .

- +
= { title: "pages/CreateWorkspacePage", + parameters: { chromatic }, component: CreateWorkspacePageView, args: { defaultName: "", diff --git a/site/src/pages/CreateWorkspacePage/ExternalAuth.stories.tsx b/site/src/pages/CreateWorkspacePage/ExternalAuth.stories.tsx index 87e7607282068..72e313968c688 100644 --- a/site/src/pages/CreateWorkspacePage/ExternalAuth.stories.tsx +++ b/site/src/pages/CreateWorkspacePage/ExternalAuth.stories.tsx @@ -9,7 +9,7 @@ const meta: Meta = { export default meta; type Story = StoryObj; -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", diff --git a/site/src/pages/CreateWorkspacePage/ExternalAuth.tsx b/site/src/pages/CreateWorkspacePage/ExternalAuth.tsx index 691b6681afddb..b85d2bcb3145b 100644 --- a/site/src/pages/CreateWorkspacePage/ExternalAuth.tsx +++ b/site/src/pages/CreateWorkspacePage/ExternalAuth.tsx @@ -16,70 +16,82 @@ export interface ExternalAuthProps { startPollingExternalAuth: () => void; error?: string; message?: string; + fullWidth?: boolean; } -export const ExternalAuth: FC = (props) => { - const { - displayName, - displayIcon, - authenticated, - authenticateURL, - externalAuthPollingState, - startPollingExternalAuth, - error, - message, - } = props; - +export const ExternalAuth: FC = ({ + displayName, + displayIcon, + authenticated, + authenticateURL, + externalAuthPollingState, + startPollingExternalAuth, + error, + message, + fullWidth = true, +}) => { const messageContent = message ?? (authenticated ? `Authenticated with ${displayName}` : `Login with ${displayName}`); + return ( - - - - ) - } - 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} - + + ) + } + 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} + - {externalAuthPollingState === "abandoned" && ( - - )} - {error && {error}} - - + {externalAuthPollingState === "abandoned" && ( + + )} + + + {error && ( + ({ color: theme.experimental.roles.error.text })} + > + {error} + + )} + ); }; diff --git a/site/src/pages/DeploySettingsPage/AppearanceSettingsPage/AppearanceSettingsPageView.tsx b/site/src/pages/DeploySettingsPage/AppearanceSettingsPage/AppearanceSettingsPageView.tsx index e4ae30c846757..be6092f0b0b88 100644 --- a/site/src/pages/DeploySettingsPage/AppearanceSettingsPage/AppearanceSettingsPageView.tsx +++ b/site/src/pages/DeploySettingsPage/AppearanceSettingsPage/AppearanceSettingsPageView.tsx @@ -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; diff --git a/site/src/pages/DeploySettingsPage/LicensesSettingsPage/LicensesSettingsPageView.stories.tsx b/site/src/pages/DeploySettingsPage/LicensesSettingsPage/LicensesSettingsPageView.stories.tsx index 21edae00d209c..54426f1cb1258 100644 --- a/site/src/pages/DeploySettingsPage/LicensesSettingsPage/LicensesSettingsPageView.stories.tsx +++ b/site/src/pages/DeploySettingsPage/LicensesSettingsPage/LicensesSettingsPageView.stories.tsx @@ -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, }; diff --git a/site/src/pages/GroupsPage/GroupsPageView.tsx b/site/src/pages/GroupsPage/GroupsPageView.tsx index 510200819eb15..faaafafb9df7d 100644 --- a/site/src/pages/GroupsPage/GroupsPageView.tsx +++ b/site/src/pages/GroupsPage/GroupsPageView.tsx @@ -59,6 +59,7 @@ export const GroupsPageView: FC = ({ rel="noreferrer" startIcon={} variant="contained" + color="primary" > Learn about Enterprise @@ -68,7 +69,7 @@ export const GroupsPageView: FC = ({ target="_blank" rel="noreferrer" > - Read the docs + Read the documentation } diff --git a/site/src/pages/HealthPage/DERPRegionPage.tsx b/site/src/pages/HealthPage/DERPRegionPage.tsx index 3dfb1cea2a7b0..35503c3f39388 100644 --- a/site/src/pages/HealthPage/DERPRegionPage.tsx +++ b/site/src/pages/HealthPage/DERPRegionPage.tsx @@ -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(); const params = useParams() as { regionId: string }; diff --git a/site/src/pages/HealthPage/HealthLayout.tsx b/site/src/pages/HealthPage/HealthLayout.tsx index 652ba8d95c893..0b80dc01b423b 100644 --- a/site/src/pages/HealthPage/HealthLayout.tsx +++ b/site/src/pages/HealthPage/HealthLayout.tsx @@ -122,6 +122,7 @@ export function HealthLayout() {
Last check Version { return { render: HealthLayout, parameters: { + chromatic, layout: "fullscreen", reactRouter: reactRouterParameters({ location: { pathParams: params }, diff --git a/site/src/pages/SetupPage/SetupPageView.stories.tsx b/site/src/pages/SetupPage/SetupPageView.stories.tsx index 98f388043efed..239fb10cab930 100644 --- a/site/src/pages/SetupPage/SetupPageView.stories.tsx +++ b/site/src/pages/SetupPage/SetupPageView.stories.tsx @@ -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 = { title: "pages/SetupPage", + parameters: { chromatic }, component: SetupPageView, }; diff --git a/site/src/pages/TemplatePage/TemplateFilesPage/TemplateFilesPage.tsx b/site/src/pages/TemplatePage/TemplateFilesPage/TemplateFilesPage.tsx index 1a1cd9b244b10..0c8b53c6d53f9 100644 --- a/site/src/pages/TemplatePage/TemplateFilesPage/TemplateFilesPage.tsx +++ b/site/src/pages/TemplatePage/TemplateFilesPage/TemplateFilesPage.tsx @@ -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 { useFileTab, useTemplateFiles } from "components/TemplateFiles/hooks"; 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"; const TemplateFilesPage: FC = () => { const { template, activeVersion } = useTemplateLayoutContext(); diff --git a/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.stories.tsx b/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.stories.tsx index 361ff4f12ce1a..894beb3a600d0 100644 --- a/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.stories.tsx +++ b/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.stories.tsx @@ -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 = { title: "pages/TemplatePage/TemplateInsightsPageView", + parameters: { chromatic }, component: TemplateInsightsPageView, }; diff --git a/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.tsx b/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.tsx index af96ee7b913b5..ed4ddc9d3d35a 100644 --- a/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.tsx +++ b/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.tsx @@ -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"; diff --git a/site/src/pages/TemplatePage/TemplateVersionsPage/VersionRow.tsx b/site/src/pages/TemplatePage/TemplateVersionsPage/VersionRow.tsx index 2f1ecffeeb1ac..af8110c7ad5b9 100644 --- a/site/src/pages/TemplatePage/TemplateVersionsPage/VersionRow.tsx +++ b/site/src/pages/TemplatePage/TemplateVersionsPage/VersionRow.tsx @@ -39,7 +39,6 @@ export const VersionRow: FC = ({ @@ -127,16 +126,6 @@ export const VersionRow: FC = ({ }; 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", diff --git a/site/src/pages/TemplateSettingsPage/TemplatePermissionsPage/TemplatePermissionsPage.tsx b/site/src/pages/TemplateSettingsPage/TemplatePermissionsPage/TemplatePermissionsPage.tsx index 6cf11ec1faf62..5b4bee3deee2a 100644 --- a/site/src/pages/TemplateSettingsPage/TemplatePermissionsPage/TemplatePermissionsPage.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplatePermissionsPage/TemplatePermissionsPage.tsx @@ -56,7 +56,7 @@ export const TemplatePermissionsPage: FC< - Read the docs + Read the documentation } diff --git a/site/src/pages/TemplateVersionEditorPage/FileTreeView.tsx b/site/src/pages/TemplateVersionEditorPage/FileTreeView.tsx index c2e753cdb2bc6..a9924a1104781 100644 --- a/site/src/pages/TemplateVersionEditorPage/FileTreeView.tsx +++ b/site/src/pages/TemplateVersionEditorPage/FileTreeView.tsx @@ -92,8 +92,8 @@ export const FileTreeView: FC = ({ } &.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 = ({ } as CSSProperties } > - {isFolder ? ( + {isFolder && Object.keys(content) .sort(sortFileTree(content)) .map((filename) => { const child = content[filename]; return buildTreeItems(filename, child, currentPath); - }) - ) : ( - <> - )} + })} ); }; diff --git a/site/src/pages/TemplateVersionEditorPage/MonacoEditor.tsx b/site/src/pages/TemplateVersionEditorPage/MonacoEditor.tsx index 7b3ed9ab29d2a..7667afc4431b7 100644 --- a/site/src/pages/TemplateVersionEditorPage/MonacoEditor.tsx +++ b/site/src/pages/TemplateVersionEditorPage/MonacoEditor.tsx @@ -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 = ({ + 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 ( { + 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", }); diff --git a/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditor.stories.tsx b/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditor.stories.tsx index bcd60352568ae..a2fe510d2badf 100644 --- a/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditor.stories.tsx +++ b/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditor.stories.tsx @@ -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 = { - 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; diff --git a/site/src/pages/TemplatesPage/TemplatesPageView.stories.tsx b/site/src/pages/TemplatesPage/TemplatesPageView.stories.tsx index 93c13c55cd794..07aecdb173ae9 100644 --- a/site/src/pages/TemplatesPage/TemplatesPageView.stories.tsx +++ b/site/src/pages/TemplatesPage/TemplatesPageView.stories.tsx @@ -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 = { 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, diff --git a/site/src/pages/TemplatesPage/TemplatesPageView.tsx b/site/src/pages/TemplatesPage/TemplatesPageView.tsx index 273990e4afbe8..d0321d11a7f83 100644 --- a/site/src/pages/TemplatesPage/TemplatesPageView.tsx +++ b/site/src/pages/TemplatesPage/TemplatesPageView.tsx @@ -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 = ({ template }) => { {template.deprecated ? ( - + ) : (
{!enableAutoStart && ( - + )} @@ -378,7 +378,7 @@ export const WorkspaceScheduleForm: FC<
{!enableAutoStop && ( - + )} diff --git a/site/src/testHelpers/chromatic.ts b/site/src/testHelpers/chromatic.ts new file mode 100644 index 0000000000000..a3282f2178c97 --- /dev/null +++ b/site/src/testHelpers/chromatic.ts @@ -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" }, + }, +}; diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index 592f74baa02ce..6687acc82ae91 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -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", }, ], }, diff --git a/site/src/testHelpers/renderHelpers.tsx b/site/src/testHelpers/renderHelpers.tsx index fad96f08e8676..4d16b40a44ded 100644 --- a/site/src/testHelpers/renderHelpers.tsx +++ b/site/src/testHelpers/renderHelpers.tsx @@ -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 => { export const renderComponent = (component: React.ReactElement) => { return tlRender(component, { - wrapper: ({ children }) => {children}, + wrapper: ({ children }) => {children}, }); }; diff --git a/site/src/theme/dark/experimental.ts b/site/src/theme/dark/experimental.ts index b5112ec5e23d0..7e929c4ea8e97 100644 --- a/site/src/theme/dark/experimental.ts +++ b/site/src/theme/dark/experimental.ts @@ -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; diff --git a/site/src/theme/dark/index.ts b/site/src/theme/dark/index.ts index 19a84fc4b2c0a..4a1d9094c6bc2 100644 --- a/site/src/theme/dark/index.ts +++ b/site/src/theme/dark/index.ts @@ -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, }; diff --git a/site/src/theme/dark/monaco.ts b/site/src/theme/dark/monaco.ts new file mode 100644 index 0000000000000..ae020bdd1ba33 --- /dev/null +++ b/site/src/theme/dark/monaco.ts @@ -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; diff --git a/site/src/theme/dark/mui.ts b/site/src/theme/dark/mui.ts index 31d00fd6fa001..ae26de7cf895b 100644 --- a/site/src/theme/dark/mui.ts +++ b/site/src/theme/dark/mui.ts @@ -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", }, }, }, diff --git a/site/src/theme/darkBlue/experimental.ts b/site/src/theme/darkBlue/experimental.ts index f694eca5823e2..9b15df75a932b 100644 --- a/site/src/theme/darkBlue/experimental.ts +++ b/site/src/theme/darkBlue/experimental.ts @@ -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; diff --git a/site/src/theme/darkBlue/index.ts b/site/src/theme/darkBlue/index.ts index 19a84fc4b2c0a..4a1d9094c6bc2 100644 --- a/site/src/theme/darkBlue/index.ts +++ b/site/src/theme/darkBlue/index.ts @@ -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, }; diff --git a/site/src/theme/darkBlue/monaco.ts b/site/src/theme/darkBlue/monaco.ts new file mode 100644 index 0000000000000..ae020bdd1ba33 --- /dev/null +++ b/site/src/theme/darkBlue/monaco.ts @@ -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; diff --git a/site/src/theme/darkBlue/mui.ts b/site/src/theme/darkBlue/mui.ts index 31d00fd6fa001..f9308dc00044b 100644 --- a/site/src/theme/darkBlue/mui.ts +++ b/site/src/theme/darkBlue/mui.ts @@ -489,6 +489,7 @@ muiTheme = createTheme(muiTheme, { lineHeight: "150%", borderRadius: 4, background: muiTheme.palette.divider, + padding: "8px 16px", }, }, }, diff --git a/site/src/theme/experimental.ts b/site/src/theme/experimental.ts index 385ab548a98b2..7bbc247f2a8e1 100644 --- a/site/src/theme/experimental.ts +++ b/site/src/theme/experimental.ts @@ -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 }; } diff --git a/site/src/theme/index.ts b/site/src/theme/index.ts index 7e23c0345c89d..6f889785286f9 100644 --- a/site/src/theme/index.ts +++ b/site/src/theme/index.ts @@ -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; export default theme; diff --git a/site/src/theme/light/colors.ts b/site/src/theme/light/colors.ts new file mode 100644 index 0000000000000..374c120e8f289 --- /dev/null +++ b/site/src/theme/light/colors.ts @@ -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], + }, +}; diff --git a/site/src/theme/light/experimental.ts b/site/src/theme/light/experimental.ts new file mode 100644 index 0000000000000..87ea50527c9f9 --- /dev/null +++ b/site/src/theme/light/experimental.ts @@ -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; diff --git a/site/src/theme/light/index.ts b/site/src/theme/light/index.ts new file mode 100644 index 0000000000000..4a1d9094c6bc2 --- /dev/null +++ b/site/src/theme/light/index.ts @@ -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, +}; diff --git a/site/src/theme/light/monaco.ts b/site/src/theme/light/monaco.ts new file mode 100644 index 0000000000000..6dc38d2be7c36 --- /dev/null +++ b/site/src/theme/light/monaco.ts @@ -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; diff --git a/site/src/theme/light/mui.ts b/site/src/theme/light/mui.ts new file mode 100644 index 0000000000000..23040ee3f7a2f --- /dev/null +++ b/site/src/theme/light/mui.ts @@ -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; diff --git a/site/src/utils/latency.ts b/site/src/utils/latency.ts index 0a7f45d551cf3..4281fa17e990f 100644 --- a/site/src/utils/latency.ts +++ b/site/src/utils/latency.ts @@ -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; }; diff --git a/site/src/utils/workspace.tsx b/site/src/utils/workspace.tsx index 048af1cab10ab..3f823b659f686 100644 --- a/site/src/utils/workspace.tsx +++ b/site/src/utils/workspace.tsx @@ -1,15 +1,16 @@ -import { Theme } from "@mui/material/styles"; -import dayjs from "dayjs"; -import duration from "dayjs/plugin/duration"; -import minMax from "dayjs/plugin/minMax"; -import utc from "dayjs/plugin/utc"; -import semver from "semver"; -import * as TypesGen from "api/typesGenerated"; import CircularProgress from "@mui/material/CircularProgress"; import ErrorIcon from "@mui/icons-material/ErrorOutline"; import StopIcon from "@mui/icons-material/StopOutlined"; import PlayIcon from "@mui/icons-material/PlayArrowOutlined"; import QueuedIcon from "@mui/icons-material/HourglassEmpty"; +import dayjs from "dayjs"; +import duration from "dayjs/plugin/duration"; +import minMax from "dayjs/plugin/minMax"; +import utc from "dayjs/plugin/utc"; +import { type Theme } from "@emotion/react"; +import { type FC } from "react"; +import semver from "semver"; +import type * as TypesGen from "api/typesGenerated"; dayjs.extend(duration); dayjs.extend(utc); @@ -28,6 +29,15 @@ const DisplayAgentVersionLanguage = { unknown: "Unknown", }; +const LoadingIcon: FC = () => { + return ( + ({ color: theme.experimental.l1.text })} + /> + ); +}; + export const getDisplayWorkspaceBuildStatus = ( theme: Theme, build: TypesGen.WorkspaceBuild, @@ -249,10 +259,6 @@ const getPendingWorkspaceStatusText = ( return "Position in queue: " + provisionerJob.queue_position; }; -const LoadingIcon = () => { - return ; -}; - export const hasJobError = (workspace: TypesGen.Workspace) => { return workspace.latest_build.job.error !== undefined; };