fix: implement proper overflow behavior for workspace history (#19340)

Before:


https://github.com/user-attachments/assets/2f1ff75c-4916-4d0a-a657-004d46691ea0


After:


https://github.com/user-attachments/assets/a8e575b5-84f9-4eea-b318-93d2a3d60aa5

Also converts a lot of emotion components to tailwind
This commit is contained in:
Brett Kolodny
2025-08-14 09:26:20 -04:00
committed by GitHub
parent 734299de71
commit 5b5fbbed33
5 changed files with 186 additions and 292 deletions
+26 -73
View File
@@ -1,30 +1,28 @@
import { type Interpolation, type Theme, useTheme } from "@emotion/react";
import type { ComponentProps, FC, HTMLAttributes } from "react";
import { Link, type LinkProps } from "react-router";
import { cn } from "utils/cn";
import { TopbarIconButton } from "./Topbar";
export const Sidebar: FC<HTMLAttributes<HTMLDivElement>> = (props) => {
const theme = useTheme();
return (
<div
css={{
width: 260,
borderRight: `1px solid ${theme.palette.divider}`,
height: "100%",
overflow: "auto",
flexShrink: 0,
padding: "8px 0",
display: "flex",
flexDirection: "column",
gap: 1,
}}
// TODO: Remove extra border classes once MUI is removed
className="flex flex-col gap-px w-64 border-solid border-0 border-r border-r-border h-full py-2 shrink-0 overflow-y-auto"
{...props}
/>
);
};
export const SidebarLink: FC<LinkProps> = (props) => {
return <Link css={styles.sidebarItem} {...props} />;
export const SidebarLink: FC<LinkProps> = ({ className, ...props }) => {
return (
<Link
className={cn(
"text-[13px] text-content-primary py-2 px-4 text-left bg-transparent hover:divide-surface-tertiary cursor-pointer border-0 no-underline",
className,
)}
{...props}
/>
);
};
interface SidebarItemProps extends HTMLAttributes<HTMLButtonElement> {
@@ -33,21 +31,16 @@ interface SidebarItemProps extends HTMLAttributes<HTMLButtonElement> {
export const SidebarItem: FC<SidebarItemProps> = ({
isActive,
className,
...buttonProps
}) => {
const theme = useTheme();
return (
<button
css={[
styles.sidebarItem,
{ opacity: "0.75", "&:hover": { opacity: 1 } },
isActive && {
background: theme.palette.action.selected,
opacity: 1,
pointerEvents: "none",
},
]}
className={cn(
"text-[13px] text-content-primary py-2 px-4 text-left bg-transparent hover:divide-surface-tertiary opacity-75 hover:opacity-100 cursor-pointer border-0",
isActive && "opacity-100 bg-surface-tertiary",
className,
)}
{...buttonProps}
/>
);
@@ -56,15 +49,7 @@ export const SidebarItem: FC<SidebarItemProps> = ({
export const SidebarCaption: FC<HTMLAttributes<HTMLSpanElement>> = (props) => {
return (
<span
css={{
fontSize: 10,
lineHeight: 1.2,
padding: "12px 16px",
display: "block",
textTransform: "uppercase",
fontWeight: 500,
letterSpacing: "0.1em",
}}
className="text-[10px] leading-tight py-3 px-4 uppercase font-medium text-content-primary tracking-widest"
{...props}
/>
);
@@ -76,49 +61,17 @@ interface SidebarIconButton extends ComponentProps<typeof TopbarIconButton> {
export const SidebarIconButton: FC<SidebarIconButton> = ({
isActive,
className,
...buttonProps
}) => {
return (
<TopbarIconButton
css={[
{ opacity: 0.75, "&:hover": { opacity: 1 } },
isActive && styles.activeSidebarIconButton,
]}
className={cn(
"opacity-75 hover:opacity-100 border-0 border-x-2 border-x-transparent border-solid",
isActive && "opacity-100 relative border-l-sky-400",
className,
)}
{...buttonProps}
/>
);
};
const styles = {
sidebarItem: (theme) => ({
fontSize: 13,
lineHeight: 1.2,
color: theme.palette.text.primary,
textDecoration: "none",
padding: "8px 16px",
display: "block",
textAlign: "left",
background: "none",
border: 0,
cursor: "pointer",
"&:hover": {
backgroundColor: theme.palette.action.hover,
},
}),
activeSidebarIconButton: (theme) => ({
opacity: 1,
position: "relative",
"&::before": {
content: '""',
position: "absolute",
left: 0,
top: 0,
bottom: 0,
width: 2,
backgroundColor: theme.palette.primary.main,
height: "100%",
},
}),
} satisfies Record<string, Interpolation<Theme>>;
@@ -23,10 +23,10 @@ export const DashboardLayout: FC = () => {
{canViewDeployment && <LicenseBanner />}
<AnnouncementBanners />
<div className="flex flex-col min-h-full">
<div className="flex flex-col h-screen">
<Navbar />
<div className="flex flex-col flex-1 pb-12">
<div className="flex flex-col flex-1 min-h-0">
<Suspense fallback={<Loader />}>
<Outlet />
</Suspense>
@@ -51,7 +51,7 @@ export const NavbarView: FC<NavbarViewProps> = ({
const webPush = useWebpushNotifications();
return (
<div className="border-0 border-b border-solid h-[72px] flex items-center leading-none px-6">
<div className="border-0 border-b border-solid h-[72px] min-h-[72px] flex items-center leading-none px-6">
<NavLink to="/workspaces">
{logo_url ? (
<ExternalImage className="h-7" src={logo_url} alt="Custom Logo" />
+34 -29
View File
@@ -8,6 +8,7 @@ import {
SidebarItem,
SidebarLink,
} from "components/FullPageLayout/Sidebar";
import { ScrollArea } from "components/ScrollArea/ScrollArea";
import { Spinner } from "components/Spinner/Spinner";
import {
WorkspaceBuildData,
@@ -30,36 +31,40 @@ export const HistorySidebar: FC<HistorySidebarProps> = ({ workspace }) => {
return (
<Sidebar>
<SidebarCaption>History</SidebarCaption>
{builds
? builds.map((build) => (
<SidebarLink
target="_blank"
key={build.id}
to={`/@${build.workspace_owner_name}/${build.workspace_name}/builds/${build.build_number}`}
>
<WorkspaceBuildData build={build} />
</SidebarLink>
))
: Array.from({ length: 15 }, (_, i) => (
<SidebarItem key={i}>
<WorkspaceBuildDataSkeleton />
</SidebarItem>
))}
{buildsQuery.hasNextPage && (
<div css={{ padding: 16 }}>
<Button
onClick={() => buildsQuery.fetchNextPage()}
disabled={buildsQuery.isFetchingNextPage}
variant="outline"
className="w-full"
>
<Spinner loading={buildsQuery.isFetchingNextPage}>
<ArrowDownwardOutlined />
</Spinner>
Show more builds
</Button>
<ScrollArea>
<div className="flex flex-col gap-px">
{builds
? builds.map((build) => (
<SidebarLink
target="_blank"
key={build.id}
to={`/@${build.workspace_owner_name}/${build.workspace_name}/builds/${build.build_number}`}
>
<WorkspaceBuildData build={build} />
</SidebarLink>
))
: Array.from({ length: 15 }, (_, i) => (
<SidebarItem key={i}>
<WorkspaceBuildDataSkeleton />
</SidebarItem>
))}
{buildsQuery.hasNextPage && (
<div css={{ padding: 16 }}>
<Button
onClick={() => buildsQuery.fetchNextPage()}
disabled={buildsQuery.isFetchingNextPage}
variant="outline"
className="w-full"
>
<Spinner loading={buildsQuery.isFetchingNextPage}>
<ArrowDownwardOutlined />
</Spinner>
Show more builds
</Button>
</div>
)}
</div>
)}
</ScrollArea>
</Sidebar>
);
};
+123 -187
View File
@@ -1,5 +1,3 @@
import type { Interpolation, Theme } from "@emotion/react";
import { useTheme } from "@emotion/react";
import HistoryOutlined from "@mui/icons-material/HistoryOutlined";
import HubOutlined from "@mui/icons-material/HubOutlined";
import AlertTitle from "@mui/material/AlertTitle";
@@ -68,7 +66,6 @@ export const Workspace: FC<WorkspaceProps> = ({
handleDebug,
}) => {
const navigate = useNavigate();
const theme = useTheme();
const transitionStats =
template !== undefined ? ActiveTransition(template, workspace) : undefined;
@@ -100,18 +97,7 @@ export const Workspace: FC<WorkspaceProps> = ({
workspacePending && !haveBuildLogs && !provisionersHealthy && !isRestarting;
return (
<div
css={{
flex: 1,
display: "grid",
gridTemplate: `
"topbar topbar topbar" auto
"leftbar sidebar content" 1fr / auto auto 1fr
`,
// We need this to make the sidebar scrollable
overflow: "hidden",
}}
>
<div className="flex flex-col flex-1 min-h-0">
<WorkspaceTopbar
workspace={workspace}
template={template}
@@ -130,153 +116,133 @@ export const Workspace: FC<WorkspaceProps> = ({
handleToggleFavorite={handleToggleFavorite}
/>
<div
css={{
gridArea: "leftbar",
height: "100%",
overflowY: "auto",
borderRight: `1px solid ${theme.palette.divider}`,
display: "flex",
flexDirection: "column",
}}
>
<SidebarIconButton
isActive={sidebarOption.value === "resources"}
onClick={() => {
setSidebarOption("resources");
}}
>
<HubOutlined />
</SidebarIconButton>
<SidebarIconButton
isActive={sidebarOption.value === "history"}
onClick={() => {
setSidebarOption("history");
}}
>
<HistoryOutlined />
</SidebarIconButton>
</div>
{sidebarOption.value === "resources" && (
<ResourcesSidebar
failed={workspace.latest_build.status === "failed"}
resources={resources}
isSelected={resourcesNav.isSelected}
onChange={resourcesNav.select}
/>
)}
{sidebarOption.value === "history" && (
<HistorySidebar workspace={workspace} />
)}
<div css={[styles.content, styles.dotsBackground]}>
{selectedResource && (
<ResourceMetadata
resource={selectedResource}
css={{ margin: "-32px -32px 0 -32px", marginBottom: 24 }}
/>
)}
<div
css={{
display: "flex",
flexDirection: "column",
gap: 24,
maxWidth: 24 * 50,
margin: "auto",
}}
>
{workspace.latest_build.status === "deleted" && (
<WorkspaceDeletedBanner
handleClick={() => navigate("/templates")}
/>
)}
{shouldShowProvisionerAlert && (
<ProvisionerStatusAlert
matchingProvisioners={
workspace.latest_build.matched_provisioners?.count
}
availableProvisioners={
workspace.latest_build.matched_provisioners?.available ?? 0
}
tags={workspace.latest_build.job.tags}
/>
)}
{workspace.latest_build.job.error && (
<Alert severity="error">
<AlertTitle>Workspace build failed</AlertTitle>
<AlertDetail>{workspace.latest_build.job.error}</AlertDetail>
</Alert>
)}
{transitionStats !== undefined && (
<WorkspaceBuildProgress
workspace={workspace}
transitionStats={transitionStats}
/>
)}
{shouldShowBuildLogs && (
<WorkspaceBuildLogsSection logs={buildLogs} />
)}
{selectedResource && (
<section
css={{
display: "flex",
flexDirection: "column",
gap: 24,
flexGrow: 1,
minWidth: 0 /* Prevent overflow */,
<div className="flex flex-1 min-h-0">
<div className="flex">
<div className="flex flex-col h-full overflow-y-auto border-solid border-0 border-r border-r-border">
<SidebarIconButton
isActive={sidebarOption.value === "resources"}
onClick={() => {
setSidebarOption("resources");
}}
>
{selectedResource.agents
// If an agent has a `parent_id`, that means it is
// child of another agent. We do not want these agents
// to be displayed at the top-level on this page. We
// want them to display _as children_ of their parents.
?.filter((agent) => agent.parent_id === null)
.map((agent) => (
<AgentRow
key={agent.id}
agent={agent}
subAgents={selectedResource.agents?.filter(
(a) => a.parent_id === agent.id,
)}
workspace={workspace}
template={template}
onUpdateAgent={handleUpdate} // On updating the workspace the agent version is also updated
/>
))}
<HubOutlined />
</SidebarIconButton>
<SidebarIconButton
isActive={sidebarOption.value === "history"}
onClick={() => {
setSidebarOption("history");
}}
>
<HistoryOutlined />
</SidebarIconButton>
</div>
{(!selectedResource.agents ||
selectedResource.agents?.length === 0) && (
<div
css={{
display: "flex",
justifyContent: "center",
alignItems: "center",
width: "100%",
height: "100%",
}}
>
<div>
<h4 css={{ fontSize: 16, fontWeight: 500 }}>
No agents are currently assigned to this resource.
</h4>
</div>
</div>
)}
</section>
{sidebarOption.value === "resources" && (
<ResourcesSidebar
failed={workspace.latest_build.status === "failed"}
resources={resources}
isSelected={resourcesNav.isSelected}
onChange={resourcesNav.select}
/>
)}
{sidebarOption.value === "history" && (
<HistorySidebar workspace={workspace} />
)}
</div>
<WorkspaceTimings
provisionerTimings={timings?.provisioner_timings}
agentScriptTimings={timings?.agent_script_timings}
agentConnectionTimings={timings?.agent_connection_timings}
/>
<div
style={{
background: `radial-gradient(
circle at 1px 1px,
hsl(var(--surface-invert-secondary)) 0,
transparent 1px
) -2px -2px / 16px 16px`,
}}
className="p-8 overflow-y-auto relative w-full"
>
{selectedResource && (
<ResourceMetadata
resource={selectedResource}
className="-mx-8 -mt-8 mb-6"
/>
)}
<div className="flex flex-col gap-6 max-w-[1200px] m-auto">
{workspace.latest_build.status === "deleted" && (
<WorkspaceDeletedBanner
handleClick={() => navigate("/templates")}
/>
)}
{shouldShowProvisionerAlert && (
<ProvisionerStatusAlert
matchingProvisioners={
workspace.latest_build.matched_provisioners?.count
}
availableProvisioners={
workspace.latest_build.matched_provisioners?.available ?? 0
}
tags={workspace.latest_build.job.tags}
/>
)}
{workspace.latest_build.job.error && (
<Alert severity="error">
<AlertTitle>Workspace build failed</AlertTitle>
<AlertDetail>{workspace.latest_build.job.error}</AlertDetail>
</Alert>
)}
{transitionStats !== undefined && (
<WorkspaceBuildProgress
workspace={workspace}
transitionStats={transitionStats}
/>
)}
{shouldShowBuildLogs && (
<WorkspaceBuildLogsSection logs={buildLogs} />
)}
{selectedResource && (
<section className="flex flex-col gap-6 flex-grow min-w-0">
{selectedResource.agents
// If an agent has a `parent_id`, that means it is
// child of another agent. We do not want these agents
// to be displayed at the top-level on this page. We
// want them to display _as children_ of their parents.
?.filter((agent) => agent.parent_id === null)
.map((agent) => (
<AgentRow
key={agent.id}
agent={agent}
subAgents={selectedResource.agents?.filter(
(a) => a.parent_id === agent.id,
)}
workspace={workspace}
template={template}
onUpdateAgent={handleUpdate} // On updating the workspace the agent version is also updated
/>
))}
{(!selectedResource.agents ||
selectedResource.agents?.length === 0) && (
<div className="flex justify-center items-center w-full h-full">
<div>
<h4 className="text-base font-medium">
No agents are currently assigned to this resource.
</h4>
</div>
</div>
)}
</section>
)}
<WorkspaceTimings
provisionerTimings={timings?.provisioner_timings}
agentScriptTimings={timings?.agent_script_timings}
agentConnectionTimings={timings?.agent_connection_timings}
/>
</div>
</div>
</div>
</div>
@@ -286,33 +252,3 @@ export const Workspace: FC<WorkspaceProps> = ({
const countAgents = (resource: TypesGen.WorkspaceResource) => {
return resource.agents ? resource.agents.length : 0;
};
const styles = {
content: {
padding: 32,
gridArea: "content",
overflowY: "auto",
position: "relative",
},
dotsBackground: (theme) => ({
"--d": "1px",
background: `
radial-gradient(
circle at
var(--d)
var(--d),
${theme.palette.dots} calc(var(--d) - 1px),
${theme.palette.background.default} var(--d)
)
-2px -2px / 16px 16px
`,
}),
actions: (theme) => ({
[theme.breakpoints.down("md")]: {
flexDirection: "column",
},
}),
} satisfies Record<string, Interpolation<Theme>>;