Skip to content

Commit 7462604

Browse files
feat(schedule-details): PR01c — ScheduleDetailPageTabs + TabsError components
Add tabs navigation and error boundary components, each in their own directory, and wire both into ScheduleDetailPage: - schedule-detail-page-tabs/: ScheduleDetailPageTabs client component — reads scheduleTab from URL params, renders PageTabs with overview/backfills/input/runs, router.push(encodeURIComponent(tab)) on selection; types file - schedule-detail-page-tabs-error/: ScheduleDetailPageTabsError — ErrorPanel + PanelSection wrapper with "Failed to load schedule content" copy; types file - schedule-detail-page.tsx: now renders <ScheduleDetailPageTabs /> between header and {children} - [scheduleTab]/error.tsx: imports from schedule-detail-page-tabs-error/ (proper component directory, replaces inline definition from PR01a) Tests: tab labels rendered, router.push called on click, error message + Retry button visible, reset callback invoked Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 51df50e commit 7462604

8 files changed

Lines changed: 191 additions & 21 deletions

File tree

Lines changed: 2 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,4 @@
11
'use client';
2-
import React from 'react';
2+
import ScheduleDetailPageTabsError from '@/views/domain-schedules/schedule-detail-page-tabs-error/schedule-detail-page-tabs-error';
33

