From b4daf36d0b3fe4eb43c99b778d3d203b4fca0876 Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Thu, 22 May 2025 21:03:13 -0300 Subject: [PATCH] refactor: refactor activity column in the workspaces table (#17976) The goal is to better integrate the activity column data with the existent data: - Make the message one line, the full message is in the tooltip, and display the state at the bottom. This way, it is visually consistent with the other columns like status, name and template. - Moved the app, and uri, to the actions column, instead of showing them together with the message in the activity column. **Previous:** Screenshot 2025-05-21 at 17 28 46 **After:** Screenshot 2025-05-21 at 17 28 57 --- .../WorkspaceAppStatus.stories.tsx | 32 -- .../WorkspaceAppStatus/WorkspaceAppStatus.tsx | 317 ++---------------- .../pages/WorkspacesPage/WorkspacesTable.tsx | 97 ++++-- 3 files changed, 106 insertions(+), 340 deletions(-) diff --git a/site/src/modules/workspaces/WorkspaceAppStatus/WorkspaceAppStatus.stories.tsx b/site/src/modules/workspaces/WorkspaceAppStatus/WorkspaceAppStatus.stories.tsx index 74ec70a863..ac64584847 100644 --- a/site/src/modules/workspaces/WorkspaceAppStatus/WorkspaceAppStatus.stories.tsx +++ b/site/src/modules/workspaces/WorkspaceAppStatus/WorkspaceAppStatus.stories.tsx @@ -2,9 +2,6 @@ import type { Meta, StoryObj } from "@storybook/react"; import { ProxyContext, getPreferredProxy } from "contexts/ProxyContext"; import { MockProxyLatencies, - MockWorkspace, - MockWorkspaceAgent, - MockWorkspaceApp, MockWorkspaceAppStatus, } from "testHelpers/entities"; import { WorkspaceAppStatus } from "./WorkspaceAppStatus"; @@ -68,24 +65,6 @@ export const Working: Story = { }, }; -export const LongURI: Story = { - args: { - status: { - ...MockWorkspaceAppStatus, - uri: "https://www.google.com/search?q=hello+world+plus+a+lot+of+other+words", - }, - }, -}; - -export const FileURI: Story = { - args: { - status: { - ...MockWorkspaceAppStatus, - uri: "file:///Users/jason/Desktop/test.txt", - }, - }, -}; - export const LongMessage: Story = { args: { status: { @@ -95,14 +74,3 @@ export const LongMessage: Story = { }, }, }; - -export const WithApp: Story = { - args: { - status: MockWorkspaceAppStatus, - app: { - ...MockWorkspaceApp, - }, - agent: MockWorkspaceAgent, - workspace: MockWorkspace, - }, -}; diff --git a/site/src/modules/workspaces/WorkspaceAppStatus/WorkspaceAppStatus.tsx b/site/src/modules/workspaces/WorkspaceAppStatus/WorkspaceAppStatus.tsx index 3a130f2baa..76e74f17c3 100644 --- a/site/src/modules/workspaces/WorkspaceAppStatus/WorkspaceAppStatus.tsx +++ b/site/src/modules/workspaces/WorkspaceAppStatus/WorkspaceAppStatus.tsx @@ -1,305 +1,54 @@ -import type { Theme } from "@emotion/react"; -import { useTheme } from "@emotion/react"; -import CircularProgress from "@mui/material/CircularProgress"; import type { WorkspaceAppStatus as APIWorkspaceAppStatus, - Workspace, - WorkspaceAgent, - WorkspaceApp, + WorkspaceAppStatusState, } from "api/typesGenerated"; +import { Spinner } from "components/Spinner/Spinner"; import { - CircleAlertIcon, - CircleCheckIcon, - ExternalLinkIcon, - FileIcon, - LayoutGridIcon, - TriangleAlertIcon, -} from "lucide-react"; -import { useAppLink } from "modules/apps/useAppLink"; -import type { FC } from "react"; + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "components/Tooltip/Tooltip"; +import { CircleAlertIcon, CircleCheckIcon } from "lucide-react"; +import type { ReactNode } from "react"; -const formatURI = (uri: string) => { - try { - const url = new URL(uri); - return url.hostname + url.pathname; - } catch { - return uri; - } -}; - -const getStatusColor = ( - theme: Theme, - state: APIWorkspaceAppStatus["state"], -) => { - switch (state) { - case "complete": - return theme.palette.success.main; - case "failure": - return theme.palette.error.main; - case "working": - return theme.palette.primary.main; - default: - // Assuming unknown state maps to warning/secondary visually - return theme.palette.text.secondary; - } -}; - -const getStatusIcon = (theme: Theme, state: APIWorkspaceAppStatus["state"]) => { - const color = getStatusColor(theme, state); - switch (state) { - case "complete": - return ; - case "failure": - return ; - case "working": - return ; - default: - return ; - } +const iconByState: Record = { + complete: ( + + ), + failure: , + working: , }; export const WorkspaceAppStatus = ({ - workspace, status, - agent, - app, }: { - workspace: Workspace; - status?: APIWorkspaceAppStatus | null; - app?: WorkspaceApp; - agent?: WorkspaceAgent; + status: APIWorkspaceAppStatus | null; }) => { - const theme = useTheme(); - const commonStyles = useCommonStyles(); - if (!status) { return ( -
-
- ― -
-
+ + -No activity + ); } - const isFileURI = status.uri?.startsWith("file://"); return ( -
-
- {getStatusIcon(theme, status.state)} -
-
-
- {status.message} -
-
- {app && agent && ( - - )} - {status.uri && ( -
- {isFileURI ? ( -
- - {formatURI(status.uri)} -
- ) : ( - - - - {formatURI(status.uri)} - - - )} +
+ + + +
+ {iconByState[status.state]} + + {status.message} +
- )} -
-
+ + {status.message} + + + {status.state}
); }; - -type AppLinkProps = { - app: WorkspaceApp; - workspace: Workspace; - agent: WorkspaceAgent; -}; - -const AppLink: FC = ({ app, workspace, agent }) => { - const theme = useTheme(); - const commonStyles = useCommonStyles(); - const link = useAppLink(app, { agent, workspace }); - - return ( - - {app.icon ? ( - {`${app.display_name} - ) : ( - - )} - {app.display_name} - - ); -}; - -const useCommonStyles = () => { - const theme = useTheme(); - - return { - fontSize: "12px", - lineHeight: "15px", - color: theme.palette.text.disabled, - display: "inline-flex", - alignItems: "center", - gap: 4, - padding: "2px 6px", - borderRadius: "6px", - bgcolor: "transparent", - minWidth: 0, - maxWidth: "fit-content", - overflow: "hidden", - textOverflow: "ellipsis", - whiteSpace: "nowrap", - textDecoration: "none", - transition: "all 0.15s ease-in-out", - "&:hover": { - textDecoration: "none", - backgroundColor: theme.palette.action.hover, - color: theme.palette.text.secondary, - }, - }; -}; diff --git a/site/src/pages/WorkspacesPage/WorkspacesTable.tsx b/site/src/pages/WorkspacesPage/WorkspacesTable.tsx index cfc96c4bd3..f812f2d05a 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesTable.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesTable.tsx @@ -43,7 +43,7 @@ import { } from "components/Tooltip/Tooltip"; import { useAuthenticated } from "hooks"; import { useClickableTableRow } from "hooks/useClickableTableRow"; -import { StarIcon } from "lucide-react"; +import { ExternalLinkIcon, FileIcon, StarIcon } from "lucide-react"; import { EllipsisVertical } from "lucide-react"; import { BanIcon, @@ -138,16 +138,22 @@ export const WorkspacesTable: FC = ({ ) || {} ); }, [workspaces]); - const hasAppStatus = useMemo( + const hasActivity = useMemo( () => Object.keys(workspaceIDToAppByStatus).length > 0, [workspaceIDToAppByStatus], ); + const tableColumnSize = { + name: "w-2/6", + template: hasActivity ? "w-1/6" : "w-2/6", + status: hasActivity ? "w-1/6" : "w-2/6", + activity: "w-2/6", + }; return ( - +
{canCheckWorkspaces && ( = ({ Name
- {hasAppStatus && Activity} - Template - Status - + Template + Status + {hasActivity && ( + Activity + )} + + Actions +
@@ -229,7 +239,9 @@ export const WorkspacesTable: FC = ({ - {workspace.name} + + {workspace.name} + {workspace.favorite && ( )} @@ -255,20 +267,13 @@ export const WorkspacesTable: FC = ({ - {hasAppStatus && ( - - - - )} - + {getDisplayWorkspaceTemplateName(workspace)} + + } subtitle={ dashboard.showOrganizations && ( <> @@ -290,6 +295,12 @@ export const WorkspacesTable: FC = ({ + {hasActivity && ( + + + + )} + = ({ workspace }) => { )} - + {lastUsedMessage(workspace.last_used_at)} @@ -504,9 +515,12 @@ const WorkspaceActionsCell: FC = ({ }} >
- {workspace.latest_build.status === "running" && ( - - )} + {workspace.latest_build.status === "running" && + (workspace.latest_app_status ? ( + + ) : ( + + ))} {abilities.actions.includes("start") && ( = ({ workspace }) => { return buttons; }; +type WorkspaceAppStatusLinksProps = { + workspace: Workspace; +}; + +const WorkspaceAppStatusLinks: FC = ({ + workspace, +}) => { + const status = workspace.latest_app_status; + const agent = workspace.latest_build.resources + .flatMap((r) => r.agents) + .find((a) => a?.id === status?.agent_id); + const app = agent?.apps.find((a) => a.id === status?.app_id); + + return ( + <> + {agent && app && ( + + )} + + {status?.uri && status?.uri !== "n/a" && ( + + {status.uri.startsWith("file://") ? ( + + ) : ( + + )} + + )} + + ); +}; + type IconAppLinkProps = { app: WorkspaceApp; workspace: Workspace; @@ -730,6 +776,7 @@ type BaseIconLinkProps = PropsWithChildren<{ href: string; isLoading?: boolean; onClick?: (e: React.MouseEvent) => void; + target?: string; }>; const BaseIconLink: FC = ({ @@ -737,6 +784,7 @@ const BaseIconLink: FC = ({ isLoading, label, children, + target, onClick, }) => { return ( @@ -745,6 +793,7 @@ const BaseIconLink: FC = ({