mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
refactor: improve settings sidebar components (#10801)
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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>>;
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>>;
|
||||
|
||||
Reference in New Issue
Block a user