Skip to content

Commit 91ac5f9

Browse files
feat(schedule-details): wire live workflow markers into metrics chart (SLICE-9.2 / PR09e)
Replace static chart fixtures with useListWorkflowsForSchedule and useDescribeSchedule, map workflow pages to timeline points, pan/scroll into older time to fetchNextPage, and show loading skeletons while data loads. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 74502b3 commit 91ac5f9

18 files changed

Lines changed: 1075 additions & 68 deletions
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { getMockRunningDescribeScheduleResponse } from '@/route-handlers/describe-schedule/__fixtures__/mock-describe-schedule-response';
2+
import { getMockWorkflowListItem } from '@/route-handlers/list-workflows/__fixtures__/mock-workflow-list-items';
3+
import { type ListWorkflowsResponse } from '@/route-handlers/list-workflows/list-workflows.types';
4+
import { SCHEDULE_WORKFLOWS_VISIBILITY_SORT_COLUMN } from '@/views/shared/hooks/use-list-workflows-for-schedule/use-list-workflows-for-schedule.constants';
5+
6+
export const SCHEDULE_METRICS_CHART_API_FIXTURE_NOW_MS = 6 * 60 * 60 * 1000;
7+
8+
const HOUR_MS = 60 * 60 * 1000;
9+
10+
export const MOCK_DOMAIN = 'test-domain';
11+
export const MOCK_CLUSTER = 'test-cluster';
12+
export const MOCK_SCHEDULE_ID = 'my-schedule';
13+
14+
export function getMockDescribeScheduleResponseForChart() {
15+
return getMockRunningDescribeScheduleResponse({
16+
info: {
17+
lastRunTime: { seconds: '21600', nanos: 0 },
18+
nextRunTime: { seconds: '25200', nanos: 0 },
19+
totalRuns: '3',
20+
createTime: null,
21+
lastUpdateTime: null,
22+
missedRuns: '0',
23+
skippedRuns: '0',
24+
ongoingBackfills: [],
25+
},
26+
});
27+
}
28+
29+
export function getMockWorkflowPagesForChart(): Array<ListWorkflowsResponse> {
30+
return [
31+
{
32+
workflows: [
33+
getMockWorkflowListItem({
34+
workflowID: 'wf-recent',
35+
status: 'WORKFLOW_EXECUTION_CLOSE_STATUS_COMPLETED',
36+
historyLength: 10,
37+
closeTime: 5 * HOUR_MS + 1000,
38+
startTime: 5 * HOUR_MS,
39+
searchAttributes: {
40+
[SCHEDULE_WORKFLOWS_VISIBILITY_SORT_COLUMN]: {
41+
data: String(5 * HOUR_MS),
42+
},
43+
},
44+
}),
45+
getMockWorkflowListItem({
46+
workflowID: 'wf-middle',
47+
status: 'WORKFLOW_EXECUTION_CLOSE_STATUS_COMPLETED',
48+
historyLength: 10,
49+
closeTime: 4.5 * HOUR_MS + 1000,
50+
startTime: 4.5 * HOUR_MS,
51+
searchAttributes: {
52+
[SCHEDULE_WORKFLOWS_VISIBILITY_SORT_COLUMN]: {
53+
data: String(4.5 * HOUR_MS),
54+
},
55+
},
56+
}),
57+
],
58+
nextPage: 'page-2',
59+
},
60+
{
61+
workflows: [
62+
getMockWorkflowListItem({
63+
workflowID: 'wf-older',
64+
status: 'WORKFLOW_EXECUTION_CLOSE_STATUS_COMPLETED',
65+
historyLength: 10,
66+
closeTime: 3 * HOUR_MS + 1000,
67+
startTime: 3 * HOUR_MS,
68+
searchAttributes: {
69+
[SCHEDULE_WORKFLOWS_VISIBILITY_SORT_COLUMN]: {
70+
data: String(3 * HOUR_MS),
71+
},
72+
},
73+
}),
74+
],
75+
nextPage: '',
76+
},
77+
];
78+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { getMockRunningDescribeScheduleResponse } from '@/route-handlers/describe-schedule/__fixtures__/mock-describe-schedule-response';
2+
3+
import describeScheduleToNextExecutionMs from '../helpers/describe-schedule-to-next-execution';
4+
5+
describe(describeScheduleToNextExecutionMs.name, () => {
6+
it('returns next run time in milliseconds', () => {
7+
const nextExecutionMs = describeScheduleToNextExecutionMs(
8+
getMockRunningDescribeScheduleResponse({
9+
info: {
10+
lastRunTime: null,
11+
nextRunTime: { seconds: '1745490629', nanos: 850000000 },
12+
totalRuns: '1',
13+
createTime: null,
14+
lastUpdateTime: null,
15+
missedRuns: '0',
16+
skippedRuns: '0',
17+
ongoingBackfills: [],
18+
},
19+
})
20+
);
21+
22+
expect(nextExecutionMs).toBe(1745490629850);
23+
});
24+
25+
it('returns null when describe has no next run time', () => {
26+
expect(describeScheduleToNextExecutionMs(undefined)).toBeNull();
27+
});
28+
});
Lines changed: 52 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,20 @@
11
import React from 'react';
22

3-
import { render, screen } from '@/test-utils/rtl';
3+
import { HttpResponse } from 'msw';
44

5+
import { getMockRunningDescribeScheduleResponse } from '@/route-handlers/describe-schedule/__fixtures__/mock-describe-schedule-response';
6+
import { render, screen, waitFor } from '@/test-utils/rtl';
7+
8+
import {
9+
MOCK_CLUSTER,
10+
MOCK_DOMAIN,
11+
MOCK_SCHEDULE_ID,
12+
SCHEDULE_METRICS_CHART_API_FIXTURE_NOW_MS,
13+
} from '../__fixtures__/schedule-detail-metrics-chart-api-fixture';
514
import {
6-
CHART_EMPTY_STATE_MESSAGE,
15+
CHART_LOADING_SKELETON_TEST_ID,
716
CHART_REGION_ARIA_LABEL,
17+
CHART_SERIES_TEST_IDS,
818
} from '../schedule-detail-metrics-chart.constants';
919
import ScheduleDetailMetricsChart from '../schedule-detail-metrics-chart';
1020

@@ -16,38 +26,57 @@ jest.mock('@visx/responsive', () => ({
1626
}) => <>{children({ width: 800, height: 280 })}</>,
1727
}));
1828

