feat(site): move resources into the sidebar (#11456)

This commit is contained in:
Bruno Quaresma
2024-01-08 13:14:25 -03:00
committed by GitHub
parent 359a642e7e
commit 61450863ff
10 changed files with 215 additions and 67 deletions
+21 -2
View File
@@ -27,8 +27,26 @@ export const SidebarLink = (props: LinkProps) => {
return <Link css={styles.sidebarItem} {...props} />;
};
export const SidebarItem = (props: HTMLAttributes<HTMLButtonElement>) => {
return <button css={styles.sidebarItem} {...props} />;
export const SidebarItem = (
props: HTMLAttributes<HTMLButtonElement> & { isActive?: boolean },
) => {
const { isActive, ...buttonProps } = props;
const theme = useTheme();
return (
<button
css={[
styles.sidebarItem,
{ opacity: "0.75", "&:hover": { opacity: 1 } },
isActive && {
background: theme.palette.action.selected,
opacity: 1,
pointerEvents: "none",
},
]}
{...buttonProps}
/>
);
};
export const SidebarCaption = (props: HTMLAttributes<HTMLSpanElement>) => {
@@ -88,6 +106,7 @@ const styles = {
textAlign: "left",
background: "none",
border: 0,
cursor: "pointer",
"&:hover": {
backgroundColor: theme.palette.action.hover,
@@ -1,32 +1,12 @@
import { type FC } from "react";
import type { WorkspaceResource } from "api/typesGenerated";
import { Avatar, AvatarIcon } from "components/Avatar/Avatar";
const FALLBACK_ICON = "/icon/widgets.svg";
// These resources (i.e. docker_image, kubernetes_deployment) map to Terraform
// resource types. These are the most used ones and are based on user usage.
// We may want to update from time-to-time.
const BUILT_IN_ICON_PATHS: Record<string, string> = {
docker_volume: "/icon/database.svg",
docker_container: "/icon/memory.svg",
docker_image: "/icon/container.svg",
kubernetes_persistent_volume_claim: "/icon/database.svg",
kubernetes_pod: "/icon/memory.svg",
google_compute_disk: "/icon/database.svg",
google_compute_instance: "/icon/memory.svg",
aws_instance: "/icon/memory.svg",
kubernetes_deployment: "/icon/memory.svg",
};
export const getIconPathResource = (resourceType: string): string => {
return BUILT_IN_ICON_PATHS[resourceType] ?? FALLBACK_ICON;
};
import { getResourceIconPath } from "utils/workspace";
export type ResourceAvatarProps = { resource: WorkspaceResource };
export const ResourceAvatar: FC<ResourceAvatarProps> = ({ resource }) => {
const avatarSrc = resource.icon || getIconPathResource(resource.type);
const avatarSrc = resource.icon || getResourceIconPath(resource.type);
return (
<Avatar background>
+1 -11
View File
@@ -15,17 +15,7 @@ const styles = {
resourceCard: (theme) => ({
borderRadius: 8,
border: `1px solid ${theme.palette.divider}`,
"&:not(:first-of-type)": {
borderTop: 0,
borderTopLeftRadius: 0,
borderTopRightRadius: 0,
},
"&:not(:last-child)": {
borderBottomLeftRadius: 0,
borderBottomRightRadius: 0,
},
background: theme.palette.background.default,
}),
resourceCardProfile: {
@@ -41,7 +41,6 @@ export const WorkspaceBuildData = ({ build }: { build: WorkspaceBuild }) => {
css={{
fontSize: 12,
color: theme.palette.text.secondary,
marginTop: 2,
}}
>
{createDayString(build.created_at)}
@@ -74,6 +73,6 @@ const styles = {
flexDirection: "row",
alignItems: "center",
gap: 12,
lineHeight: "1.4",
lineHeight: "1.5",
},
} satisfies Record<string, Interpolation<Theme>>;
@@ -0,0 +1,110 @@
import { Interpolation, Theme } from "@emotion/react";
import Skeleton from "@mui/material/Skeleton";
import { useTheme } from "@mui/material/styles";
import { WorkspaceResource } from "api/typesGenerated";
import {
Sidebar,
SidebarCaption,
SidebarItem,
} from "components/FullPageLayout/Sidebar";
import { getResourceIconPath } from "utils/workspace";
type ResourcesSidebarProps = {
failed: boolean;
resources: WorkspaceResource[];
onChange: (resourceId: string) => void;
selected: string;
};
export const ResourcesSidebar = (props: ResourcesSidebarProps) => {
const theme = useTheme();
const { failed, onChange, selected, resources } = props;
return (
<Sidebar>
<SidebarCaption>Resources</SidebarCaption>
{failed && (
<p
css={{
margin: 0,
padding: "0 16px",
fontSize: 13,
color: theme.palette.text.secondary,
lineHeight: "1.5",
}}
>
Your workspace build failed, so the necessary resources couldn&apos;t
be created.
</p>
)}
{resources.length === 0 &&
!failed &&
Array.from({ length: 8 }, (_, i) => (
<SidebarItem key={i}>
<ResourceSidebarItemSkeleton />
</SidebarItem>
))}
{resources.map((r) => (
<SidebarItem
onClick={() => onChange(r.id)}
isActive={r.id === selected}
key={r.id}
css={styles.root}
>
<div
css={{
display: "flex",
alignItems: "center",
justifyContent: "center",
lineHeight: 0,
width: 16,
height: 16,
padding: 2,
}}
>
<img
css={{ width: "100%", height: "100%", objectFit: "contain" }}
src={getResourceIconPath(r.type)}
alt=""
role="presentation"
/>
</div>
<div
css={{ display: "flex", flexDirection: "column", fontWeight: 500 }}
>
<span>{r.name}</span>
<span css={{ fontSize: 12, color: theme.palette.text.secondary }}>
{r.type}
</span>
</div>
</SidebarItem>
))}
</Sidebar>
);
};
export const ResourceSidebarItemSkeleton = () => {
return (
<div css={[styles.root, { pointerEvents: "none" }]}>
<Skeleton variant="circular" width={16} height={16} />
<div>
<Skeleton variant="text" width={94} height={16} />
<Skeleton
variant="text"
width={60}
height={14}
css={{ marginTop: 2 }}
/>
</div>
</div>
);
};
const styles = {
root: {
lineHeight: "1.5",
display: "flex",
alignItems: "center",
gap: 12,
},
} satisfies Record<string, Interpolation<Theme>>;
@@ -68,12 +68,6 @@ export const Running: Story = {
workspace: Mocks.MockWorkspace,
handleStart: action("start"),
handleStop: action("stop"),
resources: [
Mocks.MockWorkspaceResourceMultipleAgents,
Mocks.MockWorkspaceVolumeResource,
Mocks.MockWorkspaceImageResource,
Mocks.MockWorkspaceContainerResource,
],
canUpdateWorkspace: true,
workspaceErrors: {},
buildInfo: Mocks.MockBuildInfo,
+57 -20
View File
@@ -2,16 +2,15 @@ import { type Interpolation, type Theme } from "@emotion/react";
import Button from "@mui/material/Button";
import AlertTitle from "@mui/material/AlertTitle";
import { type FC, useEffect, useState } from "react";
import { useNavigate, useSearchParams } from "react-router-dom";
import { useNavigate } from "react-router-dom";
import dayjs from "dayjs";
import type * as TypesGen from "api/typesGenerated";
import { Alert, AlertDetail } from "components/Alert/Alert";
import { Resources } from "components/Resources/Resources";
import { Stack } from "components/Stack/Stack";
import { ErrorAlert } from "components/Alert/ErrorAlert";
import { DormantWorkspaceBanner } from "components/WorkspaceDeletion";
import { AgentRow } from "components/Resources/AgentRow";
import { useLocalStorage } from "hooks";
import { useLocalStorage, useTab } from "hooks";
import {
ActiveTransition,
WorkspaceBuildProgress,
@@ -24,6 +23,9 @@ import { bannerHeight } from "components/Dashboard/DeploymentBanner/DeploymentBa
import HistoryOutlined from "@mui/icons-material/HistoryOutlined";
import { useTheme } from "@mui/material/styles";
import { SidebarIconButton } from "components/FullPageLayout/Sidebar";
import HubOutlined from "@mui/icons-material/HubOutlined";
import { ResourcesSidebar } from "./ResourcesSidebar";
import { ResourceCard } from "components/Resources/ResourceCard";
export type WorkspaceError =
| "getBuildsError"
@@ -45,7 +47,6 @@ export interface WorkspaceProps {
isUpdating: boolean;
isRestarting: boolean;
workspace: TypesGen.Workspace;
resources?: TypesGen.WorkspaceResource[];
canUpdateWorkspace: boolean;
updateMessage?: string;
canChangeVersions: boolean;
@@ -78,8 +79,6 @@ export const Workspace: FC<React.PropsWithChildren<WorkspaceProps>> = ({
workspace,
isUpdating,
isRestarting,
resources,
canUpdateWorkspace,
updateMessage,
canChangeVersions,
@@ -99,8 +98,6 @@ export const Workspace: FC<React.PropsWithChildren<WorkspaceProps>> = ({
const { saveLocal, getLocal } = useLocalStorage();
const theme = useTheme();
const [searchParams, setSearchParams] = useSearchParams();
const [showAlertPendingInQueue, setShowAlertPendingInQueue] = useState(false);
// 2023-11-15 - MES - This effect will be called every single render because
@@ -148,6 +145,29 @@ export const Workspace: FC<React.PropsWithChildren<WorkspaceProps>> = ({
const transitionStats =
template !== undefined ? ActiveTransition(template, workspace) : undefined;
const sidebarOption = useTab("sidebar", "");
const setSidebarOption = (newOption: string) => {
const { set, value } = sidebarOption;
if (value === newOption) {
set("");
} else {
set(newOption);
}
};
const selectedResourceId = useTab("resources", "");
const resources = [...workspace.latest_build.resources].sort(
(a, b) => countAgents(b) - countAgents(a),
);
const selectedResource = workspace.latest_build.resources.find(
(r) => r.id === selectedResourceId.value,
);
useEffect(() => {
if (resources.length > 0 && selectedResourceId.value === "") {
selectedResourceId.set(resources[0].id);
}
}, [resources, selectedResourceId]);
return (
<div
css={{
@@ -187,25 +207,37 @@ export const Workspace: FC<React.PropsWithChildren<WorkspaceProps>> = ({
height: "100%",
overflowY: "auto",
borderRight: `1px solid ${theme.palette.divider}`,
display: "flex",
flexDirection: "column",
}}
>
<SidebarIconButton
isActive={searchParams.get("sidebar") === "history"}
isActive={sidebarOption.value === "resources"}
onClick={() => {
const sidebarOption = searchParams.get("sidebar");
if (sidebarOption === "history") {
searchParams.delete("sidebar");
} else {
searchParams.set("sidebar", "history");
}
setSearchParams(searchParams);
setSidebarOption("resources");
}}
>
<HubOutlined />
</SidebarIconButton>
<SidebarIconButton
isActive={sidebarOption.value === "history"}
onClick={() => {
setSidebarOption("history");
}}
>
<HistoryOutlined />
</SidebarIconButton>
</div>
{searchParams.get("sidebar") === "history" && (
{sidebarOption.value === "resources" && (
<ResourcesSidebar
failed={workspace.latest_build.status === "failed"}
resources={resources}
selected={selectedResourceId.value}
onChange={selectedResourceId.set}
/>
)}
{sidebarOption.value === "history" && (
<HistorySidebar workspace={workspace} />
)}
@@ -342,9 +374,9 @@ export const Workspace: FC<React.PropsWithChildren<WorkspaceProps>> = ({
{buildLogs}
{resources && resources.length > 0 && (
<Resources
resources={resources}
{selectedResource && (
<ResourceCard
resource={selectedResource}
agentRow={(agent) => (
<AgentRow
key={agent.id}
@@ -369,6 +401,10 @@ export const Workspace: FC<React.PropsWithChildren<WorkspaceProps>> = ({
);
};
const countAgents = (resource: TypesGen.WorkspaceResource) => {
return resource.agents ? resource.agents.length : 0;
};
const styles = {
content: {
padding: 24,
@@ -377,6 +413,7 @@ const styles = {
},
dotBackground: (theme) => ({
minHeight: "100%",
padding: 24,
"--d": "1px",
background: `
@@ -224,7 +224,6 @@ export const WorkspaceReadyPage = ({
displayError(message);
}
}}
resources={workspace.latest_build.resources}
canUpdateWorkspace={canUpdateWorkspace}
updateMessage={latestVersion?.message}
canChangeVersions={canChangeVersions}
@@ -10,7 +10,7 @@ import { deleteWorkspace, startWorkspace, stopWorkspace } from "api/api";
import type { Workspace } from "api/typesGenerated";
import { ConfirmDialog } from "components/Dialogs/ConfirmDialog/ConfirmDialog";
import { displayError } from "components/GlobalSnackbar/utils";
import { getIconPathResource } from "components/Resources/ResourceAvatar";
import { getResourceIconPath } from "utils/workspace";
import { Stack } from "components/Stack/Stack";
interface UseBatchActionsProps {
@@ -126,7 +126,7 @@ export const BatchDeleteConfirmation: FC<BatchDeleteConfirmationProps> = ({
...new Set(
checkedWorkspaces.flatMap((workspace) =>
workspace.latest_build.resources.map(
(resource) => resource.icon || getIconPathResource(resource.type),
(resource) => resource.icon || getResourceIconPath(resource.type),
),
),
),
@@ -257,7 +257,7 @@ const Resources: FC<StageProps> = ({ workspaces }) => {
if (!resources[resource.type]) {
resources[resource.type] = {
count: 0,
icon: resource.icon || getIconPathResource(resource.type),
icon: resource.icon || getResourceIconPath(resource.type),
};
}
+20
View File
@@ -285,3 +285,23 @@ export const workspaceUpdatePolicy = (
}
return workspace.automatic_updates;
};
// These resources (i.e. docker_image, kubernetes_deployment) map to Terraform
// resource types. These are the most used ones and are based on user usage.
// We may want to update from time-to-time.
const BUILT_IN_ICON_PATHS: Record<string, `/icon/${string}`> = {
docker_volume: "/icon/database.svg",
docker_container: "/icon/memory.svg",
docker_image: "/icon/container.svg",
kubernetes_persistent_volume_claim: "/icon/database.svg",
kubernetes_pod: "/icon/memory.svg",
google_compute_disk: "/icon/database.svg",
google_compute_instance: "/icon/memory.svg",
aws_instance: "/icon/memory.svg",
kubernetes_deployment: "/icon/memory.svg",
};
const FALLBACK_ICON = "/icon/widgets.svg";
export const getResourceIconPath = (resourceType: string): string => {
return BUILT_IN_ICON_PATHS[resourceType] ?? FALLBACK_ICON;
};