Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions .github/workflows/chromatic.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
name: Chromatic

# Publish the report's Storybook to Chromatic for visual review. Only runs when
# the report UI (or this workflow / the Storybook config) changes.
on:
push:
branches: [main]
paths:
- 'src/report/web/**'
- '.storybook/**'
- '.github/workflows/chromatic.yml'
pull_request:
paths:
- 'src/report/web/**'
- '.storybook/**'
- '.github/workflows/chromatic.yml'

concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

permissions:
contents: read

jobs:
chromatic:
name: Publish
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0 # Chromatic needs full git history to find baselines
persist-credentials: false

- name: Bootstrap
uses: ./.github/actions/bootstrap

- name: Build Storybook
run: bun run build-storybook

- name: Publish to Chromatic
run: |
bun run chromatic -- \
--project-token="${{ secrets.CHROMATIC_PROJECT_TOKEN }}" \
--storybook-build-dir=storybook-static \
--only-changed \
--exit-zero-on-changes \
--auto-accept-changes=main
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ dist
dist-release
*.tgz

# Storybook build output (published to Chromatic in CI)
storybook-static

# dev-only fixture for the report web app
src/report/web/data/fixture.json

Expand Down
1 change: 1 addition & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
dist/
node_modules/
coverage/
storybook-static/
claude-tmp/
bun.lock
.husky/_/
Expand Down
15 changes: 15 additions & 0 deletions .storybook/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import type { StorybookConfig } from '@storybook/react-vite';
import tailwindcss from '@tailwindcss/vite';
import { mergeConfig } from 'vite';

const config: StorybookConfig = {
stories: ['../src/report/web/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
addons: [],
framework: '@storybook/react-vite',
// The report's production build uses bun-plugin-tailwind, but Storybook runs
// on Vite, so the Tailwind v4 Vite plugin is wired in here rather than via a
// root vite.config.ts (which would also leak into the vitest browser config).
viteFinal: (viteConfig) => mergeConfig(viteConfig, { plugins: [tailwindcss()] }),
};

export default config;
8 changes: 8 additions & 0 deletions .storybook/preview.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/* The report's stylesheet owns `@import "tailwindcss"`, the design tokens, and
fonts; re-import it so Storybook shares the same Tailwind compilation. The
@source directive lists the report tree explicitly so Tailwind scans the
components and stories for class names regardless of the Vite root. */

@import '../src/report/web/styles.css';

@source '../src/report/web/**/*.{ts,tsx}';
21 changes: 21 additions & 0 deletions .storybook/preview.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import './preview.css';
import type { Preview } from '@storybook/react-vite';

const preview: Preview = {
parameters: {
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/i,
},
},
backgrounds: { disable: true },
options: {
storySort: {
method: 'alphabetical',
},
},
},
};

export default preview;
298 changes: 291 additions & 7 deletions bun.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ const processRestrictedProperties = [
export default defineConfig(
...baseConfig,
{
ignores: ['dist/**', 'node_modules/**', 'coverage/**', 'claude-tmp/**', 'bun.lock'],
ignores: ['dist/**', 'node_modules/**', 'coverage/**', 'storybook-static/**', 'claude-tmp/**', 'bun.lock'],
},
{
rules: {
Expand Down
7 changes: 7 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@
"prepack": "bun run build:report-web",
"build:report-web": "bun run scripts/build-report-web.ts",
"dev:report-web": "bun ./src/report/web/index.dev.html",
"storybook": "storybook dev -p 6006",
"build-storybook": "storybook build --stats-json",
"chromatic": "chromatic",
"build:darwin-arm64": "bun run build:report-web && bun build --compile --target=bun-darwin-arm64 ./src/index.ts --outfile dist/patchwave-analysis-darwin-arm64",
"build:darwin-x64": "bun run build:report-web && bun build --compile --target=bun-darwin-x64 ./src/index.ts --outfile dist/patchwave-analysis-darwin-x64",
"build:linux-x64": "bun run build:report-web && bun build --compile --target=bun-linux-x64 ./src/index.ts --outfile dist/patchwave-analysis-linux-x64"
Expand All @@ -43,6 +46,8 @@
"@contextbridge-ai/prettier-config": "^0.0.0",
"@fontsource/ibm-plex-mono": "^5.2.7",
"@fontsource/ibm-plex-sans": "^5.2.8",
"@storybook/react-vite": "^10.2.14",
"@tailwindcss/vite": "^4.3.0",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.2",
"@vitejs/plugin-react": "^5.1.1",
Expand All @@ -52,13 +57,15 @@
"@types/react-dom": "^19",
"@types/semver": "^7.7.1",
"bun-plugin-tailwind": "^0.1.2",
"chromatic": "^15.1.1",
"eslint": "^10.4.0",
"fishery": "^2.4.0",
"globals": "^17.6.0",
"husky": "^9.1.7",
"lint-staged": "^17.0.5",
"prettier": "^3.8.3",
"playwright": "^1.58.2",
"storybook": "^10.2.14",
"tailwindcss": "^4",
"typescript": "^6.0.3",
"typescript-eslint": "^8.59.4",
Expand Down
52 changes: 52 additions & 0 deletions src/report/web/App.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import type { Meta, StoryObj } from '@storybook/react-vite';
import { toEmbeddedShape } from '../embeddedShape.ts';
import { cveExposureOk, cveExposureScopeMissing, orgOverview, reportBundle } from '../testFactories.ts';
import { App } from './App.tsx';

// A representative report, built from the same fishery factories the tests use.
// Defaults cover most of the page; the overrides here only fill the spots that
// would otherwise render as single-row tables (language mix, top repos by
// severity) so the snapshot exercises the full UI.
const sampleReport = toEmbeddedShape(
reportBundle.build({
orgOverview: orgOverview.build({
topLanguages: [
{ language: 'TypeScript', bytes: 4_200_000, percentage: 58 },
{ language: 'Go', bytes: 1_600_000, percentage: 22 },
{ language: 'Python', bytes: 880_000, percentage: 12 },
{ language: 'Ruby', bytes: 560_000, percentage: 8 },
],
}),
cve: cveExposureOk.build({
topReposBySeverity: [
{ repo: 'acme/api', critical: 1, high: 2, medium: 1, low: 0 },
{ repo: 'acme/web', critical: 0, high: 1, medium: 3, low: 2 },
{ repo: 'acme/billing', critical: 0, high: 0, medium: 2, low: 5 },
],
reposWithSecurityAlertsDisabled: ['acme/legacy-cron'],
}),
}),
);

const meta = {
title: 'Report/App',
component: App,
parameters: {
layout: 'fullscreen',
},
} satisfies Meta<typeof App>;

export default meta;
type Story = StoryObj<typeof meta>;

export const Default: Story = {
args: {
data: sampleReport,
},
};

export const CveScopeMissing: Story = {
args: {
data: { ...sampleReport, cve: cveExposureScopeMissing.build() },
},
};
3 changes: 3 additions & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,5 +27,8 @@
"noUnusedParameters": false,
"noPropertyAccessFromIndexSignature": false
},
// `.storybook` is a dot-directory, so the implicit `**/*` include skips it
// (which would also hide it from typecheck and the eslint project service).
"include": ["**/*", ".storybook/**/*"],
"exclude": ["node_modules", "dist", "claude-tmp", "out"]
}
Loading