mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
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:
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -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],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user