Skip to content
Open
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
14 changes: 13 additions & 1 deletion .github/workflows/build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ jobs:
VITE_APP_GIT_SHA: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
run: |
export VITE_APP_GIT_TIMESTAMP="$(git show -s --format=%cI)"
args=(--output ./dist --head-sha "$VITE_APP_GIT_SHA")
args=(--output ./dist --artifact-output "$RUNNER_TEMP/connect-gallery.html" --head-sha "$VITE_APP_GIT_SHA")
if [[ -n "$BASELINE_URL" ]]; then
args+=(--baseline-url "$BASELINE_URL")
fi
Expand All @@ -45,3 +45,15 @@ jobs:
path: ./dist
retention-days: 1
name: build-artifacts-${{ github.run_id }}

- name: Upload directly viewable gallery report
id: gallery-artifact
uses: actions/upload-artifact@v7
with:
path: ${{ runner.temp }}/connect-gallery.html
archive: false
if-no-files-found: error
retention-days: 14

- name: Add gallery link to build summary
run: echo "### [Open connect gallery](${{ steps.gallery-artifact.outputs.artifact-url }})" >> "$GITHUB_STEP_SUMMARY"
78 changes: 59 additions & 19 deletions scripts/build-gallery.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -156,12 +156,20 @@ async function downloadBaseline(baselineUrl, destination) {
item.state === state.name && item.viewport === viewport.name
));
if (!capture?.assets?.current) return false;
const response = await getResponse(new URL(capture.assets.current, root));
await writeFile(
resolve(destination, captureFilename(state.name, viewport.name)),
Buffer.from(await response.arrayBuffer()),
);
return true;
const captureUrl = new URL(capture.assets.current, root);
try {
const response = await getResponse(captureUrl);
const buffer = Buffer.from(await response.arrayBuffer());
readPng(buffer, `Baseline capture ${state.name}/${viewport.name} from ${captureUrl}`);
await writeFile(
resolve(destination, captureFilename(state.name, viewport.name)),
buffer,
);
return true;
} catch (error) {
console.warn(`Skipping unavailable baseline capture ${state.name}/${viewport.name}: ${error.message}`);
return false;
}
}));
const downloaded = (await Promise.all(downloads)).filter(Boolean).length;
if (downloaded === 0) throw new Error(`Gallery manifest at ${manifestUrl} has no compatible captures`);
Expand Down Expand Up @@ -432,8 +440,16 @@ async function serveDirectory(directory) {
};
}

function readPng(buffer, label) {
try {
return PNG.sync.read(buffer);
} catch (error) {
throw new Error(`${label} is not a valid PNG: ${error.message}`, { cause: error });
}
}

