mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
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:
@@ -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" />
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>>;
|
||||
|
||||
Reference in New Issue
Block a user