Skip to content

Commit 2c19cc8

Browse files
feat(schedule-details): add static metrics chart series (SLICE-9.1 / PR09d)
Render visx markers for successful runs, missed executions, and the next execution time from static fixture data, replacing the empty state when series data is present. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 4215d76 commit 2c19cc8

9 files changed

Lines changed: 299 additions & 16 deletions
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { type ScheduleMetricsChartSeriesData } from '../schedule-detail-metrics-chart-series.types';
2+
3+
export const SCHEDULE_METRICS_CHART_FIXTURE_NOW_MS = new Date(
4+
'2024-06-15T12:00:00.000Z'
5+
).getTime();
6+
7+
export const scheduleMetricsChartFixture: ScheduleMetricsChartSeriesData = {
8+
successfulRuns: [
9+
{ scheduledTimeMs: SCHEDULE_METRICS_CHART_FIXTURE_NOW_MS - 6 * 60 * 60 * 1000 },
10+
{ scheduledTimeMs: SCHEDULE_METRICS_CHART_FIXTURE_NOW_MS - 4 * 60 * 60 * 1000 },
11+
{ scheduledTimeMs: SCHEDULE_METRICS_CHART_FIXTURE_NOW_MS - 1 * 60 * 60 * 1000 },
12+
],
13+
missedExecutions: [
14+
{ scheduledTimeMs: SCHEDULE_METRICS_CHART_FIXTURE_NOW_MS - 2 * 60 * 60 * 1000 },
15+
],
16+
nextExecutionTimeMs:
17+
SCHEDULE_METRICS_CHART_FIXTURE_NOW_MS + 2 * 60 * 60 * 1000,
18+
};
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import React from 'react';
2+
3+
import { render, screen } from '@/test-utils/rtl';
4+
5+
import {
6+
CHART_EMPTY_STATE_MESSAGE,
7+
CHART_REGION_ARIA_LABEL,
8+
} from '../schedule-detail-metrics-chart.constants';
9+
import ScheduleDetailMetricsChart from '../schedule-detail-metrics-chart';
10+
11+
jest.mock('@visx/responsive', () => ({
12+
ParentSize: ({
13+
children,
14+
}: {
15+
children: (args: { width: number; height: number }) => React.ReactNode;
16+
}) => <>{children({ width: 800, height: 280 })}</>,
17+
}));
18+
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+
30+
describe(`${ScheduleDetailMetricsChart.name} empty state`, () => {
31+
it('renders empty state when there is no chart data', () => {
32+
render(
33+
<ScheduleDetailMetricsChart
34+
params={{
35+
domain: 'test-domain',
36+
cluster: 'test-cluster',
37+
scheduleId: 'my-schedule',
38+
scheduleTab: 'runs',
39+
}}
40+
/>
41+
);
42+
43+
expect(
44+
screen.getByRole('region', { name: CHART_REGION_ARIA_LABEL })
45+
).toBeInTheDocument();
46+
expect(screen.getByRole('status')).toHaveTextContent(
47+
CHART_EMPTY_STATE_MESSAGE
48+
);
49+
expect(
50+
screen.queryByTestId('schedule-metrics-chart-canvas')
51+
).not.toBeInTheDocument();
52+
});
53+
});

src/views/schedule-page/schedule-detail-metrics-chart/__tests__/schedule-detail-metrics-chart.test.tsx

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,11 @@ import { render, screen, within } from '@/test-utils/rtl';
55
import {
66
CHART_EMPTY_STATE_MESSAGE,
77
CHART_REGION_ARIA_LABEL,
8+
CHART_SERIES_TEST_IDS,
89
CHART_TOOLBAR_ARIA_LABEL,
910
CHART_TOOLBAR_BUTTON_LABELS,
1011
} from '../schedule-detail-metrics-chart.constants';
12+
import { SCHEDULE_METRICS_CHART_FIXTURE_NOW_MS } from '../__fixtures__/schedule-detail-metrics-chart-fixture';
1113
import ScheduleDetailMetricsChart from '../schedule-detail-metrics-chart';
1214

