diff --git a/site/src/components/DeploySettingsLayout/Sidebar.tsx b/site/src/components/DeploySettingsLayout/Sidebar.tsx index 4ada46382ef0a..1566d1e27bba5 100644 --- a/site/src/components/DeploySettingsLayout/Sidebar.tsx +++ b/site/src/components/DeploySettingsLayout/Sidebar.tsx @@ -7,135 +7,53 @@ import Globe from "@mui/icons-material/PublicOutlined"; import HubOutlinedIcon from "@mui/icons-material/HubOutlined"; import VpnKeyOutlined from "@mui/icons-material/VpnKeyOutlined"; import MonitorHeartOutlined from "@mui/icons-material/MonitorHeartOutlined"; -import { GitIcon } from "components/Icons/GitIcon"; -import { Stack } from "components/Stack/Stack"; -import type { ElementType, FC, PropsWithChildren, ReactNode } from "react"; -import { NavLink } from "react-router-dom"; +import { type FC } from "react"; import { useDashboard } from "components/Dashboard/DashboardProvider"; -import { css } from "@emotion/css"; -import { useTheme } from "@emotion/react"; - -const SidebarNavItem: FC< - PropsWithChildren<{ href: string; icon: ReactNode }> -> = ({ children, href, icon }) => { - const theme = useTheme(); - - const activeStyles = css` - background-color: ${theme.palette.action.hover}; - - &::before { - content: ""; - display: block; - width: 3px; - height: 100%; - position: absolute; - left: 0; - top: 0; - background-color: ${theme.palette.primary.main}; - border-top-left-radius: 8px; - border-bottom-left-radius: 8px; - } - `; - - return ( - css` - ${isActive && activeStyles} - - color: inherit; - display: block; - font-size: 14px; - text-decoration: none; - padding: 12px 12px 12px 16px; - border-radius: 4px; - transition: background-color 0.15s ease-in-out; - margin-bottom: 1; - position: relative; - - &:hover { - background-color: ${theme.palette.action.hover}; - } - `} - > - - {icon} - {children} - - - ); -}; - -const SidebarNavItemIcon: FC<{ icon: ElementType }> = ({ icon: Icon }) => { - return ; -}; +import { GitIcon } from "components/Icons/GitIcon"; +import { + Sidebar as BaseSidebar, + SidebarNavItem, +} from "components/Sidebar/Sidebar"; -export const Sidebar: React.FC = () => { +export const Sidebar: FC = () => { const dashboard = useDashboard(); return ( - + ); }; diff --git a/site/src/components/SettingsLayout/Section.tsx b/site/src/components/SettingsLayout/Section.tsx index 25e3ea6e271d1..029fb33c28a08 100644 --- a/site/src/components/SettingsLayout/Section.tsx +++ b/site/src/components/SettingsLayout/Section.tsx @@ -1,4 +1,4 @@ -import { type FC, type ReactNode, type PropsWithChildren } from "react"; +import { type FC, type ReactNode } from "react"; import { type Interpolation, type Theme } from "@emotion/react"; type SectionLayout = "fixed" | "fluid"; @@ -15,9 +15,7 @@ export interface SectionProps { children?: ReactNode; } -type SectionFC = FC>; - -export const Section: SectionFC = ({ +export const Section: FC = ({ id, title, description, diff --git a/site/src/components/SettingsLayout/Sidebar.tsx b/site/src/components/SettingsLayout/Sidebar.tsx index faa09db980b5e..115f481dcc4f0 100644 --- a/site/src/components/SettingsLayout/Sidebar.tsx +++ b/site/src/components/SettingsLayout/Sidebar.tsx @@ -1,89 +1,16 @@ -import { css } from "@emotion/css"; -import { - type CSSObject, - type Interpolation, - type Theme, - useTheme, -} from "@emotion/react"; import VpnKeyOutlined from "@mui/icons-material/VpnKeyOutlined"; import FingerprintOutlinedIcon from "@mui/icons-material/FingerprintOutlined"; -import { - type FC, - type ComponentType, - type PropsWithChildren, - type ReactNode, -} from "react"; -import { NavLink } from "react-router-dom"; import AccountIcon from "@mui/icons-material/Person"; import ScheduleIcon from "@mui/icons-material/EditCalendarOutlined"; import SecurityIcon from "@mui/icons-material/LockOutlined"; import type { User } from "api/typesGenerated"; -import { Stack } from "components/Stack/Stack"; import { UserAvatar } from "components/UserAvatar/UserAvatar"; import { useDashboard } from "components/Dashboard/DashboardProvider"; -import { combineClasses } from "utils/combineClasses"; - -const SidebarNavItem: FC< - PropsWithChildren<{ href: string; icon: ReactNode }> -> = ({ children, href, icon }) => { - const theme = useTheme(); - - const sidebarNavItemStyles = css` - color: inherit; - display: block; - font-size: 14px; - text-decoration: none; - padding: 12px 12px 12px 16px; - border-radius: 4px; - transition: background-color 0.15s ease-in-out; - margin-bottom: 1px; - position: relative; - - &:hover { - background-color: theme.palette.action.hover; - } - `; - - const sidebarNavItemActiveStyles = css` - background-color: ${theme.palette.action.hover}; - - &:before { - content: ""; - display: block; - width: 3px; - height: 100%; - position: absolute; - left: 0; - top: 0; - background-color: ${theme.palette.primary.main}; - border-top-left-radius: 8px; - border-bottom-left-radius: 8px; - } - `; - - return ( - - combineClasses([ - sidebarNavItemStyles, - isActive ? sidebarNavItemActiveStyles : undefined, - ]) - } - > - - {icon} - {children} - - - ); -}; - -const SidebarNavItemIcon: React.FC<{ - icon: ComponentType<{ className?: string }>; -}> = ({ icon: Icon }) => { - return ; -}; +import { + Sidebar as BaseSidebar, + SidebarHeader, + SidebarNavItem, +} from "components/Sidebar/Sidebar"; export const Sidebar: React.FC<{ user: User }> = ({ user }) => { const { entitlements } = useDashboard(); @@ -91,73 +18,32 @@ export const Sidebar: React.FC<{ user: User }> = ({ user }) => { entitlements.features.template_autostop_requirement.enabled; return ( - + ); }; - -const styles = { - sidebar: { - width: 245, - flexShrink: 0, - }, - userInfo: (theme) => ({ - ...(theme.typography.body2 as CSSObject), - marginBottom: 16, - }), - userData: { - overflow: "hidden", - }, - username: { - fontWeight: 600, - overflow: "hidden", - textOverflow: "ellipsis", - whiteSpace: "nowrap", - }, - email: (theme) => ({ - color: theme.palette.text.secondary, - fontSize: 12, - overflow: "hidden", - textOverflow: "ellipsis", - }), -} satisfies Record>; diff --git a/site/src/components/Sidebar/Sidebar.tsx b/site/src/components/Sidebar/Sidebar.tsx index 2f2e423ee39f1..f3e268267a508 100644 --- a/site/src/components/Sidebar/Sidebar.tsx +++ b/site/src/components/Sidebar/Sidebar.tsx @@ -1,44 +1,135 @@ -import Box, { BoxProps } from "@mui/material/Box"; -import { styled } from "@mui/styles"; -import { colors } from "theme/colors"; - -export const Sidebar = styled((props: BoxProps) => ( - -))(({ theme }) => ({ - width: 256, - flexShrink: 0, - borderRight: `1px solid ${theme.palette.divider}`, - height: "100%", - overflowY: "auto", -})); - -export const SidebarItem = styled( - ({ active, ...props }: BoxProps & { active?: boolean }) => ( - - ), -)(({ theme, active }) => ({ - background: active ? colors.gray[13] : "none", - border: "none", - fontSize: 14, - width: "100%", - textAlign: "left", - padding: "0 24px", - cursor: "pointer", - pointerEvents: active ? "none" : "auto", - color: active ? theme.palette.text.primary : theme.palette.text.secondary, - "&:hover": { - background: theme.palette.action.hover, - color: theme.palette.text.primary, +import { cx } from "@emotion/css"; +import { type CSSObject, type Interpolation, type Theme } from "@emotion/react"; +import { type ElementType, type FC, type ReactNode } from "react"; +import { Link, NavLink } from "react-router-dom"; +import { Stack } from "components/Stack/Stack"; +import { type ClassName, useClassName } from "hooks/useClassName"; + +interface SidebarProps { + children?: ReactNode; +} + +export const Sidebar: FC = ({ children }) => { + return ; +}; + +interface SidebarHeaderProps { + avatar: ReactNode; + title: ReactNode; + subtitle: ReactNode; + linkTo?: string; +} + +export const SidebarHeader: FC = ({ + avatar, + title, + subtitle, + linkTo, +}) => { + return ( + + {avatar} +
+ {linkTo ? ( + + {title} + + ) : ( + {title} + )} + {subtitle} +
+
+ ); +}; + +interface SidebarNavItemProps { + children?: ReactNode; + icon: ElementType; + href: string; +} + +export const SidebarNavItem: FC = ({ + children, + href, + icon: Icon, +}) => { + const link = useClassName(classNames.link, []); + const activeLink = useClassName(classNames.activeLink, []); + + return ( + cx([link, isActive && activeLink])} + > + + + {children} + + + ); +}; + +const styles = { + sidebar: { + width: 245, + flexShrink: 0, + }, + info: (theme) => ({ + ...(theme.typography.body2 as CSSObject), + marginBottom: 16, + }), + data: { + overflow: "hidden", }, - paddingTop: 10, - paddingBottom: 10, -})); - -export const SidebarCaption = styled(Box)(({ theme }) => ({ - fontSize: 10, - textTransform: "uppercase", - fontWeight: 500, - color: theme.palette.text.secondary, - padding: "12px 24px", - letterSpacing: "0.5px", -})); + title: (theme) => ({ + fontWeight: 600, + overflow: "hidden", + textOverflow: "ellipsis", + whiteSpace: "nowrap", + color: theme.palette.text.primary, + textDecoration: "none", + }), + subtitle: (theme) => ({ + color: theme.palette.text.secondary, + fontSize: 12, + overflow: "hidden", + textOverflow: "ellipsis", + }), +} satisfies Record>; + +const classNames = { + link: (css, theme) => css` + color: inherit; + display: block; + font-size: 14px; + text-decoration: none; + padding: 12px 12px 12px 16px; + border-radius: 4px; + transition: background-color 0.15s ease-in-out; + margin-bottom: 1px; + position: relative; + + &:hover { + background-color: ${theme.palette.action.hover}; + } + `, + + activeLink: (css, theme) => css` + background-color: ${theme.palette.action.hover}; + + &:before { + content: ""; + display: block; + width: 3px; + height: 100%; + position: absolute; + left: 0; + top: 0; + background-color: ${theme.palette.primary.main}; + border-top-left-radius: 8px; + border-bottom-left-radius: 8px; + } + `, +} satisfies Record; diff --git a/site/src/hooks/useClassName.ts b/site/src/hooks/useClassName.ts new file mode 100644 index 0000000000000..595308dead9ca --- /dev/null +++ b/site/src/hooks/useClassName.ts @@ -0,0 +1,21 @@ +/* eslint-disable react-hooks/exhaustive-deps -- false positives */ + +import { css } from "@emotion/css"; +import { type DependencyList, useMemo } from "react"; +import { type Theme, useTheme } from "@emotion/react"; + +export type ClassName = (cssFn: typeof css, theme: Theme) => string; + +/** + * An escape hatch for when you really need to manually pass around a + * `className`. Prefer using the `css` prop whenever possible. If you + * can't use that, then this might be helpful for you. + */ +export function useClassName(styles: ClassName, deps: DependencyList): string { + const theme = useTheme(); + const className = useMemo(() => { + return styles(css, theme); + }, [...deps, theme]); + + return className; +} diff --git a/site/src/pages/TemplateSettingsPage/Sidebar.tsx b/site/src/pages/TemplateSettingsPage/Sidebar.tsx index 88bf1e1882f25..4a7944a5d70a8 100644 --- a/site/src/pages/TemplateSettingsPage/Sidebar.tsx +++ b/site/src/pages/TemplateSettingsPage/Sidebar.tsx @@ -1,157 +1,42 @@ -import { css } from "@emotion/css"; -import { - useTheme, - type CSSObject, - type Interpolation, - type Theme, -} from "@emotion/react"; import ScheduleIcon from "@mui/icons-material/TimerOutlined"; import VariablesIcon from "@mui/icons-material/CodeOutlined"; -import type { Template } from "api/typesGenerated"; -import { Stack } from "components/Stack/Stack"; -import { - type FC, - type ElementType, - type PropsWithChildren, - type ReactNode, -} from "react"; -import { Link, NavLink } from "react-router-dom"; import GeneralIcon from "@mui/icons-material/SettingsOutlined"; import SecurityIcon from "@mui/icons-material/LockOutlined"; +import { type FC } from "react"; +import type { Template } from "api/typesGenerated"; import { Avatar } from "components/Avatar/Avatar"; -import { combineClasses } from "utils/combineClasses"; - -const SidebarNavItem: FC< - PropsWithChildren<{ href: string; icon: ReactNode }> -> = ({ children, href, icon }) => { - const theme = useTheme(); - - const sidebarNavItemStyles = css` - color: inherit; - display: block; - font-size: 14px; - text-decoration: none; - padding: 12px 12px 12px 16px; - border-radius: 4px; - transition: background-color 0.15s ease-in-out; - margin-bottom: 1px; - position: relative; - - &:hover { - background-color: ${theme.palette.action.hover}; - } - `; - - const sidebarNavItemActiveStyles = css` - background-color: ${theme.palette.action.hover}; - - &:before { - content: ""; - display: block; - width: 3px; - height: 100%; - position: absolute; - left: 0; - top: 0; - background-color: ${theme.palette.primary.main}; - border-top-left-radius: 8px; - border-bottom-left-radius: 8px; - } - `; - - return ( - - combineClasses([ - sidebarNavItemStyles, - isActive ? sidebarNavItemActiveStyles : undefined, - ]) - } - > - - {icon} - {children} - - - ); -}; +import { + Sidebar as BaseSidebar, + SidebarHeader, + SidebarNavItem, +} from "components/Sidebar/Sidebar"; -const SidebarNavItemIcon: React.FC<{ icon: ElementType }> = ({ - icon: Icon, -}) => { - return ; -}; +interface SidebarProps { + template: Template; +} -export const Sidebar: React.FC<{ template: Template }> = ({ template }) => { +export const Sidebar: FC = ({ template }) => { return ( - + ); }; - -const styles = { - sidebar: { - width: 245, - flexShrink: 0, - }, - sidebarNavItemIcon: { - width: 16, - height: 16, - }, - templateInfo: (theme) => ({ - ...(theme.typography.body2 as CSSObject), - marginBottom: 16, - }), - templateData: { - overflow: "hidden", - }, - name: (theme) => ({ - fontWeight: 600, - overflow: "hidden", - textOverflow: "ellipsis", - whiteSpace: "nowrap", - color: theme.palette.text.primary, - textDecoration: "none", - }), - secondary: (theme) => ({ - color: theme.palette.text.secondary, - fontSize: 12, - overflow: "hidden", - textOverflow: "ellipsis", - }), -} satisfies Record>; diff --git a/site/src/pages/WorkspaceBuildPage/Sidebar.tsx b/site/src/pages/WorkspaceBuildPage/Sidebar.tsx new file mode 100644 index 0000000000000..32472117163cd --- /dev/null +++ b/site/src/pages/WorkspaceBuildPage/Sidebar.tsx @@ -0,0 +1,80 @@ +import { type FC, type HTMLAttributes } from "react"; +import { colors } from "theme/colors"; + +export const Sidebar: FC> = ({ + children, + ...attrs +}) => { + return ( + + ); +}; + +interface SidebarItemProps extends HTMLAttributes { + active?: boolean; +} + +export const SidebarItem: FC = ({ + children, + active, + ...attrs +}) => { + return ( + + ); +}; + +export const SidebarCaption: FC> = ({ + children, + ...attrs +}) => { + return ( +
({ + fontSize: 10, + textTransform: "uppercase", + fontWeight: 500, + color: theme.palette.text.secondary, + padding: "12px 24px", + letterSpacing: "0.5px", + })} + {...attrs} + > + {children} +
+ ); +}; diff --git a/site/src/pages/WorkspaceBuildPage/WorkspaceBuildPageView.tsx b/site/src/pages/WorkspaceBuildPage/WorkspaceBuildPageView.tsx index e40c6b8fd5ea3..d9a2d3828b0a3 100644 --- a/site/src/pages/WorkspaceBuildPage/WorkspaceBuildPageView.tsx +++ b/site/src/pages/WorkspaceBuildPage/WorkspaceBuildPageView.tsx @@ -1,4 +1,4 @@ -import { type Interpolation, type Theme } from "@emotion/react"; +import { type Interpolation, type Theme, useTheme } from "@emotion/react"; import { BuildAvatar } from "components/BuildAvatar/BuildAvatar"; import { type FC } from "react"; import { ProvisionerJobLog, WorkspaceBuild } from "api/typesGenerated"; @@ -17,12 +17,7 @@ import { getDisplayWorkspaceBuildInitiatedBy, getDisplayWorkspaceBuildStatus, } from "utils/workspace"; -import Box from "@mui/material/Box"; -import { - Sidebar, - SidebarCaption, - SidebarItem, -} from "components/Sidebar/Sidebar"; +import { Sidebar, SidebarCaption, SidebarItem } from "./Sidebar"; import { BuildIcon } from "components/BuildIcon/BuildIcon"; import Skeleton from "@mui/material/Skeleton"; import { Alert } from "components/Alert/Alert"; @@ -48,6 +43,8 @@ export const WorkspaceBuildPageView: FC = ({ builds, activeBuildNumber, }) => { + const theme = useTheme(); + if (!build) { return ; } @@ -94,16 +91,16 @@ export const WorkspaceBuildPageView: FC = ({ css={styles.statsItem} label="Action" value={ - + {build.transition} - + } /> - = ({ ))} - +
{build.transition === "delete" && build.job.status === "failed" && ( theme.palette.error.dark, - borderBottom: (theme) => `1px solid ${theme.palette.divider}`, + background: theme.palette.error.dark, + borderBottom: `1px solid ${theme.palette.divider}`, }} > - +
The workspace may have failed to delete due to a Terraform state mismatch. A template admin may run{" "} - - ` {`coder rm ${ build.workspace_owner_name + "/" + build.workspace_name } --orphan`} - ` - {" "} + {" "} to delete the workspace skipping resource destruction. - +
)} {logs ? ( ) : ( )} - - +
+ ); }; -const BuildSidebarItem = ({ - build, - active, -}: { +interface BuildSidebarItemProps { build: WorkspaceBuild; active: boolean; -}) => { +} + +const BuildSidebarItem: FC = ({ build, active }) => { + const theme = useTheme(); + const statusType = getDisplayWorkspaceBuildStatus(theme, build).type; + return ( - +
- theme.palette[getDisplayWorkspaceBuildStatus(theme, build).type] - .light, + color: theme.palette[statusType].light, }} /> - - +
theme.palette.text.primary, + color: theme.palette.text.primary, textOverflow: "ellipsis", overflow: "hidden", whiteSpace: "nowrap", @@ -207,33 +203,38 @@ const BuildSidebarItem = ({ > {build.transition} by{" "} {getDisplayWorkspaceBuildInitiatedBy(build)} - - +
theme.palette.text.secondary, - mt: 0.25, + color: theme.palette.text.secondary, + marginTop: 2, }} > {displayWorkspaceBuildDuration(build)} - - - +
+
+
); }; -const BuildSidebarItemSkeleton = () => { +const BuildSidebarItemSkeleton: FC = () => { return ( - +
- +
- - - + +
+
); }; diff --git a/site/src/pages/WorkspaceSettingsPage/Sidebar.tsx b/site/src/pages/WorkspaceSettingsPage/Sidebar.tsx index 53b73579d5220..c80f65069ce50 100644 --- a/site/src/pages/WorkspaceSettingsPage/Sidebar.tsx +++ b/site/src/pages/WorkspaceSettingsPage/Sidebar.tsx @@ -1,142 +1,41 @@ -import { css } from "@emotion/css"; -import { - useTheme, - type CSSObject, - type Interpolation, - type Theme, -} from "@emotion/react"; import ScheduleIcon from "@mui/icons-material/TimerOutlined"; import type { Workspace } from "api/typesGenerated"; -import { Stack } from "components/Stack/Stack"; -import { type FC, type PropsWithChildren, type ReactNode } from "react"; -import { Link, NavLink } from "react-router-dom"; -import { combineClasses } from "utils/combineClasses"; +import { type FC } from "react"; import GeneralIcon from "@mui/icons-material/SettingsOutlined"; import ParameterIcon from "@mui/icons-material/CodeOutlined"; import { Avatar } from "components/Avatar/Avatar"; +import { + Sidebar as BaseSidebar, + SidebarHeader, + SidebarNavItem, +} from "components/Sidebar/Sidebar"; -const SidebarNavItem: FC< - PropsWithChildren<{ href: string; icon: ReactNode }> -> = ({ children, href, icon }) => { - const theme = useTheme(); - - const linkStyles = css({ - color: "inherit", - display: "block", - fontSize: 14, - textDecoration: "none", - padding: "12px 12px 12px 16px", - borderRadius: 4, - transition: "background-color 0.15s ease-in-out", - marginBottom: 1, - position: "relative", - - "&:hover": { - backgroundColor: theme.palette.action.hover, - }, - }); - - const activeLinkStyles = css({ - backgroundColor: theme.palette.action.hover, - - "&:before": { - content: '""', - display: "block", - width: 3, - height: "100%", - position: "absolute", - left: 0, - top: 0, - backgroundColor: theme.palette.primary.main, - borderTopLeftRadius: 8, - borderBottomLeftRadius: 8, - }, - }); - - return ( - - combineClasses([linkStyles, isActive ? activeLinkStyles : undefined]) - } - > - - {icon} - {children} - - - ); -}; +interface SidebarProps { + username: string; + workspace: Workspace; +} -export const Sidebar: FC<{ username: string; workspace: Workspace }> = ({ - username, - workspace, -}) => { +export const Sidebar: FC = ({ username, workspace }) => { return ( - + ); }; - -const styles = { - sidebar: { - width: 245, - flexShrink: 0, - }, - sidebarItemIcon: { - width: 16, - height: 16, - }, - workspaceInfo: (theme) => ({ - ...(theme.typography.body2 as CSSObject), - marginBottom: 16, - }), - workspaceData: { - overflow: "hidden", - }, - name: (theme) => ({ - fontWeight: 600, - overflow: "hidden", - textOverflow: "ellipsis", - whiteSpace: "nowrap", - color: theme.palette.text.primary, - textDecoration: "none", - }), - secondary: (theme) => ({ - color: theme.palette.text.secondary, - fontSize: 12, - overflow: "hidden", - textOverflow: "ellipsis", - }), -} satisfies Record>;