mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
fix(site): hide table headers during loading and empty states on workspaces page (#23446)
## 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 <jaayden@coder.com>
This commit is contained in:
@@ -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) => ({
|
||||
|
||||
@@ -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<WorkspacesTableProps> = ({
|
||||
chatsByWorkspace,
|
||||
}) => {
|
||||
const dashboard = useDashboard();
|
||||
const isLoading = !workspaces;
|
||||
const isEmpty = workspaces && workspaces.length === 0;
|
||||
const hideHeaders = isLoading || isEmpty;
|
||||
|
||||
return (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-1/3">
|
||||
<div className="flex items-center gap-5">
|
||||
<Checkbox
|
||||
disabled={!workspaces || workspaces.length === 0}
|
||||
checked={
|
||||
workspaces &&
|
||||
workspaces.length > 0 &&
|
||||
checkedWorkspaces.length === workspaces.length
|
||||
}
|
||||
onCheckedChange={(checked) => {
|
||||
if (!workspaces) {
|
||||
return;
|
||||
{isLoading ? (
|
||||
<Skeleton className="h-4 w-40" />
|
||||
) : (
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center gap-5",
|
||||
isEmpty && "invisible",
|
||||
)}
|
||||
>
|
||||
<Checkbox
|
||||
disabled={isEmpty}
|
||||
checked={
|
||||
!isEmpty && checkedWorkspaces.length === workspaces.length
|
||||
}
|
||||
|
||||
if (!checked) {
|
||||
onCheckChange([]);
|
||||
} else {
|
||||
onCheckChange(workspaces);
|
||||
}
|
||||
}}
|
||||
aria-label="Select all workspaces"
|
||||
className="my-0"
|
||||
/>
|
||||
Name
|
||||
</div>
|
||||
onCheckedChange={(checked) => {
|
||||
if (!checked) {
|
||||
onCheckChange([]);
|
||||
} else {
|
||||
onCheckChange(workspaces);
|
||||
}
|
||||
}}
|
||||
aria-label="Select all workspaces"
|
||||
className="my-0"
|
||||
/>
|
||||
Name
|
||||
</div>
|
||||
)}
|
||||
</TableHead>
|
||||
<TableHead className={cn("w-1/3", hideHeaders && "invisible")}>
|
||||
Template
|
||||
</TableHead>
|
||||
<TableHead className={cn("w-1/3", hideHeaders && "invisible")}>
|
||||
Status
|
||||
</TableHead>
|
||||
<TableHead className="w-1/3">Template</TableHead>
|
||||
<TableHead className="w-1/3">Status</TableHead>
|
||||
<TableHead className="w-0">
|
||||
<span className="sr-only">Actions</span>
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody className="[&_td]:h-[72px]">
|
||||
{!workspaces && <TableLoader />}
|
||||
{workspaces && workspaces.length === 0 && (
|
||||
{isLoading && <TableLoader />}
|
||||
{isEmpty && (
|
||||
<TableRow>
|
||||
<TableCell colSpan={999}>
|
||||
<WorkspacesEmpty
|
||||
@@ -198,7 +207,7 @@ export const WorkspacesTable: FC<WorkspacesTableProps> = ({
|
||||
/>
|
||||
<AvatarData
|
||||
title={
|
||||
<Stack direction="row" spacing={0.5} alignItems="center">
|
||||
<div className="flex items-center gap-0.5">
|
||||
<span className="whitespace-nowrap">
|
||||
{workspace.name}
|
||||
</span>
|
||||
@@ -224,7 +233,7 @@ export const WorkspacesTable: FC<WorkspacesTableProps> = ({
|
||||
</Link>
|
||||
</Badge>
|
||||
)}
|
||||
</Stack>
|
||||
</div>
|
||||
}
|
||||
subtitle={
|
||||
<div className="flex items-center gap-1">
|
||||
@@ -355,11 +364,11 @@ const TableLoader: FC = () => {
|
||||
<AvatarDataSkeleton />
|
||||
</TableCell>
|
||||
<TableCell className="w-2/6">
|
||||
<Skeleton variant="text" width="50%" />
|
||||
<Skeleton className="h-4 w-1/2" />
|
||||
</TableCell>
|
||||
<TableCell className="w-0 ">
|
||||
<div className="flex gap-1 justify-end">
|
||||
<Skeleton variant="rounded" width={40} height={40} />
|
||||
<Skeleton className="h-10 w-10" />
|
||||
<Button size="icon-lg" variant="subtle" disabled>
|
||||
<EllipsisVertical aria-hidden="true" />
|
||||
</Button>
|
||||
|
||||
Reference in New Issue
Block a user