1315
jest.mock('@visx/responsive', () => ({
@@ -19,18 +21,36 @@ jest.mock('@visx/responsive', () => ({
1921
}));
2022

2123
describe(ScheduleDetailMetricsChart.name, () => {
22-
it('renders chart region with empty state when there is no chart data', () => {
24+
beforeEach(() => {
25+
jest.useFakeTimers({ now: SCHEDULE_METRICS_CHART_FIXTURE_NOW_MS });
26+
});
27+
28+
afterEach(() => {
29+
jest.useRealTimers();
30+
});
31+
32+
it('renders chart canvas with static fixture series markers', () => {
2333
setup();
2434

2535
expect(
2636
screen.getByRole('region', { name: CHART_REGION_ARIA_LABEL })
2737
).toBeInTheDocument();
28-
expect(screen.getByRole('status')).toHaveTextContent(
29-
CHART_EMPTY_STATE_MESSAGE
30-
);
3138
expect(
32-
screen.queryByTestId('schedule-metrics-chart-canvas')
33-
).not.toBeInTheDocument();
39+
screen.getByTestId('schedule-metrics-chart-canvas')
40+
).toBeInTheDocument();
41+
expect(
42+
screen.getByTestId(CHART_SERIES_TEST_IDS.svg)
43+
).toBeInTheDocument();
44+
expect(
45+
screen.getAllByTestId(CHART_SERIES_TEST_IDS.successfulRunMarker)
46+
).toHaveLength(3);
47+
expect(
48+
screen.getByTestId(CHART_SERIES_TEST_IDS.missedExecutionMarker)
49+
).toBeInTheDocument();
50+
expect(
51+
screen.getByTestId(CHART_SERIES_TEST_IDS.nextExecutionMarker)
52+
).toBeInTheDocument();
53+
expect(screen.queryByRole('status')).not.toBeInTheDocument();
3454
});
3555

3656
it('renders disabled chart toolbar controls', () => {
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { type ScheduleMetricsChartSeriesData } from '../schedule-detail-metrics-chart-series.types';
2+
3+
export default function hasScheduleMetricsChartData(
4+
data: ScheduleMetricsChartSeriesData
5+
) {
6+
return (
7+
data.successfulRuns.length > 0 ||
8+
data.missedExecutions.length > 0 ||
9+
data.nextExecutionTimeMs != null
10+
);
11+
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import React from 'react';
2+
3+
import { Group } from '@visx/group';
4+
import { Circle, Line } from '@visx/shape';
5+
6+
import {
7+
CHART_SERIES_MARKER_RADIUS_PX,
8+
CHART_SERIES_MISSED_MARKER_RADIUS_PX,
9+
CHART_SERIES_MISSED_STROKE_WIDTH_PX,
10+
CHART_SERIES_MISSED_Y_RATIO,
11+
CHART_SERIES_NEXT_EXECUTION_STROKE_WIDTH_PX,
12+
CHART_SERIES_SUCCESS_Y_RATIO,
13+
CHART_SERIES_TEST_IDS,
14+
} from './schedule-detail-metrics-chart.constants';
15+
import { type ScheduleMetricsChartSeriesProps } from './schedule-detail-metrics-chart-series.types';
16+
17+
export default function ScheduleDetailMetricsChartSeries({
18+
width,
19+
height,
20+
xScale,
21+
data,
22+
successfulRunColor,
23+
missedExecutionColor,
24+
nextExecutionColor,
25+
}: ScheduleMetricsChartSeriesProps) {
26+
const successfulRunY = height * CHART_SERIES_SUCCESS_Y_RATIO;
27+
const missedExecutionY = height * CHART_SERIES_MISSED_Y_RATIO;
28+
29+
return (
30+
<Group data-testid={CHART_SERIES_TEST_IDS.svg}>
31+
{data.successfulRuns.map(({ scheduledTimeMs }) => (
32+
<Circle
33+
key={`successful-${scheduledTimeMs}`}
34+
cx={xScale(scheduledTimeMs)}
35+
cy={successfulRunY}
36+
r={CHART_SERIES_MARKER_RADIUS_PX}
37+
fill={successfulRunColor}
38+
data-testid={CHART_SERIES_TEST_IDS.successfulRunMarker}
39+
/>
40+
))}
41+
{data.missedExecutions.map(({ scheduledTimeMs }) => {
42+
const x = xScale(scheduledTimeMs);
43+
44+
return (
45+
<Group key={`missed-${scheduledTimeMs}`}>
46+
<Line
47+
from={{ x, y: successfulRunY }}
48+
to={{ x, y: missedExecutionY }}
49+
stroke={missedExecutionColor}
50+
strokeWidth={CHART_SERIES_MISSED_STROKE_WIDTH_PX}
51+
strokeDasharray="4 3"
52+
pointerEvents="none"
53+
/>
54+
<Circle
55+
cx={x}
56+
cy={missedExecutionY}
57+
r={CHART_SERIES_MISSED_MARKER_RADIUS_PX}
58+
fill="transparent"
59+
stroke={missedExecutionColor}
60+
strokeWidth={CHART_SERIES_MISSED_STROKE_WIDTH_PX}
61+
data-testid={CHART_SERIES_TEST_IDS.missedExecutionMarker}
62+
/>
63+
</Group>
64+
);
65+
})}
66+
{data.nextExecutionTimeMs != null && (
67+
<Line
68+
from={{ x: xScale(data.nextExecutionTimeMs), y: 0 }}
69+
to={{ x: xScale(data.nextExecutionTimeMs), y: height }}
70+
stroke={nextExecutionColor}
71+
strokeWidth={CHART_SERIES_NEXT_EXECUTION_STROKE_WIDTH_PX}
72+
strokeDasharray="6 4"
73+
pointerEvents="none"
74+
data-testid={CHART_SERIES_TEST_IDS.nextExecutionMarker}
75+
/>
76+
)}
77+
<rect width={width} height={height} fill="transparent" pointerEvents="none" />
78+
</Group>
79+
);
80+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { type ScaleLinear } from 'd3-scale';
2+
3+
export type ScheduleMetricsChartExecutionPoint = {
4+
scheduledTimeMs: number;
5+
};
6+
7+
export type ScheduleMetricsChartSeriesData = {
8+
successfulRuns: ScheduleMetricsChartExecutionPoint[];
9+
missedExecutions: ScheduleMetricsChartExecutionPoint[];
10+
nextExecutionTimeMs: number | null;
11+
};
12+
13+
export type ScheduleMetricsChartXScale = ScaleLinear<number, number, never>;
14+
15+
export type ScheduleMetricsChartSeriesProps = {
16+
width: number;
17+
height: number;
18+
xScale: ScheduleMetricsChartXScale;
19+
data: ScheduleMetricsChartSeriesData;
20+
successfulRunColor: string;
21+
missedExecutionColor: string;
22+
nextExecutionColor: string;
23+
};

src/views/schedule-page/schedule-detail-metrics-chart/schedule-detail-metrics-chart.constants.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,17 @@ export const CHART_FUTURE_GUTTER_MS = 30 * 60_000;
2424

2525
/** Horizontal inset applied to the chart drawable area (px). */
2626
export const CHART_SIDE_PADDING_PX = 24;
27+
28+
export const CHART_SERIES_TEST_IDS = {
29+
svg: 'schedule-metrics-chart-series-svg',
30+
successfulRunMarker: 'schedule-metrics-chart-successful-run-marker',
31+
missedExecutionMarker: 'schedule-metrics-chart-missed-execution-marker',
32+
nextExecutionMarker: 'schedule-metrics-chart-next-execution-marker',
33+
} as const;
34+
35+
export const CHART_SERIES_MARKER_RADIUS_PX = 5;
36+
export const CHART_SERIES_MISSED_MARKER_RADIUS_PX = 6;
37+
export const CHART_SERIES_NEXT_EXECUTION_STROKE_WIDTH_PX = 2;
38+
export const CHART_SERIES_MISSED_STROKE_WIDTH_PX = 2;
39+
export const CHART_SERIES_SUCCESS_Y_RATIO = 0.45;
40+
export const CHART_SERIES_MISSED_Y_RATIO = 0.65;

src/views/schedule-page/schedule-detail-metrics-chart/schedule-detail-metrics-chart.styles.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,11 @@ export const styled = {
3232
width: '100%',
3333
height: '100%',
3434
})),
35+
ChartSvg: createStyled('svg', () => ({
36+
display: 'block',
37+
width: '100%',
38+
height: '100%',
39+
})),
3540
EmptyState: createStyled('div', ({ $theme }: { $theme: Theme }) => ({
3641
display: 'flex',
3742
alignItems: 'center',

src/views/schedule-page/schedule-detail-metrics-chart/schedule-detail-metrics-chart.tsx

Lines changed: 69 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,18 +10,29 @@ import {
1010
} from 'react-icons/md';
1111

1212
import Button from '@/components/button/button';
13+
import useStyletronClasses from '@/hooks/use-styletron-classes';
1314

15+
import { scheduleMetricsChartFixture } from './__fixtures__/schedule-detail-metrics-chart-fixture';
16+
import hasScheduleMetricsChartData from './helpers/has-schedule-metrics-chart-data';
1417
import {
1518
CHART_EMPTY_STATE_MESSAGE,
1619
CHART_REGION_ARIA_LABEL,
1720
CHART_TOOLBAR_ARIA_LABEL,
1821
CHART_TOOLBAR_BUTTON_LABELS,
1922
} from './schedule-detail-metrics-chart.constants';
23+
import {
24+
createMetricsChartXScale,
25+
resolveMetricsChartPixelRange,
26+
resolveMetricsChartTimeDomain,
27+
} from './schedule-detail-metrics-chart-scales';
28+
import ScheduleDetailMetricsChartSeries from './schedule-detail-metrics-chart-series';
2029
import { overrides, styled } from './schedule-detail-metrics-chart.styles';
2130
import { type Props } from './schedule-detail-metrics-chart.types';
2231

2332
export default function ScheduleDetailMetricsChart(_props: Props) {
24-
const hasChartData = false;
33+
const { theme } = useStyletronClasses({});
34+
const chartData = scheduleMetricsChartFixture;
35+
const hasChartData = hasScheduleMetricsChartData(chartData);
2536

2637
return (
2738
<styled.Container>
@@ -73,15 +84,63 @@ export default function ScheduleDetailMetricsChart(_props: Props) {
7384
</styled.Toolbar>
7485
<styled.ChartRegion role="region" aria-label={CHART_REGION_ARIA_LABEL}>
7586
<ParentSize>
76-
{() =>
77-
hasChartData ? (
78-
<styled.ChartCanvas data-testid="schedule-metrics-chart-canvas" />
79-
) : (
80-
<styled.EmptyState role="status">
81-
{CHART_EMPTY_STATE_MESSAGE}
82-
</styled.EmptyState>
83-
)
84-
}
87+
{({ width = 0, height = 0 }) => {
88+
const chartWidth = Math.max(width, 0);
89+
const chartHeight = Math.max(height, 0);
90+
91+
if (!hasChartData || chartWidth === 0 || chartHeight === 0) {
92+
return (
93+
<styled.EmptyState role="status">
94+
{CHART_EMPTY_STATE_MESSAGE}
95+
</styled.EmptyState>
96+
);
97+
}
98+
99+
const timestampsMs = [
100+
...chartData.successfulRuns.map(
101+
({ scheduledTimeMs }) => scheduledTimeMs
102+
),
103+
...chartData.missedExecutions.map(
104+
({ scheduledTimeMs }) => scheduledTimeMs
105+
),
106+
];
107+
const domain = resolveMetricsChartTimeDomain({
108+
timestampsMs,
109+
nowMs: Date.now(),
110+
nextExecutionMs: chartData.nextExecutionTimeMs,
111+
});
112+
const pixelRange = resolveMetricsChartPixelRange({
113+
widthPx: chartWidth,
114+
});
115+
const xScale =
116+
domain && pixelRange
117+
? createMetricsChartXScale({ domain, range: pixelRange })
118+
: null;
119+
120+
if (!xScale) {
121+
return (
122+
<styled.EmptyState role="status">
123+
{CHART_EMPTY_STATE_MESSAGE}
124+
</styled.EmptyState>
125+
);
126+
}
127+
128+
return (
129+
<styled.ChartCanvas data-testid="schedule-metrics-chart-canvas">
130+
<styled.ChartSvg width={chartWidth} height={chartHeight}>
131+
<ScheduleDetailMetricsChartSeries
132+
width={chartWidth}
133+
height={chartHeight}
134+
xScale={xScale}
135+
data={chartData}
136+
successfulRunColor={theme.colors.positive400}
137+
missedExecutionColor={theme.colors.warning400}
138+
nextExecutionColor={theme.colors.accent400}
139+
/>
140+
</styled.ChartSvg>
141+
</styled.ChartCanvas>
142+
);
143+
}}
85144
</ParentSize>
86145
</styled.ChartRegion>
87146
</styled.Container>

0 commit comments

Comments
 (0)