feat(site): add loading <Spinner /> to AgentRow (#24825)

Adds a `<Spinner />` next to the log count during agent startup.

Also, there was some complexity in sizing the spinner because `<Badge
/>` automatically sizes any `svg`s it contains to `size-icon-xs`. In
order to maintain the `svg` sizing inside existing Badges across the
site, this introduces a new `svgSize` prop that defaults to `xs`.
Existing consumers will still get `[&_svg]:size-icon-xs` regardless of
the Badge size, but can now be overridden to `sm` or `lg` (there is no
`md`).

Also also fixes a tiny spacing issue with the warning triangle
🕵️‍♂️

<img width="1203" height="624" alt="Screenshot 2026-04-29 at 3 24 35 PM"
src="https://github.com/user-attachments/assets/e4fc4a3a-e88f-4253-a697-195f8a347230"
/>
This commit is contained in:
Jeremy Ruppel
2026-05-01 10:57:30 -04:00
committed by GitHub
parent fa227be74a
commit 8709d42fe0
5 changed files with 133 additions and 19 deletions
+14 -4
View File
@@ -30,9 +30,14 @@ const badgeVariants = cva(
info: "border-border-pending bg-surface-sky text-highlight-sky shadow",
},
size: {
xs: "border-0 text-2xs font-normal h-[18px] [&_svg]:size-icon-xs rounded",
sm: "text-2xs font-normal h-5.5 py-1 [&_svg]:size-icon-xs",
md: "text-xs font-normal py-1 [&_svg]:size-icon-xs",
xs: "border-0 text-2xs font-normal h-[18px] rounded",
sm: "text-2xs font-normal h-5.5 py-1",
md: "text-xs font-normal py-1",
},
svgSize: {
xs: "[&_svg]:size-icon-xs",
sm: "[&_svg]:size-icon-sm",
lg: "[&_svg]:size-icon-lg",
},
hover: {
false: null,
@@ -54,6 +59,7 @@ const badgeVariants = cva(
defaultVariants: {
variant: "default",
size: "md",
svgSize: "xs",
hover: false,
},
},
@@ -68,6 +74,7 @@ export const Badge: React.FC<BadgeProps> = ({
className,
variant,
size,
svgSize = "xs",
hover,
asChild = false,
...props
@@ -77,7 +84,10 @@ export const Badge: React.FC<BadgeProps> = ({
return (
<Comp
{...props}
className={cn(badgeVariants({ variant, size, hover }), className)}
className={cn(
badgeVariants({ variant, size, svgSize, hover }),
className,
)}
/>
);
};
@@ -165,7 +165,7 @@ export const Example: Story = {};
export const BunchOfApps: Story = {
args: {
agent: {
...M.MockWorkspaceAgent,
...M.MockWorkspaceAgentReady,
apps: [
M.MockWorkspaceApp,
M.MockWorkspaceApp,
@@ -203,7 +203,10 @@ export const Timeout: Story = {
export const Starting: Story = {
args: {
agent: M.MockWorkspaceAgentStarting,
agent: {
...M.MockWorkspaceAgentStarting,
logs_length: logs.length,
},
},
};
@@ -357,7 +360,7 @@ export const Deprecated: Story = {
export const HideApp: Story = {
args: {
agent: {
...M.MockWorkspaceAgent,
...M.MockWorkspaceAgentReady,
apps: [
{
...M.MockWorkspaceApp,
@@ -371,7 +374,7 @@ export const HideApp: Story = {
export const GroupApp: Story = {
args: {
agent: {
...M.MockWorkspaceAgent,
...M.MockWorkspaceAgentReady,
apps: [
{
...M.MockWorkspaceApp,
+30 -2
View File
@@ -2,6 +2,7 @@ import Collapse from "@mui/material/Collapse";
import {
CopyIcon,
EllipsisIcon,
InfoIcon,
PackageIcon,
PlayIcon,
SquareCheckBigIcon,
@@ -39,6 +40,7 @@ import {
import { ExternalImage } from "#/components/ExternalImage/ExternalImage";
import type { Line } from "#/components/Logs/LogLine";
import { Skeleton } from "#/components/Skeleton/Skeleton";
import { Spinner } from "#/components/Spinner/Spinner";
import {
Tabs,
TabsContent,
@@ -157,6 +159,7 @@ export const AgentRow: FC<AgentRowProps> = ({
const hasStartupFeatures = Boolean(agent.logs_length);
const healthIssues = getAgentHealthIssues(agent);
const hasAgentIssues = healthIssues.length > 0;
const hasWarningIssues = healthIssues.some((i) => i.severity === "warning");
const failedStartTimings = agentScriptTimings?.filter(
(t) =>
t.workspace_agent_id === agent.id &&
@@ -500,9 +503,34 @@ export const AgentRow: FC<AgentRowProps> = ({
>
<ChevronDownIcon open={showLogs} />
<span>Logs</span>
{agent.lifecycle_state === "starting" &&
agent.log_sources.length > 0 &&
healthIssues.length === 0 && (
<Badge
variant="default"
size="xs"
className="ml-1.5"
svgSize="sm"
>
<Spinner
size="lg"
loading={true}
className="text-content-secondary -ml-1"
/>
<span>{agent.log_sources.length}</span>
</Badge>
)}
{healthIssues.length > 0 && (
<Badge variant="warning" size="xs" className="ml-1.5">
<TriangleAlertIcon />
<Badge
variant={hasWarningIssues ? "warning" : "info"}
size="xs"
className="ml-1.5"
>
{hasWarningIssues ? (
<TriangleAlertIcon className="-ml-0.5" />
) : (
<InfoIcon className="-ml-0.5" />
)}
<span>{healthIssues.length}</span>
</Badge>
)}
+19 -6
View File
@@ -30,7 +30,6 @@ import {
MockTasks,
MockUserOwner,
MockWorkspace,
MockWorkspaceAgent,
MockWorkspaceAgentLogSource,
MockWorkspaceAgentReady,
MockWorkspaceAgentStarting,
@@ -804,7 +803,7 @@ export const StartupScriptError: Story = {
resources: [
{
...MockWorkspaceResource,
agents: [MockWorkspaceAgent],
agents: [MockWorkspaceAgentStarting],
},
],
},
@@ -859,7 +858,7 @@ export const StartupScriptTimeout: Story = {
resources: [
{
...MockWorkspaceResource,
agents: [MockWorkspaceAgent],
agents: [MockWorkspaceAgentStarting],
},
],
},
@@ -1188,14 +1187,22 @@ export const LongDisplayName: Story = {
// Sidebar: uses getTasks() which returns an array
key: ["tasks", { owner: MockTask.owner_name }],
data: [
{ ...MockDisplayNameTasks[0], display_name: longDisplayName },
{
...MockDisplayNameTasks[0],
display_name: longDisplayName,
workspace_agent_lifecycle: "starting",
},
...MockDisplayNameTasks.slice(1),
],
},
{
// TaskTopbar: uses getTask() which returns a single task
key: ["tasks", MockTask.owner_name, MockTask.id],
data: { ...MockDisplayNameTasks[0], display_name: longDisplayName },
data: {
...MockDisplayNameTasks[0],
display_name: longDisplayName,
workspace_agent_lifecycle: "starting",
},
},
{
// Workspace data for the task
@@ -1205,7 +1212,13 @@ export const LongDisplayName: Story = {
MockTask.workspace_name,
"settings",
],
data: MockWorkspace,
data: {
...MockStartingWorkspace,
latest_build: {
...MockStartingWorkspace.latest_build,
has_ai_task: true,
},
},
},
],
},
+63 -3
View File
@@ -1009,7 +1009,7 @@ export const MockWorkspaceAgent: TypesGen.WorkspaceAgent = {
},
connection_timeout_seconds: 120,
troubleshooting_url: "https://coder.com/troubleshoot",
lifecycle_state: "starting",
lifecycle_state: "ready",
logs_length: 0,
logs_overflowed: false,
log_sources: [MockWorkspaceAgentLogSource],
@@ -1496,7 +1496,16 @@ export const MockFavoriteWorkspace: TypesGen.Workspace = {
export const MockStoppedWorkspace: TypesGen.Workspace = {
...MockWorkspace,
id: "test-stopped-workspace",
latest_build: { ...MockWorkspaceBuildStop, status: "stopped" },
latest_build: {
...MockWorkspaceBuildStop,
status: "stopped",
resources: [
{
...MockWorkspaceResource,
agents: [MockWorkspaceAgentOff],
},
],
},
};
export const MockStoppingWorkspace: TypesGen.Workspace = {
...MockWorkspace,
@@ -1505,6 +1514,12 @@ export const MockStoppingWorkspace: TypesGen.Workspace = {
...MockWorkspaceBuildStop,
job: MockRunningProvisionerJob,
status: "stopping",
resources: [
{
...MockWorkspaceResource,
agents: [MockWorkspaceAgentShuttingDown],
},
],
},
};
export const MockUnhealthyWorkspace: TypesGen.Workspace = {
@@ -1529,6 +1544,12 @@ export const MockStartingWorkspace: TypesGen.Workspace = {
job: MockRunningProvisionerJob,
transition: "start",
status: "starting",
resources: [
{
...MockWorkspaceResource,
agents: [MockWorkspaceAgentStarting],
},
],
},
};
export const MockCancelingWorkspace: TypesGen.Workspace = {
@@ -1538,6 +1559,12 @@ export const MockCancelingWorkspace: TypesGen.Workspace = {
...MockWorkspaceBuild,
job: MockCancelingProvisionerJob,
status: "canceling",
resources: [
{
...MockWorkspaceResource,
agents: [MockWorkspaceAgentShuttingDown],
},
],
},
};
export const MockCanceledWorkspace: TypesGen.Workspace = {
@@ -1547,6 +1574,12 @@ export const MockCanceledWorkspace: TypesGen.Workspace = {
...MockWorkspaceBuild,
job: MockCanceledProvisionerJob,
status: "canceled",
resources: [
{
...MockWorkspaceResource,
agents: [MockWorkspaceAgentOff],
},
],
},
};
export const MockFailedWorkspace: TypesGen.Workspace = {
@@ -1556,6 +1589,12 @@ export const MockFailedWorkspace: TypesGen.Workspace = {
...MockWorkspaceBuild,
job: MockFailedProvisionerJob,
status: "failed",
resources: [
{
...MockWorkspaceResource,
agents: [MockWorkspaceAgentStartError],
},
],
},
};
export const MockDeletingWorkspace: TypesGen.Workspace = {
@@ -1565,6 +1604,12 @@ export const MockDeletingWorkspace: TypesGen.Workspace = {
...MockWorkspaceBuildDelete,
job: MockRunningProvisionerJob,
status: "deleting",
resources: [
{
...MockWorkspaceResource,
agents: [MockWorkspaceAgentShuttingDown],
},
],
},
};
@@ -1576,7 +1621,16 @@ const MockWorkspaceWithDeletion = {
export const MockDeletedWorkspace: TypesGen.Workspace = {
...MockWorkspace,
id: "test-deleted-workspace",
latest_build: { ...MockWorkspaceBuildDelete, status: "deleted" },
latest_build: {
...MockWorkspaceBuildDelete,
status: "deleted",
resources: [
{
...MockWorkspaceResource,
agents: [MockWorkspaceAgentOff],
},
],
},
};
export const MockOutdatedWorkspace: TypesGen.Workspace = {
@@ -1648,6 +1702,12 @@ export const MockPendingWorkspace: TypesGen.Workspace = {
job: MockPendingProvisionerJob,
transition: "start",
status: "pending",
resources: [
{
...MockWorkspaceResource,
agents: [MockWorkspaceAgentConnecting],
},
],
},
};