From c56db611e481deb0660666f175427e21f1ad4431 Mon Sep 17 00:00:00 2001 From: NkToGo Date: Wed, 5 Aug 2026 12:37:19 +0200 Subject: [PATCH 1/6] feat(frontend): add dashboard API and routing foundation --- frontend/package-lock.json | 60 +++- frontend/package.json | 3 +- frontend/src/App.test.tsx | 30 +- frontend/src/App.tsx | 87 +++--- frontend/src/api/client.test.ts | 284 +++++++++++++++++++ frontend/src/api/client.ts | 109 +++++++ frontend/src/api/contracts.ts | 214 ++++++++++++++ frontend/src/hooks/useAsyncResource.test.tsx | 88 ++++++ frontend/src/hooks/useAsyncResource.ts | 61 ++++ frontend/src/main.tsx | 5 +- frontend/src/test/fixtures.ts | 41 +++ frontend/src/test/renderWithRouter.tsx | 7 + frontend/src/test/setup.ts | 8 + frontend/vite.config.ts | 8 + 14 files changed, 957 insertions(+), 48 deletions(-) create mode 100644 frontend/src/api/client.test.ts create mode 100644 frontend/src/api/client.ts create mode 100644 frontend/src/api/contracts.ts create mode 100644 frontend/src/hooks/useAsyncResource.test.tsx create mode 100644 frontend/src/hooks/useAsyncResource.ts create mode 100644 frontend/src/test/fixtures.ts create mode 100644 frontend/src/test/renderWithRouter.tsx diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 9ab6492..bfe5257 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -9,7 +9,8 @@ "version": "0.0.1", "dependencies": { "react": "^19.2.7", - "react-dom": "^19.2.7" + "react-dom": "^19.2.7", + "react-router-dom": "^7.18.2" }, "devDependencies": { "@eslint/js": "^10.0.1", @@ -1856,6 +1857,19 @@ "dev": true, "license": "MIT" }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -3221,6 +3235,44 @@ "dev": true, "license": "MIT" }, + "node_modules/react-router": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.2.tgz", + "integrity": "sha512-aUVMjFm3GAPTTZL7oYr5E7ETiqfQCHRLH+B+5afnICvf0r7kkK4eR6SMuwbSTJw/7t+12khT/Kahij49fqOCIg==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.18.2.tgz", + "integrity": "sha512-AIKJ/jgGlFb3EbfCXk5Gzshiwt+l3mqbCrNjmEWMMjqQxNJ3svBa6bgzFyCC2Sw3RA0VWF1kg3uQf2OFhxb8hw==", + "license": "MIT", + "dependencies": { + "react-router": "7.18.2" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, "node_modules/redent": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", @@ -3308,6 +3360,12 @@ "semver": "bin/semver.js" } }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index 5e9000e..e3a4cad 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -19,7 +19,8 @@ }, "dependencies": { "react": "^19.2.7", - "react-dom": "^19.2.7" + "react-dom": "^19.2.7", + "react-router-dom": "^7.18.2" }, "devDependencies": { "@eslint/js": "^10.0.1", diff --git a/frontend/src/App.test.tsx b/frontend/src/App.test.tsx index bd21231..d7f8392 100644 --- a/frontend/src/App.test.tsx +++ b/frontend/src/App.test.tsx @@ -1,16 +1,34 @@ -import { render, screen } from '@testing-library/react'; +import { fireEvent, screen } from '@testing-library/react'; import { describe, expect, it } from 'vitest'; import App from './App'; +import { validationRunFixture } from './test/fixtures'; +import { renderWithRouter } from './test/renderWithRouter'; -describe('App', () => { - it('renders the foundation shell without claiming later features are available', () => { - render(); +describe('App routing', () => { + it('renders the dashboard route', () => { + renderWithRouter(); expect( - screen.getByRole('heading', { level: 1, name: 'Data Quality Platform' }), + screen.getByRole('heading', { level: 2, name: 'Dashboard foundation' }), ).toBeInTheDocument(); + }); + + it('renders an addressable Validation Run route', () => { + renderWithRouter(, `/runs/${validationRunFixture.id}`); + + expect(screen.getByRole('heading', { level: 2, name: 'Validation Run' })).toBeInTheDocument(); + expect(screen.getByText(validationRunFixture.id)).toBeInTheDocument(); + }); + + it('renders an unmatched route and links back to the dashboard', () => { + renderWithRouter(, '/does-not-exist'); + + expect(screen.getByRole('heading', { level: 2, name: 'Page not found' })).toBeInTheDocument(); + + fireEvent.click(screen.getByRole('link', { name: 'Return to dashboard' })); + expect( - screen.getByText('These workflows are not available in the frontend yet.'), + screen.getByRole('heading', { level: 2, name: 'Dashboard foundation' }), ).toBeInTheDocument(); }); }); diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index d2d5151..178938d 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,8 +1,41 @@ -const plannedCapabilities = [ - 'Dataset management and CSV ingestion', - 'Configurable validation rules and issue tracking', - 'Run summaries, issue filtering, and report export', -]; +import { Link, Route, Routes, useParams } from 'react-router-dom'; + +function DashboardRoute() { + return ( +
+

Dashboard foundation

+

Read-only Dataset and Validation Run views will be added in the next Milestone 5 slice.

+
+ ); +} + +function ValidationRunRoute() { + const { runId } = useParams(); + + return ( +
+

Validation Run

+

+ The addressable Run detail route is ready. Run data will be added in a later Milestone 5 + slice. +

+

+ Run ID: {runId} +

+ Back to dashboard +
+ ); +} + +function NotFoundRoute() { + return ( +
+

Page not found

+

The requested frontend route does not exist.

+ Return to dashboard +
+ ); +} function App() { return ( @@ -13,44 +46,20 @@ function App() {
-

Project status

-

Data Quality Platform

-

- A foundation for building transparent, testable data-quality workflows in later - milestones. -

+

Milestone 5

+

+ Data Quality Platform +

+

Read-only dashboard routing and API connectivity foundation.

-
-

-

-

Project foundation

-

- The React application shell, Spring Boot service, local PostgreSQL configuration, and - automated checks are in place. PostgreSQL-backed Dataset, Validation Profile, Validation - Rule, SourceFile upload, and Validation Run creation, list, and detail APIs are - available in the backend, together with Validation Issue retrieval. Creating a - Validation Run synchronously parses and validates its private stored CSV bytes. Normal - processing persists Issues and summary counters as COMPLETED, while expected or - recovered processing failures record safe FAILED outcomes. Unexpected server failures - can retain a durable PENDING Run. The frontend shell does not call these APIs or provide - Validation Run screens yet. -

-
- -
-

Planned frontend capabilities

-
    - {plannedCapabilities.map((capability) => ( -
  • {capability}
  • - ))} -
-

These workflows are not available in the frontend yet.

-
+ + } /> + } /> + } /> +