refactor: improve settings sidebar components (#10801)

This commit is contained in:
Kayla Washburn
2023-11-21 09:38:55 -07:00
committed by GitHub
parent 7fa70ce159
commit e51eeb67ce
9 changed files with 377 additions and 598 deletions
@@ -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";
import { GitIcon } from "components/Icons/GitIcon";
import {
Sidebar as BaseSidebar,
SidebarNavItem,
} from "components/Sidebar/Sidebar";
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 (
<NavLink
to={href}
className={({ isActive }) => 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};
}
`}
>
<Stack alignItems="center" spacing={1.5} direction="row">
{icon}
{children}
</Stack>
</NavLink>
);
};
const SidebarNavItemIcon: FC<{ icon: ElementType }> = ({ icon: Icon }) => {
return <Icon css={{ width: 16, height: 16 }} />;
};
export const Sidebar: React.FC = () => {
export const Sidebar: FC = () => {
const dashboard = useDashboard();
return (
<nav css={{ width: 245 }}>
<SidebarNavItem
href="general"
icon={<SidebarNavItemIcon icon={LaunchOutlined} />}
>
<BaseSidebar>
<SidebarNavItem href="general" icon={LaunchOutlined}>
General
</SidebarNavItem>
<SidebarNavItem
href="licenses"
icon={<SidebarNavItemIcon icon={ApprovalIcon} />}
>
<SidebarNavItem href="licenses" icon={ApprovalIcon}>
Licenses
</SidebarNavItem>
<SidebarNavItem
href="appearance"
icon={<SidebarNavItemIcon icon={Brush} />}
>
<SidebarNavItem href="appearance" icon={Brush}>
Appearance
</SidebarNavItem>
<SidebarNavItem
href="userauth"
icon={<SidebarNavItemIcon icon={VpnKeyOutlined} />}
>
<SidebarNavItem href="userauth" icon={VpnKeyOutlined}>
User Authentication
</SidebarNavItem>
<SidebarNavItem
href="external-auth"
icon={<SidebarNavItemIcon icon={GitIcon} />}
>
<SidebarNavItem href="external-auth" icon={GitIcon}>
External Authentication
</SidebarNavItem>
<SidebarNavItem href="network" icon={<SidebarNavItemIcon icon={Globe} />}>
<SidebarNavItem href="network" icon={Globe}>
Network
</SidebarNavItem>
{dashboard.experiments.includes("moons") && (
<SidebarNavItem
href="workspace-proxies"
icon={<SidebarNavItemIcon icon={HubOutlinedIcon} />}
>
<SidebarNavItem href="workspace-proxies" icon={HubOutlinedIcon}>
Workspace Proxies
</SidebarNavItem>
)}
<SidebarNavItem
href="security"
icon={<SidebarNavItemIcon icon={LockRounded} />}
>
<SidebarNavItem href="security" icon={LockRounded}>
Security
</SidebarNavItem>
<SidebarNavItem
href="observability"
icon={<SidebarNavItemIcon icon={InsertChartIcon} />}
>
<SidebarNavItem href="observability" icon={InsertChartIcon}>
Observability
</SidebarNavItem>
{dashboard.experiments.includes("deployment_health_page") && (
<SidebarNavItem
href="/health"
icon={<SidebarNavItemIcon icon={MonitorHeartOutlined} />}
>
<SidebarNavItem href="/health" icon={MonitorHeartOutlined}>
Health
</SidebarNavItem>
)}
</nav>
</BaseSidebar>
);
};
@@ -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<PropsWithChildren<SectionProps>>;
export const Section: SectionFC = ({
export const Section: FC<SectionProps> = ({
id,
title,
description,
+20 -134
View File
@@ -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 (
<NavLink
to={href}
className={({ isActive }) =>
combineClasses([
sidebarNavItemStyles,
isActive ? sidebarNavItemActiveStyles : undefined,
])
}
>
<Stack alignItems="center" spacing={1.5} direction="row">
{icon}
{children}
</Stack>
</NavLink>
);
};
const SidebarNavItemIcon: React.FC<{
icon: ComponentType<{ className?: string }>;
}> = ({ icon: Icon }) => {
return <Icon css={{ width: 16, height: 16 }} />;
};
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 (
<nav css={styles.sidebar}>
<Stack direction="row" alignItems="center" css={styles.userInfo}>
<UserAvatar username={user.username} avatarURL={user.avatar_url} />
<Stack spacing={0} css={styles.userData}>
<span css={styles.username}>{user.username}</span>
<span css={styles.email}>{user.email}</span>
</Stack>
</Stack>
<SidebarNavItem
href="account"
icon={<SidebarNavItemIcon icon={AccountIcon} />}
>
<BaseSidebar>
<SidebarHeader
avatar={
<UserAvatar username={user.username} avatarURL={user.avatar_url} />
}
title={user.username}
subtitle={user.email}
/>
;
<SidebarNavItem href="account" icon={AccountIcon}>
Account
</SidebarNavItem>
{allowAutostopRequirement && (
<SidebarNavItem
href="schedule"
icon={<SidebarNavItemIcon icon={ScheduleIcon} />}
>
<SidebarNavItem href="schedule" icon={ScheduleIcon}>
Schedule
</SidebarNavItem>
)}
<SidebarNavItem
href="security"
icon={<SidebarNavItemIcon icon={SecurityIcon} />}
>
<SidebarNavItem href="security" icon={SecurityIcon}>
Security
</SidebarNavItem>
<SidebarNavItem
href="ssh-keys"
icon={<SidebarNavItemIcon icon={FingerprintOutlinedIcon} />}
>
<SidebarNavItem href="ssh-keys" icon={FingerprintOutlinedIcon}>
SSH Keys
</SidebarNavItem>
<SidebarNavItem
href="tokens"
icon={<SidebarNavItemIcon icon={VpnKeyOutlined} />}
>
<SidebarNavItem href="tokens" icon={VpnKeyOutlined}>
Tokens
</SidebarNavItem>
</nav>
</BaseSidebar>
);
};
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<string, Interpolation<Theme>>;
+131 -40
View File
@@ -1,44 +1,135 @@
import Box, { BoxProps } from "@mui/material/Box";
import { styled } from "@mui/styles";
import { colors } from "theme/colors";
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";
export const Sidebar = styled((props: BoxProps) => (
<Box {...props} component="nav" />
))(({ theme }) => ({
width: 256,
flexShrink: 0,
borderRight: `1px solid ${theme.palette.divider}`,
height: "100%",
overflowY: "auto",
}));
interface SidebarProps {
children?: ReactNode;
}
export const SidebarItem = styled(
({ active, ...props }: BoxProps & { active?: boolean }) => (
<Box component="button" {...props} />
),
)(({ 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,
export const Sidebar: FC<SidebarProps> = ({ children }) => {
return <nav css={styles.sidebar}>{children}</nav>;
};
interface SidebarHeaderProps {
avatar: ReactNode;
title: ReactNode;
subtitle: ReactNode;
linkTo?: string;
}
export const SidebarHeader: FC<SidebarHeaderProps> = ({
avatar,
title,
subtitle,
linkTo,
}) => {
return (
<Stack direction="row" alignItems="center" css={styles.info}>
{avatar}
<div css={styles.data}>
{linkTo ? (
<Link css={styles.title} to={linkTo}>
{title}
</Link>
) : (
<span css={styles.title}>{title}</span>
)}
<span css={styles.subtitle}>{subtitle}</span>
</div>
</Stack>
);
};
interface SidebarNavItemProps {
children?: ReactNode;
icon: ElementType;
href: string;
}
export const SidebarNavItem: FC<SidebarNavItemProps> = ({
children,
href,
icon: Icon,
}) => {
const link = useClassName(classNames.link, []);
const activeLink = useClassName(classNames.activeLink, []);
return (
<NavLink
end
to={href}
className={({ isActive }) => cx([link, isActive && activeLink])}
>
<Stack alignItems="center" spacing={1.5} direction="row">
<Icon css={{ width: 16, height: 16 }} />
{children}
</Stack>
</NavLink>
);
};
const styles = {
sidebar: {
width: 245,
flexShrink: 0,
},
paddingTop: 10,
paddingBottom: 10,
}));
info: (theme) => ({
...(theme.typography.body2 as CSSObject),
marginBottom: 16,
}),
data: {
overflow: "hidden",
},
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<string, Interpolation<Theme>>;
export const SidebarCaption = styled(Box)(({ theme }) => ({
fontSize: 10,
textTransform: "uppercase",
fontWeight: 500,
color: theme.palette.text.secondary,
padding: "12px 24px",
letterSpacing: "0.5px",
}));
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<string, ClassName>;
+21
View File
@@ -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;
}
+23 -138
View File
@@ -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";
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 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;
}
`;
interface SidebarProps {
template: Template;
}
export const Sidebar: FC<SidebarProps> = ({ template }) => {
return (
<NavLink
end
to={href}
className={({ isActive }) =>
combineClasses([
sidebarNavItemStyles,
isActive ? sidebarNavItemActiveStyles : undefined,
])
}
>
<Stack alignItems="center" spacing={1.5} direction="row">
{icon}
{children}
</Stack>
</NavLink>
);
};
<BaseSidebar>
<SidebarHeader
avatar={<Avatar src={template.icon} variant="square" fitImage />}
title={template.display_name || template.name}
linkTo={`/templates/${template.name}`}
subtitle={template.name}
/>
const SidebarNavItemIcon: React.FC<{ icon: ElementType }> = ({
icon: Icon,
}) => {
return <Icon css={styles.sidebarNavItemIcon} />;
};
export const Sidebar: React.FC<{ template: Template }> = ({ template }) => {
return (
<nav css={styles.sidebar}>
<Stack direction="row" alignItems="center" css={styles.templateInfo}>
<Avatar src={template.icon} variant="square" fitImage />
<Stack spacing={0} css={styles.templateData}>
<Link css={styles.name} to={`/templates/${template.name}`}>
{template.display_name !== ""
? template.display_name
: template.name}
</Link>
<span css={styles.secondary}>{template.name}</span>
</Stack>
</Stack>
<SidebarNavItem href="" icon={<SidebarNavItemIcon icon={GeneralIcon} />}>
<SidebarNavItem href="" icon={GeneralIcon}>
General
</SidebarNavItem>
<SidebarNavItem
href="permissions"
icon={<SidebarNavItemIcon icon={SecurityIcon} />}
>
<SidebarNavItem href="permissions" icon={SecurityIcon}>
Permissions
</SidebarNavItem>
<SidebarNavItem
href="variables"
icon={<SidebarNavItemIcon icon={VariablesIcon} />}
>
<SidebarNavItem href="variables" icon={VariablesIcon}>
Variables
</SidebarNavItem>
<SidebarNavItem
href="schedule"
icon={<SidebarNavItemIcon icon={ScheduleIcon} />}
>
<SidebarNavItem href="schedule" icon={ScheduleIcon}>
Schedule
</SidebarNavItem>
</nav>
</BaseSidebar>
);
};
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<string, Interpolation<Theme>>;
@@ -0,0 +1,80 @@
import { type FC, type HTMLAttributes } from "react";
import { colors } from "theme/colors";
export const Sidebar: FC<HTMLAttributes<HTMLElement>> = ({
children,
...attrs
}) => {
return (
<nav
css={(theme) => ({
width: 256,
flexShrink: 0,
borderRight: `1px solid ${theme.palette.divider}`,
height: "100%",
overflowY: "auto",
})}
{...attrs}
>
{children}
</nav>
);
};
interface SidebarItemProps extends HTMLAttributes<HTMLElement> {
active?: boolean;
}
export const SidebarItem: FC<SidebarItemProps> = ({
children,
active,
...attrs
}) => {
return (
<button
css={(theme) => ({
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,
},
paddingTop: 10,
paddingBottom: 10,
})}
{...attrs}
>
{children}
</button>
);
};
export const SidebarCaption: FC<HTMLAttributes<HTMLDivElement>> = ({
children,
...attrs
}) => {
return (
<div
css={(theme) => ({
fontSize: 10,
textTransform: "uppercase",
fontWeight: 500,
color: theme.palette.text.secondary,
padding: "12px 24px",
letterSpacing: "0.5px",
})}
{...attrs}
>
{children}
</div>
);
};
@@ -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<WorkspaceBuildPageViewProps> = ({
builds,
activeBuildNumber,
}) => {
const theme = useTheme();
if (!build) {
return <Loader />;
}
@@ -94,16 +91,16 @@ export const WorkspaceBuildPageView: FC<WorkspaceBuildPageViewProps> = ({
css={styles.statsItem}
label="Action"
value={
<Box component="span" sx={{ textTransform: "capitalize" }}>
<span css={{ textTransform: "capitalize" }}>
{build.transition}
</Box>
</span>
}
/>
</Stats>
</FullWidthPageHeader>
<Box
sx={{
<div
css={{
display: "flex",
alignItems: "start",
overflow: "hidden",
@@ -127,79 +124,78 @@ export const WorkspaceBuildPageView: FC<WorkspaceBuildPageViewProps> = ({
))}
</Sidebar>
<Box sx={{ height: "100%", overflowY: "auto", width: "100%" }}>
<div css={{ height: "100%", overflowY: "auto", width: "100%" }}>
{build.transition === "delete" && build.job.status === "failed" && (
<Alert
severity="error"
sx={{
css={{
borderRadius: 0,
border: 0,
background: (theme) => theme.palette.error.dark,
borderBottom: (theme) => `1px solid ${theme.palette.divider}`,
background: theme.palette.error.dark,
borderBottom: `1px solid ${theme.palette.divider}`,
}}
>
<Box>
<div>
The workspace may have failed to delete due to a Terraform state
mismatch. A template admin may run{" "}
<Box
component="code"
display="inline-block"
width="fit-content"
fontWeight={600}
<code
css={{
display: "inline-block",
width: "fit-content",
fontWeight: 600,
}}
>
`
{`coder rm ${
build.workspace_owner_name + "/" + build.workspace_name
} --orphan`}
`
</Box>{" "}
</code>{" "}
to delete the workspace skipping resource destruction.
</Box>
</div>
</Alert>
)}
{logs ? (
<WorkspaceBuildLogs
sx={{ border: 0 }}
css={{ border: 0 }}
logs={sortLogsByCreatedAt(logs)}
/>
) : (
<Loader />
)}
</Box>
</Box>
</div>
</div>
</DashboardFullPage>
);
};
const BuildSidebarItem = ({
build,
active,
}: {
interface BuildSidebarItemProps {
build: WorkspaceBuild;
active: boolean;
}) => {
}
const BuildSidebarItem: FC<BuildSidebarItemProps> = ({ build, active }) => {
const theme = useTheme();
const statusType = getDisplayWorkspaceBuildStatus(theme, build).type;
return (
<Link
key={build.id}
to={`/@${build.workspace_owner_name}/${build.workspace_name}/builds/${build.build_number}`}
>
<SidebarItem active={active}>
<Box sx={{ display: "flex", alignItems: "start", gap: 1 }}>
<div css={{ display: "flex", alignItems: "start", gap: 8 }}>
<BuildIcon
transition={build.transition}
sx={{
css={{
width: 16,
height: 16,
color: (theme) =>
theme.palette[getDisplayWorkspaceBuildStatus(theme, build).type]
.light,
color: theme.palette[statusType].light,
}}
/>
<Box sx={{ overflow: "hidden" }}>
<Box
sx={{
<div css={{ overflow: "hidden" }}>
<div
css={{
textTransform: "capitalize",
color: (theme) => theme.palette.text.primary,
color: theme.palette.text.primary,
textOverflow: "ellipsis",
overflow: "hidden",
whiteSpace: "nowrap",
@@ -207,33 +203,38 @@ const BuildSidebarItem = ({
>
{build.transition} by{" "}
<strong>{getDisplayWorkspaceBuildInitiatedBy(build)}</strong>
</Box>
<Box
sx={{
</div>
<div
css={{
fontSize: 12,
color: (theme) => theme.palette.text.secondary,
mt: 0.25,
color: theme.palette.text.secondary,
marginTop: 2,
}}
>
{displayWorkspaceBuildDuration(build)}
</Box>
</Box>
</Box>
</div>
</div>
</div>
</SidebarItem>
</Link>
);
};
const BuildSidebarItemSkeleton = () => {
const BuildSidebarItemSkeleton: FC = () => {
return (
<SidebarItem>
<Box sx={{ display: "flex", alignItems: "start", gap: 1 }}>
<div css={{ display: "flex", alignItems: "start", gap: 8 }}>
<Skeleton variant="circular" width={16} height={16} />
<Box>
<div>
<Skeleton variant="text" width={94} height={16} />
<Skeleton variant="text" width={60} height={14} sx={{ mt: 0.25 }} />
</Box>
</Box>
<Skeleton
variant="text"
width={60}
height={14}
css={{ marginTop: 2 }}
/>
</div>
</div>
</SidebarItem>
);
};
+24 -125
View File
@@ -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,
},
});
interface SidebarProps {
username: string;
workspace: Workspace;
}
export const Sidebar: FC<SidebarProps> = ({ username, workspace }) => {
return (
<NavLink
end
to={href}
className={({ isActive }) =>
combineClasses([linkStyles, isActive ? activeLinkStyles : undefined])
}
>
<Stack alignItems="center" spacing={1.5} direction="row">
{icon}
{children}
</Stack>
</NavLink>
);
};
<BaseSidebar>
<SidebarHeader
avatar={
<Avatar src={workspace.template_icon} variant="square" fitImage />
}
title={workspace.name}
linkTo={`/@${username}/${workspace.name}`}
subtitle={workspace.template_display_name ?? workspace.template_name}
/>
export const Sidebar: FC<{ username: string; workspace: Workspace }> = ({
username,
workspace,
}) => {
return (
<nav css={styles.sidebar}>
<Stack direction="row" alignItems="center" css={styles.workspaceInfo}>
<Avatar src={workspace.template_icon} variant="square" fitImage />
<Stack spacing={0} css={styles.workspaceData}>
<Link css={styles.name} to={`/@${username}/${workspace.name}`}>
{workspace.name}
</Link>
<span css={styles.secondary}>
{workspace.template_display_name ?? workspace.template_name}
</span>
</Stack>
</Stack>
<SidebarNavItem
href=""
icon={<GeneralIcon css={styles.sidebarItemIcon} />}
>
<SidebarNavItem href="" icon={GeneralIcon}>
General
</SidebarNavItem>
<SidebarNavItem
href="parameters"
icon={<ParameterIcon css={styles.sidebarItemIcon} />}
>
<SidebarNavItem href="parameters" icon={ParameterIcon}>
Parameters
</SidebarNavItem>
<SidebarNavItem
href="schedule"
icon={<ScheduleIcon css={styles.sidebarItemIcon} />}
>
<SidebarNavItem href="schedule" icon={ScheduleIcon}>
Schedule
</SidebarNavItem>
</nav>
</BaseSidebar>
);
};
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<string, Interpolation<Theme>>;