From f2bce7e8f1b60e87249b3f2c5044f5afb27a885d Mon Sep 17 00:00:00 2001 From: AbigailDeng Date: Sat, 1 Aug 2026 14:34:20 +0800 Subject: [PATCH] Unify automation rows with owning runs --- apps/aevatar-console-web/src/locales/en-US.ts | 3 + apps/aevatar-console-web/src/locales/zh-CN.ts | 3 + .../teams/tabs/TeamAutomationsTab.test.tsx | 89 +++++++++++++++++++ .../pages/teams/tabs/TeamAutomationsTab.tsx | 28 +++++- 4 files changed, 122 insertions(+), 1 deletion(-) diff --git a/apps/aevatar-console-web/src/locales/en-US.ts b/apps/aevatar-console-web/src/locales/en-US.ts index 16bf596ed..7437bf95c 100644 --- a/apps/aevatar-console-web/src/locales/en-US.ts +++ b/apps/aevatar-console-web/src/locales/en-US.ts @@ -786,6 +786,7 @@ const enUSMessages = { 'teams.automations.actions.retryRevocation': 'Retry revocation', 'teams.automations.actions.resume': 'Resume', 'teams.automations.actions.runNow': 'Run now', + 'teams.automations.actions.viewRuns': 'View runs', 'teams.automations.actions.runHistory': 'View member run history', 'teams.automations.actions.tryAgain': 'Try again', 'teams.automations.authorization.back': 'Back', @@ -993,6 +994,8 @@ const enUSMessages = { 'teams.automations.preview.weekly.title': 'Weekly release handoff', 'teams.automations.previewOnly': 'Automation API wiring is coming next.', 'teams.automations.row.nextRun': 'Next {time}', + 'teams.automations.row.lastRun': 'Last {time}', + 'teams.automations.row.noLastRun': 'No previous run', 'teams.automations.row.awaitingReadModel': 'Waiting for schedule sync', 'teams.automations.row.manualRunRequested': 'Run requested {time}', 'teams.automations.row.noNextRun': 'No next run', diff --git a/apps/aevatar-console-web/src/locales/zh-CN.ts b/apps/aevatar-console-web/src/locales/zh-CN.ts index e6ba6949e..8994938e2 100644 --- a/apps/aevatar-console-web/src/locales/zh-CN.ts +++ b/apps/aevatar-console-web/src/locales/zh-CN.ts @@ -745,6 +745,7 @@ const zhCNMessages = { 'teams.automations.actions.retryRevocation': '重试撤销', 'teams.automations.actions.resume': '恢复', 'teams.automations.actions.runNow': '立即运行', + 'teams.automations.actions.viewRuns': '查看运行记录', 'teams.automations.actions.runHistory': '查看成员运行记录', 'teams.automations.actions.tryAgain': '重试', 'teams.automations.authorization.back': '返回', @@ -936,6 +937,8 @@ const zhCNMessages = { 'teams.automations.preview.weekly.title': '每周发布交接', 'teams.automations.previewOnly': '自动化 API 接入会在下一步完成。', 'teams.automations.row.nextRun': '下次 {time}', + 'teams.automations.row.lastRun': '上次 {time}', + 'teams.automations.row.noLastRun': '暂无历史运行', 'teams.automations.row.awaitingReadModel': '正在等待计划同步', 'teams.automations.row.manualRunRequested': '已在 {time} 请求运行', 'teams.automations.row.noNextRun': '暂无下次运行', diff --git a/apps/aevatar-console-web/src/pages/teams/tabs/TeamAutomationsTab.test.tsx b/apps/aevatar-console-web/src/pages/teams/tabs/TeamAutomationsTab.test.tsx index d22d297be..84f7bcad6 100644 --- a/apps/aevatar-console-web/src/pages/teams/tabs/TeamAutomationsTab.test.tsx +++ b/apps/aevatar-console-web/src/pages/teams/tabs/TeamAutomationsTab.test.tsx @@ -415,6 +415,95 @@ describe("TeamAutomationsTab canonical member authority", () => { ); }); + it("opens schedule-filtered runs from the row's canonical member authority", async () => { + const otherMember = { + ...member, + key: "m-beta", + memberId: "m-beta", + name: "Reviewer", + serviceId: "svc-beta", + }; + (teamAutomationApi.listAll as jest.Mock).mockResolvedValue({ + items: [ + automationView({ + displayName: "Reviewer review", + memberId: "m-beta", + publishedServiceId: "svc-beta", + scheduleId: "sch-beta", + }), + ], + nextCursor: null, + totalCount: 1, + }); + + renderTab("", [member, otherMember]); + + const reviewerRow = await screen.findByRole("article", { + name: "Reviewer review", + }); + fireEvent.click( + within(reviewerRow).getByRole("button", { name: "View runs" }), + ); + + expect(history.push).toHaveBeenCalledWith( + "/scopes/scope-alpha/teams/team-alpha/members/m-beta/runs?scheduleId=sch-beta", + ); + }); + + it("shows the schedule timezone with next and last fire state", async () => { + (teamAutomationApi.listAll as jest.Mock).mockResolvedValue({ + items: [ + automationView({ + lastFireAt: "2026-07-28T01:00:00Z", + timezone: "Asia/Singapore", + }), + ], + nextCursor: null, + totalCount: 1, + }); + + renderTab("m-alpha"); + + const row = await screen.findByRole("article", { name: "Daily review" }); + expect(within(row).getByText("Asia/Singapore")).toBeInTheDocument(); + expect(within(row).getByText(/^Next /)).toBeInTheDocument(); + expect(within(row).getByText(/^Last /)).toBeInTheDocument(); + }); + + it("keeps Run now single-flight while the accepted request is pending", async () => { + let resolveRunNow: ((value: unknown) => void) | undefined; + (teamAutomationApi.listAll as jest.Mock).mockResolvedValue({ + items: [automationView()], + nextCursor: null, + totalCount: 1, + }); + (teamAutomationApi.runNow as jest.Mock).mockImplementation( + () => + new Promise((resolve) => { + resolveRunNow = resolve; + }), + ); + renderTab("m-alpha"); + + const runNow = await screen.findByRole("button", { name: "Run now" }); + fireEvent.click(runNow); + fireEvent.click(runNow); + + expect(teamAutomationApi.runNow).toHaveBeenCalledTimes(1); + expect(runNow).toBeDisabled(); + + await act(async () => { + resolveRunNow?.({ + accepted: true, + commandId: "cmd-run", + operationId: "op-run", + scheduleId: "sch-alpha", + status: "accepted", + }); + await Promise.resolve(); + }); + }); + it("opens the complete dev form directly from the Team shell", async () => { renderTab(); diff --git a/apps/aevatar-console-web/src/pages/teams/tabs/TeamAutomationsTab.tsx b/apps/aevatar-console-web/src/pages/teams/tabs/TeamAutomationsTab.tsx index b48d710bf..ca359ea27 100644 --- a/apps/aevatar-console-web/src/pages/teams/tabs/TeamAutomationsTab.tsx +++ b/apps/aevatar-console-web/src/pages/teams/tabs/TeamAutomationsTab.tsx @@ -4,6 +4,7 @@ import { DeleteOutlined, EditOutlined, ExclamationCircleOutlined, + HistoryOutlined, PauseCircleOutlined, PlayCircleOutlined, PlusOutlined, @@ -47,8 +48,10 @@ import { previewScheduledDispatch } from "@/shared/api/scheduledDispatchApi"; import { NyxIDAuthClient } from "@/shared/auth/client"; import { getNyxIDRuntimeConfig } from "@/shared/auth/config"; import { formatCompactDateTime } from "@/shared/datetime/dateTime"; +import { history } from "@/shared/navigation/history"; import { buildTeamMemberAutomationsHref, + buildTeamMemberPublishedRunsHref, } from "@/shared/navigation/teamRoutes"; import { AevatarInspectorEmpty, @@ -269,7 +272,7 @@ const responsiveStyle = ` @media (max-width: 520px) { .team-automation-actions { display: grid !important; - grid-template-columns: repeat(4, minmax(0, 1fr)); + grid-template-columns: repeat(5, minmax(0, 1fr)); } .team-automation-actions .ant-btn { @@ -1371,6 +1374,19 @@ const TeamAutomationsTab: React.FC = ({ border: `1px solid ${token.colorBorderSecondary}`, }} > + {renderAutomationActionButton({ + icon: , + label: copy("teams.automations.actions.viewRuns", "View runs"), + onClick: () => + history.push( + buildTeamMemberPublishedRunsHref({ + scopeId: view.scopeId, + teamId: view.teamId, + memberId: view.memberId, + scheduleId: view.scheduleId, + }), + ), + })} {active ? renderAutomationActionButton({ icon: , @@ -1502,6 +1518,9 @@ const TeamAutomationsTab: React.FC = ({
+ + {view.timezone} + {view.nextFireAt ? copy("teams.automations.row.nextRun", "Next {time}", { @@ -1509,6 +1528,13 @@ const TeamAutomationsTab: React.FC = ({ }) : copy("teams.automations.row.noNextRun", "No next run")} + + {view.lastFireAt + ? copy("teams.automations.row.lastRun", "Last {time}", { + time: formatCompactDateTime(view.lastFireAt, "--"), + }) + : copy("teams.automations.row.noLastRun", "No previous run")} +
{renderActions(view)}