Skip to content

Commit fb8c031

Browse files
feat(schedule-details): PR01a — App Router route shell + tab content component
Add the Next.js App Router pages for the schedule detail route and the ScheduleDetailPageTabContent component in its own directory: Route group (Schedule): - layout.tsx → ScheduleDetailPage (minimal shell, renders children only for now) - page.tsx → redirect bare [scheduleId] to default tab (overview) with replace - [scheduleTab]/page.tsx → ScheduleDetailPageTabContent - [scheduleTab]/loading.tsx — SectionLoadingIndicator - [scheduleTab]/error.tsx — inline error panel (tabs-error component ships in PR01c) Views (each component in its own sibling directory under domain-schedules/): - schedule-detail-page/: tabs config (4 tabs: overview/backfills/input/runs), page types, and minimal layout shell (header + tabs wired in subsequent PRs) - schedule-detail-page-tab-content/: notFound() for unknown slugs, placeholder per valid tab, styles, types, tests Browser tests: all 4 valid tabs render, unknown tab → notFound() Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 477f81b commit fb8c031

12 files changed

Lines changed: 230 additions & 0 deletions

File tree

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
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+
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+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import SectionLoadingIndicator from '@/components/section-loading-indicator/section-loading-indicator';
2+
3+
export default SectionLoadingIndicator;
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import ScheduleDetailPageTabContent from '@/views/domain-schedules/schedule-detail-page-tab-content/schedule-detail-page-tab-content';
2+
3+
export default ScheduleDetailPageTabContent;
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import ScheduleDetailPage from '@/views/domain-schedules/schedule-detail-page/schedule-detail-page';
2+
3+
export default ScheduleDetailPage;
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { redirect } from 'next/navigation';
2+
3+
import { DEFAULT_SCHEDULE_DETAIL_TAB } from '@/views/domain-schedules/schedule-detail-page/schedule-detail-tabs.config';
4+
5+
type Props = {
6+
params: {
7+
domain: string;
8+
cluster: string;
9+
scheduleId: string;
10+
};
11+
};
12+
13+
export default function ScheduleDetailRedirectPage({ params }: Props) {
14+
redirect(
15+
`/domains/${encodeURIComponent(params.domain)}/${encodeURIComponent(params.cluster)}/schedules/${encodeURIComponent(params.scheduleId)}/${DEFAULT_SCHEDULE_DETAIL_TAB}`
16+
);
17+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import React from 'react';
2+
3+
import { render, screen } from '@/test-utils/rtl';
4+
5+
import { type ScheduleDetailPageParams } from '../../schedule-detail-page/schedule-detail-page.types';
6+
import ScheduleDetailPageTabContent from '../schedule-detail-page-tab-content';
7+
8+
jest.mock('next/navigation', () => ({
9+
...jest.requireActual('next/navigation'),
10+
notFound: jest.fn(() => {
11+
throw new Error('NEXT_NOT_FOUND');
12+
}),
13+
}));
14+
15+
describe(ScheduleDetailPageTabContent.name, () => {
16+
it('renders placeholder for overview tab', () => {
17+
setup({ scheduleTab: 'overview' });
18+
expect(screen.getByText(/Overview/i)).toBeInTheDocument();
19+
});
20+
21+
it('renders placeholder for backfills tab', () => {
22+
setup({ scheduleTab: 'backfills' });
23+
expect(screen.getByText(/Backfills/i)).toBeInTheDocument();
24+
});
25+
26+
it('renders placeholder for input tab', () => {
27+
setup({ scheduleTab: 'input' });
28+
expect(screen.getByText(/Input/i)).toBeInTheDocument();
29+
});
30+
31+
it('renders placeholder for runs tab', () => {
32+
setup({ scheduleTab: 'runs' });
33+
expect(screen.getByText(/Runs/i)).toBeInTheDocument();
34+
});
35+
36+
it('calls notFound for unknown tab slug', () => {
37+
const { notFound } = jest.requireMock('next/navigation');
38+
expect(() =>
39+
setup({
40+
scheduleTab: 'unknown-tab' as ScheduleDetailPageParams['scheduleTab'],
41+
})
42+
).toThrow('NEXT_NOT_FOUND');
43+
expect(notFound).toHaveBeenCalled();
44+
});
45+
});
46+
47+
function setup({
48+
scheduleTab = 'overview',
49+
}: {
50+
scheduleTab?: ScheduleDetailPageParams['scheduleTab'];
51+
} = {}) {
52+
render(
53+
<ScheduleDetailPageTabContent
54+
params={{
55+
domain: 'test-domain',
56+
cluster: 'test-cluster',
57+
scheduleId: 'my-schedule',
58+
scheduleTab,
59+
}}
60+
/>
61+
);
62+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import type {
2+
StyletronCSSObject,
3+
StyletronCSSObjectOf,
4+
} from '@/hooks/use-styletron-classes';
5+
6+
const cssStylesObj = {
7+
tabContentContainer: (theme) => ({
8+
display: 'flex',
9+
flexDirection: 'column',
10+
marginTop: theme.sizing.scale900,
11+
marginBottom: theme.sizing.scale900,
12+
flex: 1,
13+
}),
14+
} satisfies StyletronCSSObject;
15+
16+
export const cssStyles: StyletronCSSObjectOf<typeof cssStylesObj> =
17+
cssStylesObj;
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
'use client';
2+
import React from 'react';
3+
4+
import { notFound } from 'next/navigation';
5+
6+
import useStyletronClasses from '@/hooks/use-styletron-classes';
7+
import decodeUrlParams from '@/utils/decode-url-params';
8+
9+
import scheduleDetailTabsConfig from '../schedule-detail-page/schedule-detail-tabs.config';
10+
import { type ScheduleDetailPageParams } from '../schedule-detail-page/schedule-detail-page.types';
11+
12+
import { cssStyles } from './schedule-detail-page-tab-content.styles';
13+
import { type Props } from './schedule-detail-page-tab-content.types';
14+
15+
export default function ScheduleDetailPageTabContent({ params }: Props) {
16+
const { cls } = useStyletronClasses(cssStyles);
17+
const decodedParams = decodeUrlParams(params) as ScheduleDetailPageParams;
18+
const tabConfig = scheduleDetailTabsConfig[decodedParams.scheduleTab];
19+
20+
if (!tabConfig) {
21+
return notFound();
22+
}
23+
24+
return (
25+
<div className={cls.tabContentContainer}>
26+
<div>{tabConfig.title} — coming soon</div>
27+
</div>
28+
);
29+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { type ScheduleDetailPageParams } from '@/views/domain-schedules/schedule-detail-page/schedule-detail-page.types';
2+
3+
export type Props = {
4+
params: ScheduleDetailPageParams;
5+
};
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { type Props } from './schedule-detail-page.types';
2+
3+
export default function ScheduleDetailPage({ children }: Props) {
4+
return <>{children}</>;
5+
}

0 commit comments

Comments
 (0)