Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions apps/aevatar-console-web/src/locales/en-US.ts
Original file line number Diff line number Diff line change
Expand Up @@ -429,6 +429,29 @@ const enUSMessages = {
'teams.detail.runtimeStatus.retired': 'Retired',
'teams.detail.runtimeStatus.running': 'Running',
'teams.detail.runtimeStatus.waiting': 'Waiting',
'teams.detail.activity.actions.refresh': 'Refresh',
'teams.detail.activity.actions.refreshAria': 'Refresh recent activity',
'teams.detail.activity.actions.retry': 'Retry',
'teams.detail.activity.actions.runTeam': 'Run team',
'teams.detail.activity.empty.description':
'Run the team or a published member to create the first visible run.',
'teams.detail.activity.empty.title': 'No recent activity yet',
'teams.detail.activity.error.description':
'The last stable team facts remain available on Overview. Retry this read when the run list is available.',
'teams.detail.activity.error.title': 'Recent activity could not be loaded',
'teams.detail.activity.filteredEmpty.description':
'Choose another status to return to the visible runs.',
'teams.detail.activity.filteredEmpty.title':
'No recent runs match this filter',
'teams.detail.activity.filters.all': 'All',
'teams.detail.activity.filters.aria': 'Filter recent activity',
'teams.detail.activity.filters.attention': 'Needs attention',
'teams.detail.activity.filters.failed': 'Failed',
'teams.detail.activity.filters.reset': 'Show all recent runs',
'teams.detail.activity.filters.running': 'Running',
'teams.detail.activity.subtitle':
'Latest runs exposed by the current team entry service.',
'teams.detail.activity.title': 'Recent activity',
'teams.detail.status.active': 'Active',
'teams.detail.status.archived': 'Archived',
'teams.detail.status.bindReady': 'Callable',
Expand All @@ -445,6 +468,7 @@ const enUSMessages = {
'teams.detail.status.unknown': 'Unknown status',
'teams.detail.tabList.label': 'Team detail tabs',
'teams.detail.tabs.automations': 'Automations',
'teams.detail.tabs.activity': 'Activity',
'teams.detail.tabs.members': 'Team members',
'teams.detail.tabs.overview': 'Overview',
'teams.detail.test.actions.retry': 'Retry',
Expand Down
22 changes: 22 additions & 0 deletions apps/aevatar-console-web/src/locales/zh-CN.ts
Original file line number Diff line number Diff line change
Expand Up @@ -405,6 +405,27 @@ const zhCNMessages = {
'teams.detail.runtimeStatus.retired': '已停用',
'teams.detail.runtimeStatus.running': '运行中',
'teams.detail.runtimeStatus.waiting': '等待处理',
'teams.detail.activity.actions.refresh': '刷新',
'teams.detail.activity.actions.refreshAria': '刷新最近活动',
'teams.detail.activity.actions.retry': '重试',
'teams.detail.activity.actions.runTeam': '运行团队',
'teams.detail.activity.empty.description':
'运行团队或已发布成员,即可生成第一条可见运行。',
'teams.detail.activity.empty.title': '还没有最近活动',
'teams.detail.activity.error.description':
'概览中的稳定团队信息仍然可用。运行列表恢复后可重试读取。',
'teams.detail.activity.error.title': '无法加载最近活动',
'teams.detail.activity.filteredEmpty.description':
'选择其他状态可返回当前可见的运行。',
'teams.detail.activity.filteredEmpty.title': '没有符合当前筛选的最近运行',
'teams.detail.activity.filters.all': '全部',
'teams.detail.activity.filters.aria': '筛选最近活动',
'teams.detail.activity.filters.attention': '需要处理',
'teams.detail.activity.filters.failed': '失败',
'teams.detail.activity.filters.reset': '显示全部最近运行',
'teams.detail.activity.filters.running': '运行中',
'teams.detail.activity.subtitle': '当前团队入口服务公开的最新运行。',
'teams.detail.activity.title': '最近活动',
'teams.detail.status.active': '已启用',
'teams.detail.status.archived': '已归档',
'teams.detail.status.bindReady': '可调用',
Expand All @@ -421,6 +442,7 @@ const zhCNMessages = {
'teams.detail.status.unknown': '状态未知',
'teams.detail.tabList.label': '团队详情标签',
'teams.detail.tabs.automations': '自动化',
'teams.detail.tabs.activity': '活动',
'teams.detail.tabs.members': '团队成员',
'teams.detail.tabs.overview': '概览',
'teams.detail.test.actions.retry': '重试',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
import {
ClockCircleOutlined,
EyeOutlined,
InfoCircleOutlined,
} from "@ant-design/icons";
import { Button, Tooltip, Typography, theme } from "antd";
import { useIntl } from "@umijs/max";
import React from "react";
import { AevatarInspectorEmpty } from "@/shared/ui/aevatarPageShells";
import { DetailPill, FactLine } from "./TeamDetailPrimitives";

export type TeamActivityRunRow = {
readonly detailsHref?: string;
readonly detailItems: readonly {
readonly label: string;
readonly value: string;
}[];
readonly detailTooltipLabel: string;
readonly memberLabel: string;
readonly outputPreview: string;
readonly runId: string;
readonly statusKey: string;
readonly statusLabel: string;
readonly statusStyle: React.CSSProperties;
readonly updatedLabel: string;
readonly workflowLabel: string;
readonly workflowMetaLabel: string;
};

type TeamRecentRunsListProps = {
readonly emptyDescription: string;
readonly emptyTitle: string;
readonly onNavigate?: (href: string) => void;
readonly runs: readonly TeamActivityRunRow[];
};

const TeamRecentRunsList: React.FC<TeamRecentRunsListProps> = ({
emptyDescription,
emptyTitle,
onNavigate,
runs,
}) => {
const intl = useIntl();
const { token } = theme.useToken();
const handleNavigate = React.useCallback(
(href?: string) => (event: React.MouseEvent<HTMLElement>) => {
if (!href || !onNavigate) {
return;
}

event.preventDefault();
onNavigate(href);
},
[onNavigate],
);

if (runs.length === 0) {
return (
<div
style={{
alignItems: "center",
border: `1px dashed ${token.colorBorderSecondary}`,
borderRadius: 8,
display: "flex",
justifyContent: "center",
minHeight: 144,
padding: "24px 16px",
textAlign: "center",
}}
>
<AevatarInspectorEmpty
compact
description={emptyDescription}
title={emptyTitle}
/>
</div>
);
}

return (
<div style={{ display: "grid", gap: 10 }}>
{runs.map((run) => {
const statusAccent =
typeof run.statusStyle.color === "string"
? run.statusStyle.color
: token.colorBorder;

return (
<article
key={run.runId}
style={{
alignItems: "start",
background: token.colorBgContainer,
border: `1px solid ${token.colorBorderSecondary}`,
borderInlineStart: `4px solid ${statusAccent}`,
borderRadius: 8,
display: "grid",
gap: 12,
gridTemplateColumns: "minmax(0, 1fr) max-content",
minWidth: 0,
padding: 14,
}}
>
<div
style={{
display: "flex",
flexDirection: "column",
gap: 6,
minWidth: 0,
}}
>
<div
style={{
alignItems: "center",
display: "flex",
flexWrap: "wrap",
gap: 8,
}}
>
<DetailPill
compact
style={run.statusStyle}
text={run.statusLabel}
/>
<Typography.Text style={{ fontSize: 12 }} type="secondary">
<ClockCircleOutlined /> {run.updatedLabel}
</Typography.Text>
<Tooltip
title={
<div style={{ display: "grid", gap: 4 }}>
{run.detailItems.map((item) => (
<span key={item.label}>
{item.label}: {item.value}
</span>
))}
</div>
}
>
<button
aria-label={run.detailTooltipLabel}
style={{
background: "transparent",
border: 0,
color: token.colorTextTertiary,
cursor: "help",
lineHeight: 1,
padding: 0,
}}
type="button"
>
<InfoCircleOutlined />
</button>
</Tooltip>
</div>
<Typography.Text strong>{run.memberLabel}</Typography.Text>
<Typography.Text style={{ fontSize: 12 }} type="secondary">
{run.workflowMetaLabel}
</Typography.Text>
<FactLine rows={2} secondary text={run.outputPreview} />
</div>
{run.detailsHref ? (
<Tooltip
title={intl.formatMessage({
id: "teams.detail.overview.history.actions.view",
})}
>
<Button
aria-label={intl.formatMessage({
id: "teams.detail.overview.history.actions.view",
})}
href={run.detailsHref}
icon={<EyeOutlined />}
onClick={handleNavigate(run.detailsHref)}
/>
</Tooltip>
) : null}
</article>
);
})}
</div>
);
};

export default TeamRecentRunsList;
74 changes: 74 additions & 0 deletions apps/aevatar-console-web/src/pages/teams/detail.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1481,6 +1481,80 @@ describe("TeamDetailPage", () => {
expect(window.location.search).not.toContain("step=bind");
});

it("loads, filters, and refreshes team-scoped activity from a direct route", async () => {
const memberId = "member-team-alpha";
const workflowId = "workflow-1";
const publishedServiceId = "alpha-service";
window.history.replaceState(
{},
"",
`/scopes/scope-1/teams/t-alpha?memberId=${memberId}&workflowId=${workflowId}&serviceId=${publishedServiceId}&runId=run-current&tab=activity`,
);
(scopeRuntimeApi.listServiceRuns as jest.Mock).mockImplementation(async () => ({
...mockCreateRunsCatalog(),
serviceId: publishedServiceId,
serviceKey: `scope-1:${publishedServiceId}`,
runs: mockCreateRunsCatalog().runs.map((run) => ({
...run,
serviceId: publishedServiceId,
})),
}));

renderWithQueryClient(React.createElement(TeamDetailPage));

const activity = await screen.findByTestId("team-activity-tab");
expect(within(activity).getByRole("heading", { name: "最近活动" })).toBeTruthy();
expect(await within(activity).findAllByText("Team Alpha Operator")).toHaveLength(2);
expect(within(activity).getByText("Waiting on approval")).toBeTruthy();
expect(within(activity).getByText("Resolved")).toBeTruthy();
expect(screen.queryByText("当前态势")).toBeNull();
expect(screen.getByRole("navigation", { name: "面包屑" })).toHaveTextContent(
"活动",
);
expect(within(activity).getAllByRole("link", { name: "查看详情" })[0])
.toHaveAttribute(
"href",
expect.stringContaining(
`/scopes/scope-1/teams/t-alpha/members/${memberId}/runs?runId=run-current`,
),
);
await waitFor(() => {
expect(scopeRuntimeApi.listServiceRuns).toHaveBeenCalledWith(
"scope-1",
publishedServiceId,
expect.objectContaining({ take: 12 }),
);
});

fireEvent.click(within(activity).getByText("需要处理"));
expect(within(activity).getByText("Waiting on approval")).toBeTruthy();
expect(within(activity).queryByText("Resolved")).toBeNull();

fireEvent.click(within(activity).getByText("运行中"));
expect(
within(activity).getByText("没有符合当前筛选的最近运行"),
).toBeTruthy();
fireEvent.click(
within(activity).getByRole("button", { name: "显示全部最近运行" }),
);
expect(within(activity).getByText("Resolved")).toBeTruthy();

const requestCountBeforeRefresh = (
scopeRuntimeApi.listServiceRuns as jest.Mock
).mock.calls.length;
fireEvent.click(
within(activity).getByRole("button", { name: "刷新最近活动" }),
);
await waitFor(() => {
expect((scopeRuntimeApi.listServiceRuns as jest.Mock).mock.calls.length)
.toBeGreaterThan(requestCountBeforeRefresh);
});

expect(memberId).not.toBe(workflowId);
expect(memberId).not.toBe(publishedServiceId);
expect(workflowId).not.toBe(publishedServiceId);
});

it("falls legacy event deep links back to the overview tab", async () => {
window.history.replaceState(
{},
Expand Down
Loading
Loading