19-
jest.mock('../__fixtures__/schedule-detail-metrics-chart-fixture', () => ({
20-
SCHEDULE_METRICS_CHART_FIXTURE_NOW_MS: new Date(
21-
'2024-06-15T12:00:00.000Z'
22-
).getTime(),
23-
scheduleMetricsChartFixture: {
24-
successfulRuns: [],
25-
missedExecutions: [],
26-
nextExecutionTimeMs: null,
27-
},
28-
}));
29+
describe(`${ScheduleDetailMetricsChart.name} empty workflows`, () => {
30+
beforeEach(() => {
31+
jest.useFakeTimers({ now: SCHEDULE_METRICS_CHART_API_FIXTURE_NOW_MS });
32+
});
33+
34+
afterEach(() => {
35+
jest.useRealTimers();
36+
});
2937

30-
describe(`${ScheduleDetailMetricsChart.name} empty state`, () => {
31-
it('renders empty state when there is no chart data', () => {
38+
it('renders a default timeline when workflows are empty but describe succeeds', async () => {
3239
render(
3340
<ScheduleDetailMetricsChart
3441
params={{
35-
domain: 'test-domain',
36-
cluster: 'test-cluster',
37-
scheduleId: 'my-schedule',
42+
domain: MOCK_DOMAIN,
43+
cluster: MOCK_CLUSTER,
44+
scheduleId: MOCK_SCHEDULE_ID,
3845
scheduleTab: 'runs',
3946
}}
40-
/>
47+
/>,
48+
{
49+
endpointsMocks: [
50+
{
51+
path: `/api/domains/${MOCK_DOMAIN}/${MOCK_CLUSTER}/schedules/${MOCK_SCHEDULE_ID}`,
52+
httpMethod: 'GET',
53+
httpResolver: async () =>
54+
HttpResponse.json(getMockRunningDescribeScheduleResponse()),
55+
},
56+
{
57+
path: `/api/domains/${MOCK_DOMAIN}/${MOCK_CLUSTER}/workflows`,
58+
httpMethod: 'GET',
59+
httpResolver: async () =>
60+
HttpResponse.json({ workflows: [], nextPage: '' }),
61+
},
62+
],
63+
}
4164
);
4265

66+
await waitFor(() => {
67+
expect(
68+
screen.queryByTestId(CHART_LOADING_SKELETON_TEST_ID)
69+
).not.toBeInTheDocument();
70+
});
71+
4372
expect(
4473
screen.getByRole('region', { name: CHART_REGION_ARIA_LABEL })
4574
).toBeInTheDocument();
46-
expect(screen.getByRole('status')).toHaveTextContent(
47-
CHART_EMPTY_STATE_MESSAGE
48-
);
4975
expect(
50-
screen.queryByTestId('schedule-metrics-chart-canvas')
51-
).not.toBeInTheDocument();
76+
screen.getByTestId('schedule-metrics-chart-canvas')
77+
).toBeInTheDocument();
78+
expect(
79+
screen.queryAllByTestId(CHART_SERIES_TEST_IDS.successfulRunMarker)
80+
).toHaveLength(0);
5281
});
5382
});
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import React from 'react';
2+
3+
import { HttpResponse } from 'msw';
4+
import { act } from '@testing-library/react';
5+
6+
import { type ListWorkflowsResponse } from '@/route-handlers/list-workflows/list-workflows.types';
7+
import { fireEvent, render, screen, waitFor } from '@/test-utils/rtl';
8+
9+
import {
10+
getMockDescribeScheduleResponseForChart,
11+
getMockWorkflowPagesForChart,
12+
MOCK_CLUSTER,
13+
MOCK_DOMAIN,
14+
MOCK_SCHEDULE_ID,
15+
SCHEDULE_METRICS_CHART_API_FIXTURE_NOW_MS,
16+
} from '../__fixtures__/schedule-detail-metrics-chart-api-fixture';
17+
import {
18+
CHART_FETCH_LOADING_TEST_ID,
19+
CHART_LOADING_SKELETON_TEST_ID,
20+
CHART_SERIES_TEST_IDS,
21+
} from '../schedule-detail-metrics-chart.constants';
22+
import ScheduleDetailMetricsChart from '../schedule-detail-metrics-chart';
23+
24+
jest.mock('@visx/responsive', () => ({
25+
ParentSize: ({
26+
children,
27+
}: {
28+
children: (args: { width: number; height: number }) => React.ReactNode;
29+
}) => <>{children({ width: 800, height: 280 })}</>,
30+
}));
31+
32+
describe(`${ScheduleDetailMetricsChart.name} scroll fetch`, () => {
33+
beforeEach(() => {
34+
jest.useFakeTimers({ now: SCHEDULE_METRICS_CHART_API_FIXTURE_NOW_MS });
35+
});
36+
37+
afterEach(() => {
38+
jest.useRealTimers();
39+
});
40+
41+
it('loads the next workflow page when panning horizontally into older time', async () => {
42+
const workflowPages = getMockWorkflowPagesForChart();
43+
const { getWorkflowRequestCount } = setup({ workflowPages });
44+
45+
await waitFor(() => {
46+
expect(
47+
screen.queryByTestId(CHART_LOADING_SKELETON_TEST_ID)
48+
).not.toBeInTheDocument();
49+
});
50+
51+
expect(getWorkflowRequestCount()).toBe(1);
52+
expect(
53+
screen.getAllByTestId(CHART_SERIES_TEST_IDS.successfulRunMarker)
54+
).toHaveLength(2);
55+
56+
const canvas = screen.getByTestId('schedule-metrics-chart-canvas');
57+
58+
await act(async () => {
59+
fireEvent.wheel(canvas, { deltaY: -4000 });
60+
});
61+
62+
await waitFor(() => {
63+
expect(getWorkflowRequestCount()).toBeGreaterThan(1);
64+
});
65+
66+
await waitFor(() => {
67+
expect(
68+
screen.getAllByTestId(CHART_SERIES_TEST_IDS.successfulRunMarker)
69+
).toHaveLength(3);
70+
});
71+
72+
expect(
73+
screen.queryByTestId(CHART_FETCH_LOADING_TEST_ID)
74+
).not.toBeInTheDocument();
75+
});
76+
});
77+
78+
function setup({
79+
workflowPages,
80+
}: {
81+
workflowPages: Array<ListWorkflowsResponse>;
82+
}) {
83+
let workflowRequestCount = 0;
84+
85+
const utils = render(
86+
<ScheduleDetailMetricsChart
87+
params={{
88+
domain: MOCK_DOMAIN,
89+
cluster: MOCK_CLUSTER,
90+
scheduleId: MOCK_SCHEDULE_ID,
91+
scheduleTab: 'runs',
92+
}}
93+
/>,
94+
{
95+
endpointsMocks: [
96+
{
97+
path: `/api/domains/${MOCK_DOMAIN}/${MOCK_CLUSTER}/schedules/${MOCK_SCHEDULE_ID}`,
98+
httpMethod: 'GET',
99+
httpResolver: async () =>
100+
HttpResponse.json(getMockDescribeScheduleResponseForChart()),
101+
},
102+
{
103+
path: `/api/domains/${MOCK_DOMAIN}/${MOCK_CLUSTER}/workflows`,
104+
httpMethod: 'GET',
105+
mockOnce: false,
106+
httpResolver: async () => {
107+
const page =
108+
workflowPages[workflowRequestCount] ??
109+
workflowPages[workflowPages.length - 1];
110+
workflowRequestCount += 1;
111+
return HttpResponse.json(page);
112+
},
113+
},
114+
],
115+
}
116+
);
117+
118+
return {
119+
...utils,
120+
getWorkflowRequestCount: () => workflowRequestCount,
121+
};
122+
}

0 commit comments

Comments
 (0)