4-
import ErrorPanel from '@/components/error-panel/error-panel';
5-
import PanelSection from '@/components/panel-section/panel-section';
6-
7-
type Props = {
8-
error: Error;
9-
reset: () => void;
10-
};
11-
12-
export default function ScheduleDetailPageError({ error, reset }: Props) {
13-
return (
14-
<PanelSection>
15-
<ErrorPanel
16-
error={error}
17-
message="Failed to load schedule content"
18-
actions={[{ kind: 'retry', label: 'Retry' }]}
19-
reset={reset}
20-
/>
21-
</PanelSection>
22-
);
23-
}
4+
export default ScheduleDetailPageTabsError;
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import React from 'react';
2+
3+
import { render, screen, userEvent } from '@/test-utils/rtl';
4+
5+
jest.mock('next/navigation', () => ({
6+
...jest.requireActual('next/navigation'),
7+
useRouter: () => ({
8+
push: jest.fn(),
9+
back: jest.fn(),
10+
replace: jest.fn(),
11+
forward: jest.fn(),
12+
prefetch: jest.fn(),
13+
refresh: jest.fn(),
14+
}),
15+
}));
16+
17+
import ScheduleDetailPageTabsError from '../schedule-detail-page-tabs-error';
18+
19+
describe(ScheduleDetailPageTabsError.name, () => {
20+
it('renders the error message', () => {
21+
setup();
22+
23+
expect(
24+
screen.getByText('Failed to load schedule content')
25+
).toBeInTheDocument();
26+
});
27+
28+
it('renders a Retry button', () => {
29+
setup();
30+
31+
expect(screen.getByRole('button', { name: 'Retry' })).toBeInTheDocument();
32+
});
33+
34+
it('calls reset when Retry is clicked', async () => {
35+
const reset = jest.fn();
36+
const { user } = setup({ reset });
37+
38+
await user.click(screen.getByRole('button', { name: 'Retry' }));
39+
40+
expect(reset).toHaveBeenCalledTimes(1);
41+
});
42+
});
43+
44+
function setup({
45+
error = new Error('Something went wrong'),
46+
reset = jest.fn(),
47+
}: {
48+
error?: Error;
49+
reset?: jest.Mock;
50+
} = {}) {
51+
const user = userEvent.setup();
52+
render(<ScheduleDetailPageTabsError error={error} reset={reset} />);
53+
return { user };
54+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
'use client';
2+
import React from 'react';
3+
4+
import ErrorPanel from '@/components/error-panel/error-panel';
5+
import PanelSection from '@/components/panel-section/panel-section';
6+
7+
import { type Props } from './schedule-detail-page-tabs-error.types';
8+
9+
export default function ScheduleDetailPageTabsError({ error, reset }: Props) {
10+
return (
11+
<PanelSection>
12+
<ErrorPanel
13+
error={error}
14+
message="Failed to load schedule content"
15+
actions={[{ kind: 'retry', label: 'Retry' }]}
16+
reset={reset}
17+
/>
18+
</PanelSection>
19+
);
20+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export type Props = {
2+
error: Error;
3+
reset: () => void;
4+
};
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import React from 'react';
2+
3+
import { act, fireEvent, render, screen } from '@/test-utils/rtl';
4+
5+
import ScheduleDetailPageTabs from '../schedule-detail-page-tabs';
6+
7+
const mockPushFn = jest.fn();
8+
9+
jest.mock('next/navigation', () => ({
10+
...jest.requireActual('next/navigation'),
11+
useRouter: () => ({
12+
push: mockPushFn,
13+
back: jest.fn(),
14+
replace: jest.fn(),
15+
forward: jest.fn(),
16+
prefetch: jest.fn(),
17+
refresh: jest.fn(),
18+
}),
19+
useParams: () => ({
20+
domain: 'test-domain',
21+
cluster: 'test-cluster',
22+
scheduleId: 'my-schedule',
23+
scheduleTab: 'overview',
24+
}),
25+
}));
26+
27+
describe(ScheduleDetailPageTabs.name, () => {
28+
afterEach(() => {
29+
jest.clearAllMocks();
30+
});
31+
32+
it('renders all four tab titles', () => {
33+
setup();
34+
35+
expect(screen.getByText('Overview')).toBeInTheDocument();
36+
expect(screen.getByText('Backfills')).toBeInTheDocument();
37+
expect(screen.getByText('Input')).toBeInTheDocument();
38+
expect(screen.getByText('Runs')).toBeInTheDocument();
39+
});
40+
41+
it('navigates with router.push when a tab is clicked', () => {
42+
setup();
43+
44+
act(() => {
45+
fireEvent.click(screen.getByText('Backfills'));
46+
});
47+
48+
expect(mockPushFn).toHaveBeenCalledWith('backfills');
49+
});
50+
51+
it('navigates to runs tab when clicked', () => {
52+
setup();
53+
54+
act(() => {
55+
fireEvent.click(screen.getByText('Runs'));
56+
});
57+
58+
expect(mockPushFn).toHaveBeenCalledWith('runs');
59+
});
60+
});
61+
62+
function setup() {
63+
render(<ScheduleDetailPageTabs />);
64+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
'use client';
2+
import React, { useMemo } from 'react';
3+
4+
import { useParams, useRouter } from 'next/navigation';
5+
6+
import PageTabs from '@/components/page-tabs/page-tabs';
7+
import decodeUrlParams from '@/utils/decode-url-params';
8+
9+
import scheduleDetailTabsConfig from '../schedule-detail-page/schedule-detail-tabs.config';
10+
11+
import { type ScheduleDetailPageTabsParams } from './schedule-detail-page-tabs.types';
12+
13+
export default function ScheduleDetailPageTabs() {
14+
const router = useRouter();
15+
const params = useParams<ScheduleDetailPageTabsParams>();
16+
const decodedParams = decodeUrlParams(params) as ScheduleDetailPageTabsParams;
17+
18+
const tabList = useMemo(
19+
() =>
20+
Object.entries(scheduleDetailTabsConfig).map(([key, tabConfig]) => ({
21+
key,
22+
title: tabConfig.title,
23+
artwork: tabConfig.artwork,
24+
})),
25+
[]
26+
);
27+
28+
return (
29+
<PageTabs
30+
selectedTab={decodedParams.scheduleTab}
31+
tabList={tabList}
32+
setSelectedTab={(newTab) => {
33+
router.push(encodeURIComponent(newTab.toString()));
34+
}}
35+
/>
36+
);
37+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { type ScheduleDetailTabName } from '../schedule-detail-page/schedule-detail-tabs.config';
2+
3+
export type ScheduleDetailPageTabsParams = {
4+
domain: string;
5+
cluster: string;
6+
scheduleId: string;
7+
scheduleTab: ScheduleDetailTabName;
8+
};

src/views/domain-schedules/schedule-detail-page/schedule-detail-page.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import React from 'react';
33
import decodeUrlParams from '@/utils/decode-url-params';
44

55
import ScheduleDetailPageHeader from '../schedule-detail-page-header/schedule-detail-page-header';
6+
import ScheduleDetailPageTabs from '../schedule-detail-page-tabs/schedule-detail-page-tabs';
67

78
import { type Props } from './schedule-detail-page.types';
89

@@ -16,6 +17,7 @@ export default function ScheduleDetailPage({ params, children }: Props) {
1617
cluster={decodedParams.cluster}
1718
scheduleId={decodedParams.scheduleId}
1819
/>
20+
<ScheduleDetailPageTabs />
1921
{children}
2022
</>
2123
);

0 commit comments

Comments
 (0)