From 78986efed8a72341dac57feaa6b4382e69fb6aef Mon Sep 17 00:00:00 2001 From: TJ Date: Sun, 29 Mar 2026 23:28:30 -0700 Subject: [PATCH] fix(site): hide table headers during loading and empty states on workspaces page (#23446) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Problem When the workspaces table transitions between states (loading → populated, or populated → empty search results), the table column headers visibly jump. This happens because the Actions column's width is content-driven: when workspace rows are present, the action buttons give it intrinsic width, shrinking the Name/Template/Status columns. When the body is empty or loading, the Actions column collapses to zero, and the other columns expand to fill the space. ## Solution Hide the header content during loading and empty states using `visibility: hidden` (Tailwind's `invisible` class), which preserves the row's layout height but hides the text. This prevents the visual jump since headers aren't visible during the states where column widths differ. - **Loading**: first column shows a skeleton bar matching the body skeleton aesthetic; other columns are invisible - **Empty search results**: all header content is invisible - **Populated**: headers display normally --------- Co-authored-by: Jaayden Halko --- .../WorkspacesPageView.stories.tsx | 7 ++ .../pages/WorkspacesPage/WorkspacesTable.tsx | 75 +++++++++++-------- 2 files changed, 49 insertions(+), 33 deletions(-) diff --git a/site/src/pages/WorkspacesPage/WorkspacesPageView.stories.tsx b/site/src/pages/WorkspacesPage/WorkspacesPageView.stories.tsx index 0091cd709f..8dbda6b01a 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesPageView.stories.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesPageView.stories.tsx @@ -195,6 +195,13 @@ export const AllStates: Story = { }, }; +export const Loading: Story = { + args: { + workspaces: undefined, + count: undefined, + }, +}; + export const AllStatesWithFavorites: Story = { args: { workspaces: allWorkspaces.map((workspace, i) => ({ diff --git a/site/src/pages/WorkspacesPage/WorkspacesTable.tsx b/site/src/pages/WorkspacesPage/WorkspacesTable.tsx index f60c64caa5..0ecc517f12 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesTable.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesTable.tsx @@ -1,4 +1,3 @@ -import Skeleton from "@mui/material/Skeleton"; import { BanIcon, CloudIcon, @@ -43,8 +42,8 @@ import { ConfirmDialog } from "#/components/Dialogs/ConfirmDialog/ConfirmDialog" import { ExternalImage } from "#/components/ExternalImage/ExternalImage"; import { VSCodeIcon } from "#/components/Icons/VSCodeIcon"; import { VSCodeInsidersIcon } from "#/components/Icons/VSCodeInsidersIcon"; +import { Skeleton } from "#/components/Skeleton/Skeleton"; import { Spinner } from "#/components/Spinner/Spinner"; -import { Stack } from "#/components/Stack/Stack"; import { Table, TableBody, @@ -111,47 +110,57 @@ export const WorkspacesTable: FC = ({ chatsByWorkspace, }) => { const dashboard = useDashboard(); + const isLoading = !workspaces; + const isEmpty = workspaces && workspaces.length === 0; + const hideHeaders = isLoading || isEmpty; return ( -
- 0 && - checkedWorkspaces.length === workspaces.length - } - onCheckedChange={(checked) => { - if (!workspaces) { - return; + {isLoading ? ( + + ) : ( +
+ - Name -
+ onCheckedChange={(checked) => { + if (!checked) { + onCheckChange([]); + } else { + onCheckChange(workspaces); + } + }} + aria-label="Select all workspaces" + className="my-0" + /> + Name +
+ )} +
+ + Template + + + Status - Template - Status Actions
- {!workspaces && } - {workspaces && workspaces.length === 0 && ( + {isLoading && } + {isEmpty && ( = ({ /> +
{workspace.name} @@ -224,7 +233,7 @@ export const WorkspacesTable: FC = ({ )} - +
} subtitle={
@@ -355,11 +364,11 @@ const TableLoader: FC = () => { - +
- +