Skip to content

Commit 6145086

Browse files
feat(site): move resources into the sidebar (#11456)
1 parent 359a642 commit 6145086

File tree

10 files changed

+215
-67
lines changed

10 files changed

+215
-67
lines changed

site/src/components/FullPageLayout/Sidebar.tsx

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,26 @@ export const SidebarLink = (props: LinkProps) => {
2727
return <Link css={styles.sidebarItem} {...props} />;
2828
};
2929

30-
export const SidebarItem = (props: HTMLAttributes<HTMLButtonElement>) => {
31-
return <button css={styles.sidebarItem} {...props} />;
30+
export const SidebarItem = (
31+
props: HTMLAttributes<HTMLButtonElement> & { isActive?: boolean },
32+
) => {
33+
const { isActive, ...buttonProps } = props;
34+
const theme = useTheme();
35+
36+
return (
37+
<button
38+
css={[
39+
styles.sidebarItem,
40+
{ opacity: "0.75", "&:hover": { opacity: 1 } },
41+
isActive && {
42+
background: theme.palette.action.selected,
43+
opacity: 1,
44+
pointerEvents: "none",
45+
},
46+
]}
47+
{...buttonProps}
48+
/>
49+
);
3250
};
3351

3452
export const SidebarCaption = (props: HTMLAttributes<HTMLSpanElement>) => {
@@ -88,6 +106,7 @@ const styles = {
88106
textAlign: "left",
89107
background: "none",
90108
border: 0,
109+
cursor: "pointer",
91110

92111
"&:hover": {
93112
backgroundColor: theme.palette.action.hover,

site/src/components/Resources/ResourceAvatar.tsx

Lines changed: 2 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,12 @@
11
import { type FC } from "react";
22
import type { WorkspaceResource } from "api/typesGenerated";
33
import { Avatar, AvatarIcon } from "components/Avatar/Avatar";
4-
5-
const FALLBACK_ICON = "/icon/widgets.svg";
6-
7-
// These resources (i.e. docker_image, kubernetes_deployment) map to Terraform
8-
// resource types. These are the most used ones and are based on user usage.
9-
// We may want to update from time-to-time.
10-
const BUILT_IN_ICON_PATHS: Record<string, string> = {
11-
docker_volume: "/icon/database.svg",
12-
docker_container: "/icon/memory.svg",
13-
docker_image: "/icon/container.svg",
14-
kubernetes_persistent_volume_claim: "/icon/database.svg",
15-
kubernetes_pod: "/icon/memory.svg",
16-
google_compute_disk: "/icon/database.svg",
17-
google_compute_instance: "/icon/memory.svg",
18-
aws_instance: "/icon/memory.svg",
19-
kubernetes_deployment: "/icon/memory.svg",
20-
};
21-
22-
export const getIconPathResource = (resourceType: string): string => {
23-
return BUILT_IN_ICON_PATHS[resourceType] ?? FALLBACK_ICON;
24-
};
4+
import { getResourceIconPath } from "utils/workspace";
255

266
export type ResourceAvatarProps = { resource: WorkspaceResource };
277

288
export const ResourceAvatar: FC<ResourceAvatarProps> = ({ resource }) => {
29-
const avatarSrc = resource.icon || getIconPathResource(resource.type);
9+
const avatarSrc = resource.icon || getResourceIconPath(resource.type);
3010

3111
return (
3212
<Avatar background>

site/src/components/Resources/ResourceCard.tsx

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,7 @@ const styles = {
1515
resourceCard: (theme) => ({
1616
borderRadius: 8,
1717
border: `1px solid ${theme.palette.divider}`,
18-
19-
"&:not(:first-of-type)": {
20-
borderTop: 0,
21-
borderTopLeftRadius: 0,
22-
borderTopRightRadius: 0,
23-
},
24-
25-
"&:not(:last-child)": {
26-
borderBottomLeftRadius: 0,
27-
borderBottomRightRadius: 0,
28-
},
18+
background: theme.palette.background.default,
2919
}),
3020

3121
resourceCardProfile: {

site/src/components/WorkspaceBuild/WorkspaceBuildData.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,6 @@ export const WorkspaceBuildData = ({ build }: { build: WorkspaceBuild }) => {
4141
css={{
4242
fontSize: 12,
4343
color: theme.palette.text.secondary,
44-
marginTop: 2,
4544
}}
4645
>
4746
{createDayString(build.created_at)}
@@ -74,6 +73,6 @@ const styles = {
7473
flexDirection: "row",
7574
alignItems: "center",
7675
gap: 12,
77-
lineHeight: "1.4",
76+
lineHeight: "1.5",
7877
},
7978
} satisfies Record<string, Interpolation<Theme>>;
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import { Interpolation, Theme } from "@emotion/react";
2+
import Skeleton from "@mui/material/Skeleton";
3+
import { useTheme } from "@mui/material/styles";
4+
import { WorkspaceResource } from "api/typesGenerated";
5+
import {
6+
Sidebar,
7+
SidebarCaption,
8+
SidebarItem,
9+
} from "components/FullPageLayout/Sidebar";
10+
import { getResourceIconPath } from "utils/workspace";
11+
12+
type ResourcesSidebarProps = {
13+
failed: boolean;
14+
resources: WorkspaceResource[];
15+
onChange: (resourceId: string) => void;
16+
selected: string;
17+
};
18+
19+
export const ResourcesSidebar = (props: ResourcesSidebarProps) => {
20+
const theme = useTheme();
21+
const { failed, onChange, selected, resources } = props;
22+
23+
return (
24+
<Sidebar>
25+
<SidebarCaption>Resources</SidebarCaption>
26+
{failed && (
27+
<p
28+
css={{
29+
margin: 0,
30+
padding: "0 16px",
31+
fontSize: 13,
32+
color: theme.palette.text.secondary,
33+
lineHeight: "1.5",
34+
}}
35+
>
36+
Your workspace build failed, so the necessary resources couldn&apos;t
37+
be created.
38+
</p>
39+
)}
40+
{resources.length === 0 &&
41+
!failed &&
42+
Array.from({ length: 8 }, (_, i) => (
43+
<SidebarItem key={i}>
44+
<ResourceSidebarItemSkeleton />
45+
</SidebarItem>
46+
))}
47+
{resources.map((r) => (
48+
<SidebarItem
49+
onClick={() => onChange(r.id)}
50+
isActive={r.id === selected}
51+
key={r.id}
52+
css={styles.root}
53+
>
54+
<div
55+
css={{
56+
display: "flex",
57+
alignItems: "center",
58+
justifyContent: "center",
59+
lineHeight: 0,
60+
width: 16,
61+
height: 16,
62+
padding: 2,
63+
}}
64+
>
65+
<img
66+
css={{ width: "100%", height: "100%", objectFit: "contain" }}
67+
src={getResourceIconPath(r.type)}
68+
alt=""
69+
role="presentation"
70+
/>
71+
</div>
72+
<div
73+
css={{ display: "flex", flexDirection: "column", fontWeight: 500 }}
74+
>
75+
<span>{r.name}</span>
76+
<span css={{ fontSize: 12, color: theme.palette.text.secondary }}>
77+
{r.type}
78+
</span>
79+
</div>
80+
</SidebarItem>
81+
))}
82+
</Sidebar>
83+
);
84+
};
85+
86+
export const ResourceSidebarItemSkeleton = () => {
87+
return (
88+
<div css={[styles.root, { pointerEvents: "none" }]}>
89+
<Skeleton variant="circular" width={16} height={16} />
90+
<div>
91+
<Skeleton variant="text" width={94} height={16} />
92+
<Skeleton
93+
variant="text"
94+
width={60}
95+
height={14}
96+
css={{ marginTop: 2 }}
97+
/>
98+
</div>
99+
</div>
100+
);
101+
};
102+
103+
const styles = {
104+
root: {
105+
lineHeight: "1.5",
106+
display: "flex",
107+
alignItems: "center",
108+
gap: 12,
109+
},
110+
} satisfies Record<string, Interpolation<Theme>>;

site/src/pages/WorkspacePage/Workspace.stories.tsx

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -68,12 +68,6 @@ export const Running: Story = {
6868
workspace: Mocks.MockWorkspace,
6969
handleStart: action("start"),
7070
handleStop: action("stop"),
71-
resources: [
72-
Mocks.MockWorkspaceResourceMultipleAgents,
73-
Mocks.MockWorkspaceVolumeResource,
74-
Mocks.MockWorkspaceImageResource,
75-
Mocks.MockWorkspaceContainerResource,
76-
],
7771
canUpdateWorkspace: true,
7872
workspaceErrors: {},
7973
buildInfo: Mocks.MockBuildInfo,

site/src/pages/WorkspacePage/Workspace.tsx

Lines changed: 57 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,15 @@ import { type Interpolation, type Theme } from "@emotion/react";
22
import Button from "@mui/material/Button";
33
import AlertTitle from "@mui/material/AlertTitle";
44
import { type FC, useEffect, useState } from "react";
5-
import { useNavigate, useSearchParams } from "react-router-dom";
5+
import { useNavigate } from "react-router-dom";
66
import dayjs from "dayjs";
77
import type * as TypesGen from "api/typesGenerated";
88
import { Alert, AlertDetail } from "components/Alert/Alert";
9-
import { Resources } from "components/Resources/Resources";
109
import { Stack } from "components/Stack/Stack";
1110
import { ErrorAlert } from "components/Alert/ErrorAlert";
1211
import { DormantWorkspaceBanner } from "components/WorkspaceDeletion";
1312
import { AgentRow } from "components/Resources/AgentRow";
14-
import { useLocalStorage } from "hooks";
13+
import { useLocalStorage, useTab } from "hooks";
1514
import {
1615
ActiveTransition,
1716
WorkspaceBuildProgress,
@@ -24,6 +23,9 @@ import { bannerHeight } from "components/Dashboard/DeploymentBanner/DeploymentBa
2423
import HistoryOutlined from "@mui/icons-material/HistoryOutlined";
2524
import { useTheme } from "@mui/material/styles";
2625
import { SidebarIconButton } from "components/FullPageLayout/Sidebar";
26+
import HubOutlined from "@mui/icons-material/HubOutlined";
27+
import { ResourcesSidebar } from "./ResourcesSidebar";
28+
import { ResourceCard } from "components/Resources/ResourceCard";
2729

2830
export type WorkspaceError =
2931
| "getBuildsError"
@@ -45,7 +47,6 @@ export interface WorkspaceProps {
4547
isUpdating: boolean;
4648
isRestarting: boolean;
4749
workspace: TypesGen.Workspace;
48-
resources?: TypesGen.WorkspaceResource[];
4950
canUpdateWorkspace: boolean;
5051
updateMessage?: string;
5152
canChangeVersions: boolean;
@@ -78,8 +79,6 @@ export const Workspace: FC<React.PropsWithChildren<WorkspaceProps>> = ({
7879
workspace,
7980
isUpdating,
8081
isRestarting,
81-
resources,
82-
8382
canUpdateWorkspace,
8483
updateMessage,
8584
canChangeVersions,
@@ -99,8 +98,6 @@ export const Workspace: FC<React.PropsWithChildren<WorkspaceProps>> = ({
9998
const { saveLocal, getLocal } = useLocalStorage();
10099
const theme = useTheme();
101100

102-
const [searchParams, setSearchParams] = useSearchParams();
103-
104101
const [showAlertPendingInQueue, setShowAlertPendingInQueue] = useState(false);
105102

106103
// 2023-11-15 - MES - This effect will be called every single render because
@@ -148,6 +145,29 @@ export const Workspace: FC<React.PropsWithChildren<WorkspaceProps>> = ({
148145
const transitionStats =
149146
template !== undefined ? ActiveTransition(template, workspace) : undefined;
150147

148+
const sidebarOption = useTab("sidebar", "");
149+
const setSidebarOption = (newOption: string) => {
150+
const { set, value } = sidebarOption;
151+
if (value === newOption) {
152+
set("");
153+
} else {
154+
set(newOption);
155+
}
156+
};
157+
158+
const selectedResourceId = useTab("resources", "");
159+
const resources = [...workspace.latest_build.resources].sort(
160+
(a, b) => countAgents(b) - countAgents(a),
161+
);
162+
const selectedResource = workspace.latest_build.resources.find(
163+
(r) => r.id === selectedResourceId.value,
164+
);
165+
useEffect(() => {
166+
if (resources.length > 0 && selectedResourceId.value === "") {
167+
selectedResourceId.set(resources[0].id);
168+
}
169+
}, [resources, selectedResourceId]);
170+
151171
return (
152172
<div
153173
css={{
@@ -187,25 +207,37 @@ export const Workspace: FC<React.PropsWithChildren<WorkspaceProps>> = ({
187207
height: "100%",
188208
overflowY: "auto",
189209
borderRight: `1px solid ${theme.palette.divider}`,
210+
display: "flex",
211+
flexDirection: "column",
190212
}}
191213
>
192214
<SidebarIconButton
193-
isActive={searchParams.get("sidebar") === "history"}
215+
isActive={sidebarOption.value === "resources"}
194216
onClick={() => {
195-
const sidebarOption = searchParams.get("sidebar");
196-
if (sidebarOption === "history") {
197-
searchParams.delete("sidebar");
198-
} else {
199-
searchParams.set("sidebar", "history");
200-
}
201-
setSearchParams(searchParams);
217+
setSidebarOption("resources");
218+
}}
219+
>
220+
<HubOutlined />
221+
</SidebarIconButton>
222+
<SidebarIconButton
223+
isActive={sidebarOption.value === "history"}
224+
onClick={() => {
225+
setSidebarOption("history");
202226
}}
203227
>
204228
<HistoryOutlined />
205229
</SidebarIconButton>
206230
</div>
207231

208-
{searchParams.get("sidebar") === "history" && (
232+
{sidebarOption.value === "resources" && (
233+
<ResourcesSidebar
234+
failed={workspace.latest_build.status === "failed"}
235+
resources={resources}
236+
selected={selectedResourceId.value}
237+
onChange={selectedResourceId.set}
238+
/>
239+
)}
240+
{sidebarOption.value === "history" && (
209241
<HistorySidebar workspace={workspace} />
210242
)}
211243

@@ -342,9 +374,9 @@ export const Workspace: FC<React.PropsWithChildren<WorkspaceProps>> = ({
342374

343375
{buildLogs}
344376

345-
{resources && resources.length > 0 && (
346-
<Resources
347-
resources={resources}
377+
{selectedResource && (
378+
<ResourceCard
379+
resource={selectedResource}
348380
agentRow={(agent) => (
349381
<AgentRow
350382
key={agent.id}
@@ -369,6 +401,10 @@ export const Workspace: FC<React.PropsWithChildren<WorkspaceProps>> = ({
369401
);
370402
};
371403

404+
const countAgents = (resource: TypesGen.WorkspaceResource) => {
405+
return resource.agents ? resource.agents.length : 0;
406+
};
407+
372408
const styles = {
373409
content: {
374410
padding: 24,
@@ -377,6 +413,7 @@ const styles = {
377413
},
378414

379415
dotBackground: (theme) => ({
416+
minHeight: "100%",
380417
padding: 24,
381418
"--d": "1px",
382419
background: `

0 commit comments

Comments
 (0)