From efe3cabaaf9ef68101a1926a93c939316fd16cd2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Flor=C3=AAncio?= Date: Wed, 26 Aug 2026 21:05:26 +0000 Subject: [PATCH] fix: restore automation sidebar running indicator (#3064) --- .../sections/automations-section.test.tsx | 74 +++++++++++ .../sections/automations-section.tsx | 26 ++-- .../runs/use-automation-summaries.test.ts | 17 +++ .../runs/use-automation-summaries.ts | 6 +- .../web/e2e/tests/automations-sidebar.spec.ts | 38 ++++++ .../plan.md | 118 ++++++++++++++++++ .../task-01-live-sidebar-indicator.md | 102 +++++++++++++++ .../office/requirements/automation-runs.md | 1 + .../office/system-design/automation-runs.md | 15 +++ 9 files changed, 389 insertions(+), 8 deletions(-) create mode 100644 docs/plans/automation-sidebar-running-indicator/plan.md create mode 100644 docs/plans/automation-sidebar-running-indicator/task-01-live-sidebar-indicator.md diff --git a/apps/web/components/app-sidebar/sections/automations-section.test.tsx b/apps/web/components/app-sidebar/sections/automations-section.test.tsx index 8a7c0ea3c4..a2a9dab096 100644 --- a/apps/web/components/app-sidebar/sections/automations-section.test.tsx +++ b/apps/web/components/app-sidebar/sections/automations-section.test.tsx @@ -2,6 +2,7 @@ import { act, cleanup, render, screen, waitFor } from "@testing-library/react"; import { TooltipProvider } from "@kandev/ui/tooltip"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import type { Automation } from "@/lib/types/automation"; +import { LIVE_REFRESH_INTERVAL_MS } from "@/components/runs/use-live-refresh"; const WORKSPACE_ID = "workspace-1"; const AUTOMATION_ID = "automation-1"; @@ -12,6 +13,7 @@ const mocks = vi.hoisted(() => ({ listAutomationSummaries: vi.fn(), activeWorkspaceId: { current: "workspace-1" as string | undefined }, sectionExpanded: { current: {} as Record }, + responsive: { isMobile: false }, toggleSection: vi.fn(), setCollapsed: vi.fn(), })); @@ -43,6 +45,10 @@ vi.mock("@/lib/routing/client-router", () => ({ useRouter: () => ({ push: vi.fn() }), })); +vi.mock("@/hooks/use-responsive-breakpoint", () => ({ + useResponsiveBreakpoint: () => mocks.responsive, +})); + import { AutomationsSection } from "./automations-section"; function mkAutomation(overrides: Partial = {}): Automation { @@ -77,6 +83,9 @@ function renderOpenSection() { beforeEach(() => { mocks.activeWorkspaceId.current = WORKSPACE_ID; mocks.sectionExpanded.current = {}; + mocks.responsive.isMobile = false; + mocks.listAutomations.mockReset(); + mocks.listAutomationSummaries.mockReset(); mocks.listAutomations.mockResolvedValue([mkAutomation()]); mocks.listAutomationSummaries.mockResolvedValue([]); }); @@ -223,6 +232,71 @@ describe("AutomationsSection", () => { }); }); +describe("AutomationsSection live running state", () => { + it("refreshes a visible row and swaps its indicator as the run state changes", async () => { + mocks.listAutomationSummaries + .mockResolvedValueOnce([{ automation_id: AUTOMATION_ID, open_runs: 0 }]) + .mockResolvedValueOnce([{ automation_id: AUTOMATION_ID, open_runs: 1 }]) + .mockResolvedValueOnce([ + { + automation_id: AUTOMATION_ID, + open_runs: 0, + last_run: { + id: "run-1", + automation_id: AUTOMATION_ID, + status: "succeeded", + created_at: new Date().toISOString(), + }, + }, + ]); + + vi.useFakeTimers({ shouldAdvanceTime: true }); + try { + renderOpenSection(); + await screen.findByTestId(`sidebar-automation-${AUTOMATION_ID}`); + await waitFor(() => expect(mocks.listAutomationSummaries).toHaveBeenCalledTimes(1)); + + await act(async () => { + await vi.advanceTimersByTimeAsync(LIVE_REFRESH_INTERVAL_MS); + }); + const runningIndicator = await screen.findByTestId( + `sidebar-automation-running-${AUTOMATION_ID}`, + ); + expect(runningIndicator.classList).toContain("animate-spin"); + expect(runningIndicator.getAttribute("aria-hidden")).toBe("true"); + expect(screen.getByText("Running.")).toBeTruthy(); + + await act(async () => { + await vi.advanceTimersByTimeAsync(LIVE_REFRESH_INTERVAL_MS); + }); + await waitFor(() => expect(mocks.listAutomationSummaries).toHaveBeenCalledTimes(3)); + expect(screen.queryByTestId(`sidebar-automation-running-${AUTOMATION_ID}`)).toBeNull(); + expect(screen.getByText("Idle.")).toBeTruthy(); + } finally { + vi.useRealTimers(); + } + }); +}); + +describe("AutomationsSection responsive visibility", () => { + it("does not fetch or poll health while the desktop rail is hidden on mobile", async () => { + mocks.responsive.isMobile = true; + vi.useFakeTimers({ shouldAdvanceTime: true }); + try { + renderOpenSection(); + await screen.findByTestId(`sidebar-automation-${AUTOMATION_ID}`); + + await act(async () => { + await vi.advanceTimersByTimeAsync(LIVE_REFRESH_INTERVAL_MS * 2); + }); + + expect(mocks.listAutomationSummaries).not.toHaveBeenCalled(); + } finally { + vi.useRealTimers(); + } + }); +}); + describe("AutomationsSection empty state", () => { it("keeps an empty section folded until the user opens it", () => { mocks.listAutomations.mockResolvedValue([]); diff --git a/apps/web/components/app-sidebar/sections/automations-section.tsx b/apps/web/components/app-sidebar/sections/automations-section.tsx index 0c451a94b7..1bb3098c32 100644 --- a/apps/web/components/app-sidebar/sections/automations-section.tsx +++ b/apps/web/components/app-sidebar/sections/automations-section.tsx @@ -1,7 +1,7 @@ "use client"; import { useTranslation } from "react-i18next"; -import { IconBolt, IconListDetails } from "@tabler/icons-react"; +import { IconBolt, IconListDetails, IconLoader2 } from "@tabler/icons-react"; import Link from "@/components/routing/app-link"; import { Tooltip, TooltipContent, TooltipTrigger } from "@kandev/ui/tooltip"; import { useAppStore } from "@/components/state-provider"; @@ -12,9 +12,11 @@ import { } from "@/components/runs/automation-rows"; import type { AutomationRow } from "@/components/runs/automation-rows"; import { useAutomationSummaries } from "@/components/runs/use-automation-summaries"; +import { useLiveRefresh } from "@/components/runs/use-live-refresh"; import { useWorkspaceAutomations } from "@/components/runs/use-workspace-automations"; import { AUTOMATIONS_HREF } from "@/components/runs/runs-view"; import { usePathname } from "@/lib/routing/client-router"; +import { useResponsiveBreakpoint } from "@/hooks/use-responsive-breakpoint"; import { useNow } from "@/hooks/use-now"; import { cn, formatRelativeTime } from "@/lib/utils"; import { @@ -78,10 +80,18 @@ function AutomationRowLink({ row, active }: { row: AutomationRow; active: boolea active ? SIDEBAR_ITEM_ACTIVE : SIDEBAR_ITEM_INACTIVE, )} > -