function assertNotBlank(buffer, name) {
const png = PNG.sync.read(buffer);
const png = readPng(buffer, name);
const first = [png.data[0], png.data[1], png.data[2], png.data[3]];
for (let offset = 4; offset < png.data.length; offset += 4) {
if (
Expand Down Expand Up @@ -729,8 +745,8 @@ async function captureRenderers(renderers, output, fixtures) {

async function compareImages(basePath, currentPath, diffPath) {
const [baseBuffer, currentBuffer] = await Promise.all([readFile(basePath), readFile(currentPath)]);
const baseline = PNG.sync.read(baseBuffer);
const current = PNG.sync.read(currentBuffer);
const baseline = readPng(baseBuffer, `Baseline image ${basePath}`);
const current = readPng(currentBuffer, `Current image ${currentPath}`);
if (baseline.width !== current.width || baseline.height !== current.height) {
throw new Error(`Image dimensions differ: ${baseline.width}x${baseline.height} vs ${current.width}x${current.height}`);
}
Expand Down Expand Up @@ -764,7 +780,7 @@ function imageMarkup(path, alt, viewport) {
return `<a href="${path}" aria-label="Open full-resolution ${alt}"><img src="${path}" loading="lazy" width="${viewport.width}" height="${viewport.height}" alt="${alt}"></a>`;
}

function renderReport(manifest) {
function renderReport(manifest, { showPreviewLink = true } = {}) {
const hasBaseline = Boolean(manifest.baseSha);
const count = (status) => manifest.captures.filter((capture) => capture.status === status).length;
const renderRows = (viewportName) => manifest.captures.filter((capture) => capture.viewport === viewportName).map((capture) => {
Expand Down Expand Up @@ -792,7 +808,7 @@ function renderReport(manifest) {
const tables = GALLERY_VIEWPORTS.map((viewport) => `
<section>
<h2>${viewport.name[0].toUpperCase()}${viewport.name.slice(1)} (${viewport.width} × ${viewport.height})</h2>
<table border="1" cellspacing="0">
<table class="${hasBaseline ? 'comparison' : 'gallery'}" border="1" cellspacing="0">
<thead>${hasBaseline
? '<tr><th>Page</th><th>Baseline</th><th>PR</th><th>Diff</th><th>Result</th></tr>'
: '<tr><th>Page</th><th>Screenshot</th></tr>'}</thead>
Expand All @@ -814,13 +830,16 @@ function renderReport(manifest) {
<title>connect ${hasBaseline ? 'visual regression report' : 'gallery'}</title>
<style>
table { width: 100%; table-layout: fixed; }
img { max-width: 100%; height: auto; }
table th:first-child { width: 7rem; }
table.comparison th:last-child { width: 9rem; }
td > a { display: block; max-width: 100%; overflow: auto; }
img { display: block; width: auto; max-width: none; height: auto; }
[hidden] { display: none !important; }
</style>
</head>
<body>
<header>
<p><a class="preview" href="/">Open interactive preview</a></p>
${showPreviewLink ? '<p><a class="preview" href="/">Open interactive preview</a></p>' : ''}
<h1>${hasBaseline ? 'Visual regression report' : 'Gallery'}</h1>
<p>${hasBaseline ? `Base: <code>${manifest.baseSha}</code><br>` : ''}Head: <code>${manifest.headSha}</code><br>Generated: <time datetime="${manifest.generatedAt}">${manifest.generatedAt}</time></p>
</header>
Expand All @@ -843,7 +862,19 @@ function renderReport(manifest) {
</html>\n`;
}

async function buildReport(captures, output, headSha, baseSha, baselineUrl) {
async function renderSelfContainedReport(manifest, output) {
const inlineManifest = JSON.parse(JSON.stringify(manifest));
await Promise.all(inlineManifest.captures.flatMap((capture) => (
Object.entries(capture.assets).map(async ([name, asset]) => {
if (!asset) return;
const contents = await readFile(resolve(output, asset.replace(/^\/+/, '')));
capture.assets[name] = `data:image/png;base64,${contents.toString('base64')}`;
})
)));
return renderReport(inlineManifest, { showPreviewLink: false });
}

async function buildReport(captures, output, headSha, baseSha, baselineUrl, artifactOutput) {
const currentDirectory = resolve(captures, 'current');
const baseDirectory = resolve(captures, 'base');
const hasBaseline = Boolean(baseSha);
Expand All @@ -863,23 +894,23 @@ async function buildReport(captures, output, headSha, baseSha, baselineUrl) {
for (const viewport of GALLERY_VIEWPORTS) {
const filename = captureFilename(state.name, viewport.name);
const currentSource = resolve(currentDirectory, filename);
const currentAsset = `/connect-gallery-assets/current/${filename}`;
const currentAsset = `./connect-gallery-assets/current/${filename}`;
let status = 'unavailable';
let changedPixels = null;
let changedRatio = null;
let baselineAsset = null;
let diffAsset = null;
const baselineSource = resolve(baseDirectory, filename);
if (hasBaseline && await fileExists(baselineSource)) {
baselineAsset = `/connect-gallery-assets/baseline/${filename}`;
baselineAsset = `./connect-gallery-assets/baseline/${filename}`;
let hasDiff;
({ changedPixels, changedRatio, hasDiff } = await compareImages(
baselineSource,
currentSource,
resolve(assetsDirectory, 'diff', filename),
));
status = changedRatio > CHANGE_THRESHOLD ? 'changed' : 'unchanged';
if (hasDiff) diffAsset = `/connect-gallery-assets/diff/${filename}`;
if (hasDiff) diffAsset = `./connect-gallery-assets/diff/${filename}`;
}
results.push({
state: state.name,
Expand All @@ -904,11 +935,19 @@ async function buildReport(captures, output, headSha, baseSha, baselineUrl) {
captures: results,
};
await mkdir(output, { recursive: true });
await Promise.all([
const writes = [
writeFile(resolve(assetsDirectory, 'manifest.json'), `${JSON.stringify(manifest, null, 2)}\n`),
writeFile(resolve(output, 'connect-gallery.html'), renderReport(manifest)),
]);
];
if (artifactOutput) {
await mkdir(dirname(artifactOutput), { recursive: true });
writes.push(renderSelfContainedReport(manifest, output).then((report) => (
writeFile(artifactOutput, report)
)));
}
await Promise.all(writes);
console.log(`Gallery report written to ${resolve(output, 'connect-gallery.html')}`);
if (artifactOutput) console.log(`Self-contained gallery artifact written to ${artifactOutput}`);
}

async function main() {
Expand Down Expand Up @@ -948,6 +987,7 @@ async function main() {
args['head-sha'] ?? process.env.GITHUB_SHA ?? await gitSha(source),
baseSha,
baselineUrl,
args['artifact-output'] ? resolve(args['artifact-output']) : null,
);
} finally {
await rm(temporary, { recursive: true, force: true });
Expand Down
10 changes: 3 additions & 7 deletions src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@ import qs from 'query-string';
import localforage from 'localforage';
import * as Sentry from '@sentry/react';

import { CircularProgress, Grid } from '@material-ui/core';

import MyCommaAuth, { config as AuthConfig, storage as AuthStorage } from '@commaai/my-comma-auth';
import { athena as Athena, auth as Auth, billing as Billing, request as Request } from './api';

Expand Down Expand Up @@ -121,11 +119,9 @@ class App extends Component {

renderLoading() {
return (
<Grid container alignItems="center" style={{ width: '100%', height: '100vh' }}>
<Grid item align="center" xs={12}>
<CircularProgress size="10vh" style={{ color: '#525E66' }} />
</Grid>
</Grid>
<div className="flex h-screen w-full items-center justify-center" role="status" aria-label="Loading">
<div className="h-[10vh] w-[10vh] animate-spin rounded-full border-[0.8vh] border-progress border-t-transparent" />
</div>
);
}

Expand Down
115 changes: 0 additions & 115 deletions src/colors.js
Original file line number Diff line number Diff line change
@@ -1,20 +1,9 @@
const Colors = {
transparent: 'transparent',
white: '#fff',
darken10: 'rgba(0, 0, 0, 0.1)',
darken20: 'rgba(0, 0, 0, 0.2)',
darken30: 'rgba(0, 0, 0, 0.3)',
darken40: 'rgba(0, 0, 0, 0.4)',
darken50: 'rgba(0, 0, 0, 0.5)',
darken60: 'rgba(0, 0, 0, 0.6)',
darken70: 'rgba(0, 0, 0, 0.7)',
darken80: 'rgba(0, 0, 0, 0.8)',
darken90: 'rgba(0, 0, 0, 0.9)',
white03: 'rgba(255, 255, 255, 0.03)',
white05: 'rgba(255, 255, 255, 0.05)',
white08: 'rgba(255, 255, 255, 0.08)',
white10: 'rgba(255, 255, 255, 0.1)',
white12: 'rgba(255, 255, 255, 0.12)',
white20: 'rgba(255, 255, 255, 0.2)',
white30: 'rgba(255, 255, 255, 0.3)',
white40: 'rgba(255, 255, 255, 0.4)',
Expand All @@ -23,112 +12,20 @@ const Colors = {
white70: 'rgba(255, 255, 255, 0.7)',
white80: 'rgba(255, 255, 255, 0.8)',
white90: 'rgba(255, 255, 255, 0.9)',
black: '#030404',
blue50: '#258FDA',
blue100: '#2284c9',
blue200: '#1f79b8',
blue300: '#1c6ea8',
blue400: '#1a6397',
blue500: '#175886',
blue600: '#144d75',
blue700: '#114265',
blue800: '#0e3754',
blue900: '#0b2c43',
blue950: '#061622',
blue999: '#030b11',
lightBlue50: '#eef6fc',
lightBlue100: '#ddeef9',
lightBlue200: '#cde5f6',
lightBlue300: '#bcddf4',
lightBlue400: '#abd4f1',
lightBlue500: '#9acbee',
lightBlue600: '#8ac3eb',
lightBlue700: '#79bae8',
lightBlue800: '#68b1e5',
lightBlue900: '#57a9e3',
desatBlue800: '#657f92',
primeBlue50: '#5e8bff',
primeBlue100: '#5984F2',
primeBlue200: '#547de6',
primeBlue300: '#4b6fcc',
primeBlue400: '#4261b3',
primeBlue500: '#385399',
primeBlue600: '#2f4680',
primeBlue700: '#263866',
primeBlue800: '#1c2a4d',
primeBlue900: '#131c33',
red50: '#da2535',
red100: '#c92231',
red200: '#b81f2d',
red300: '#971a25',
red400: '#861721',
red500: '#75141d',
red600: '#651118',
red700: '#540e14',
red800: '#430b10',
red900: '#32090c',
lightRed50: '#fceeef',
lightRed100: '#f9dde0',
lightRed200: '#f6cdd0',
lightRed300: '#f4bcc1',
lightRed400: '#f1abb1',
lightRed500: '#ee9aa2',
lightRed600: '#eb8a92',
lightRed700: '#e87983',
lightRed800: '#e56873',
lightRed900: '#e35764',
lightRed950: '#e04754',
lightRed999: '#dd3645',
green50: '#22c967',
green100: '#20b85f',
green200: '#1da756',
green300: '#1a974e',
green400: '#178645',
green500: '#14753c',
green600: '#116534',
green700: '#0e542b',
green800: '#0c4323',
green900: '#09321a',
lightGreen50: '#eefcf4',
lightGreen100: '#def9e9',
lightGreen200: '#cdf6de',
lightGreen300: '#bcf4d3',
lightGreen400: '#abf1c8',
lightGreen500: '#9beebd',
lightGreen600: '#8aebb2',
lightGreen700: '#79e8a7',
lightGreen800: '#68e59c',
lightGreen900: '#25da70',
orange50: '#da6f25',
orange100: '#c96722',
orange200: '#b85e1f',
orange300: '#a7551c',
orange400: '#964d19',
orange500: '#864417',
orange600: '#753c14',
orange700: '#643311',
orange800: '#532b0e',
orange900: '#42220b',
lightOrange50: '#f9e8dd',
lightOrange100: '#f6ddcc',
lightOrange200: '#f4d2bb',
lightOrange300: '#f1c7aa',
lightOrange400: '#eebc9a',
lightOrange500: '#ebb189',
lightOrange600: '#e8a678',
lightOrange700: '#e59b67',
lightOrange800: '#e08546',
lightOrange900: '#dd7a35',
yellow50: '#f1ebaa',
yellow100: '#eee79a',
yellow200: '#ebe389',
yellow300: '#e8df78',
yellow400: '#e5db67',
yellow500: '#e3d756',
yellow600: '#e0d346',
yellow700: '#ddcf35',
yellow800: '#daca25',
yellow900: '#c9bb22',
grey50: '#6e7d84',
grey100: '#65737a',
grey200: '#5c696f',
Expand All @@ -140,19 +37,7 @@ const Colors = {
grey800: '#272c2f',
grey900: '#1e2224',
grey950: '#151819',
grey999: '#0c0e0f',
lightGrey50: '#f8f9f9',
lightGrey100: '#eeeff0',
lightGrey200: '#e3e6e8',
lightGrey300: '#d8dcdf',
lightGrey400: '#cdd3d6',
lightGrey500: '#c3c9cd',
lightGrey600: '#b8c0c4',
lightGrey700: '#adb6bb',
lightGrey800: '#a3adb2',
lightGrey900: '#98a3a9',
lightGrey950: '#8d9aa1',
lightGrey999: '#77878f',
};

export default Colors;
4 changes: 2 additions & 2 deletions src/components/AppDrawer/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,9 @@ const AppDrawer = ({
open={isPermanent || drawerIsOpen}
onClose={toggleDrawerOff}
variant={isPermanent ? 'permanent' : 'temporary'}
PaperProps={{ style: { width, top: 'auto' } }}
PaperProps={{ style: { width, top: 'auto', borderRight: 'none' } }}
>
<div ref={contentRef} className="flex flex-col h-full bg-[linear-gradient(180deg,#1B2023_0%,#111516_100%)] ml-safe-left">
<div ref={contentRef} className="flex flex-col h-full bg-linear-to-b from-surface-overlay to-surface-overlay-deep ml-safe-left">
{!isPermanent
&& (
<Link to="/" className="flex items-center min-h-[64px] mx-2">
Expand Down
Loading
Loading