mirror of
https://github.com/vercel/workflow.git
synced 2026-09-14 19:59:43 +08:00
3c7875ad73
* chore: upgrade @vercel/geistdocs to 1.17.1
Picks up the new footer (Footer no longer takes a config prop), the
heading font-weight change to 450, and the tightened navbar OSS-menu
marks. Also switches the site's own navbar logo from the vendored
geistcn LogoWorkflow fallback to the package's LogoWorkflowSdk, using
its new tuned default height instead of a hardcoded 15.
Fixes a resulting regression: navbarOssProducts entries lacked an `id`,
so resolveOssProducts' `product.id !== activeProduct` filter evaluated
to `undefined !== undefined` (false) for every entry and emptied the
OSS flyout. Added stable `id`/`label` values to each entry.
* refactor: use default navbarOssProducts list instead of a custom override
The manual navbarOssProducts array (with local logo imports/heights) is
no longer needed now that the package's DEFAULT_OSS_PRODUCTS list
already includes all these SDKs with proper id/label/section values.
navbarActiveProduct: 'workflow-sdk' now handles self-exclusion instead.
* fix: use bg-background-200 for docs surfaces to match the template
The docs, cookbook, and v5 route layouts plus the shared DocsLayout
container hardcoded bg-background-100 (pure white), so /docs/* pages
rendered on a lighter surface than the rest of the site. Switch them to
bg-background-200, matching the geistdocs template's page background.
* style: adopt package text-heading-* utilities for homepage headings
The marketing homepage headings hardcoded font-semibold (weight 600)
plus manual responsive sizes/tracking, so they rendered heavier than
the docs headings that now use Geist's 450 heading weight. Swap each
display heading to the package's text-heading-* utilities, which bundle
the 450 weight, line-height, and tracking, mapped across breakpoints to
the nearest design-system size. Inline label/emphasis spans keep their
own weight.
* style: adopt package text-heading-* utilities for worlds headings
Extends the homepage heading change to the /worlds section: the world
listing, detail, compare, and building-a-world pages plus their
components hardcoded font-semibold display headings. Swap each to the
package's text-heading-* utilities (450 weight + line-height +
tracking), mapped across breakpoints to the nearest design-system size.
Mono stat numbers, per-benchmark item labels, and the dialog title keep
their own weight.
* style: remove the bordered grid framing from the homepage
The homepage sections were wrapped in a grid divide-y border-y sm:border-x
container, drawing side borders and divider lines between every section.
Drop that framing so the sections flow with whitespace separation.
* style: remove vertical column dividers from homepage sections
Drop the divide-x column dividers still drawn inside the use-cases
(3-col), feature-grid (2-col), and templates sections, so no vertical
lines remain after the section-grid removal. Section padding keeps the
columns visually separated.
* style: make the homepage "Get started" CTA button rounded-full
* style: use text-heading-* for the feature-grid paragraph text
The two 2-col feature blurbs ("Deep integration with AI SDK.",
"Durable agents by default.") hardcoded their size/leading/tracking
plus font-medium/font-semibold weights. Those manual sizes already
equal text-heading-20/24, so swap to text-heading-20 lg:text-heading-24
— same sizes, but the Geist 450 heading weight (lead drops 600 -> 500
via the utility's [&>strong] rule). The lead stays gray-1000 for
emphasis; body stays gray-900.
* style: fade out the run-anywhere provider logos at the left/right edges
Add linear-gradient masks to the flanking cloud-provider logo groups in
the "Run anywhere, no lock-in" viz so they fade to transparent toward
the outer edges, leaving the centered code block untouched.
* style: widen the right-edge fade on the Vercel dashboard viz
The "Workflow SDK on Vercel" dashboard is offset off the right edge, so
the existing to_left black_10% mask fell off-screen and the visible
right edge hard-clipped. Widen it to black_40% so the dashboard fades
out gradually at the visible right edge.
* style: add spacing between the Vercel, use-cases, and templates sections
Wrap the UseCases and Templates sections with a top margin so there's
clear separation between "Workflow SDK on Vercel", "Build anything with
AI Agents", and "Get started" now that the section dividers are gone.
* style: widen the homepage layout from 1080px to 1200px
* style: align use-cases code block and templates cards with the Vercel section
Switch the "Build anything with" and "Get started" sections from
grid-cols-3 / [1fr_2fr] to [1fr_1.5fr], matching the "Workflow SDK on
Vercel" section above so their code block and cards share the same
right-hand column. The wider text column also lets "Build anything with"
sit on one line. Normalize both to outer padding + column gap so the
code block and cards line up exactly.
* style: extend use-cases/templates content to the right layout edge
Drop the right padding at md+ (md:pr-0) so the code block and template
cards reach the same right edge as the "Workflow SDK on Vercel"
dashboard above, which bleeds to the container edge. Mobile keeps its
padding.
* style: remove the divider between the two feature cards
Drop divide-y/lg:divide-y-0 from the feature grid so no border shows
between "Deep integration with AI SDK" and "Durable agents by default".
* refactor: position homepage sections on a shared 12-col grid
Replace the ad-hoc [1fr_1.5fr] + md:pr-0 + lg:pl-* positioning on the
Vercel, use-cases, and templates sections with a shared grid-cols-12
layout (text col-span-5, visual col-span-7), matching the vercel.com
marketing grid convention. The Vercel dashboard becomes a proper grid
cell instead of an absolutely-offset right-bleed, so all three
sections' visuals align by the grid columns with no magic values.
* style: align homepage width with the navbar content
Widen the homepage container from max-w-[1200px] to the site's
max-w-[1448px] (matching the navbar/footer) and reduce the section
gutters from sm:px-12 to sm:px-6, so section content lines up with the
navbar's content edges (right edge flush at the same column as the
navbar and footer). Also convert the "Reliability-as-code" section to
the shared grid-cols-12 layout (col-span-5 text / col-span-7 code
example), replacing its lg:grid-cols-[330px_1fr] magic values.
* refactor: handle homepage horizontal padding at the root container
Move the mobile/desktop gutter (px-4 sm:px-6) onto the homepage root
container and remove the horizontal padding from every section
component. Section content still aligns with the navbar/footer content
edges, but the gutter is now defined once instead of repeated per
section. Inner-element padding (tab buttons, visual internals) is
unchanged.
* style: left-align content sections on mobile + fix run-anywhere/o11y viz
- Left-align the centered content sections on mobile only (FeatureCardWide,
TweetWall heading, Frameworks, Run-anywhere heading/buttons), restoring
their centered layout at sm and up.
- Make the "Inspect every run" timeline span edge-to-edge by shifting its
gantt from a 14-col grid (content in cols 2-13) to a flush 12-col grid.
- Constrain the run-anywhere viz cluster to the code block width so the
provider cards (AWS/Docker/etc.) overlap behind the code block again.
* style: anchor run-anywhere provider cards to overlap the code block
Position the flanking provider-card groups relative to the centered
code block (right/left calc(50%+140px)) instead of the section edges,
so the cards sit behind and overlap the code block regardless of the
section width.
* style: make the reliability-as-code example fill its column to the right edge
Drop max-w-3xl mx-auto from the workflow/non-workflow code examples so
they fill the col-span-7 cell, aligning the code block's right edge with
the layout's right content edge (matching the tabs and other sections).
* style: split feature-card copy into a title + description
Break the AI SDK / durable-agents feature blurbs into a heading and a
separate muted description with a gap (matching the other sections)
instead of one inline paragraph, and drop the trailing periods from the
feature titles so they read as headings.
* update
* update
* style: give the tweet cards a bg-background-100 surface
* fix(swc-playground): pin monaco-editor to 0.55.1
The lockfile refresh resolved the unpinned `monaco-editor: "latest"` from
0.55.1 to 0.56.0, breaking the workflow-swc-playground Turbopack build.
0.56.0 rewrote its exports map to reroot subpaths under `esm/vs/`
("./*": "./esm/vs/*.js"). monaco-vim@0.4.4 deep-imports
`monaco-editor/esm/vs/editor/editor.api` and
`.../common/commands/shiftCommand`, which now map to
`esm/vs/esm/vs/...` — a path that does not exist. Under 0.55.1
("./*": "./*") both specifiers resolve to real files.
monaco-vim 0.4.4 is the latest published release, so pinning
monaco-editor is the only available fix.
* update
---------
Signed-off-by: christopherkindl <53372002+christopherkindl@users.noreply.github.com>
466 lines
18 KiB
TypeScript
466 lines
18 KiB
TypeScript
'use client';
|
|
|
|
import {
|
|
Tooltip,
|
|
TooltipContent,
|
|
TooltipTrigger,
|
|
} from '@vercel/geistdocs/components/tooltip';
|
|
import {
|
|
AlertCircle,
|
|
CheckCircle2,
|
|
Clock,
|
|
Info,
|
|
TrendingUp,
|
|
XCircle,
|
|
} from 'lucide-react';
|
|
import { useState } from 'react';
|
|
import { Badge } from '@/components/ui/badge';
|
|
import {
|
|
Table,
|
|
TableBody,
|
|
TableCell,
|
|
TableHead,
|
|
TableHeader,
|
|
TableRow,
|
|
} from '@/components/ui/table';
|
|
import { BenchmarkHistoryChart } from './BenchmarkHistoryChart';
|
|
import { formatTime, type World } from './types';
|
|
|
|
const TimeColumnHeader = () => (
|
|
<div className="flex items-center justify-end gap-1">
|
|
<span>Time</span>
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<Info className="h-3.5 w-3.5 text-muted-foreground cursor-help" />
|
|
</TooltipTrigger>
|
|
<TooltipContent side="top" className="max-w-[200px]">
|
|
Time from workflow created to workflow completed
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
</div>
|
|
);
|
|
|
|
const TTFBColumnHeader = () => (
|
|
<div className="flex items-center justify-end gap-1">
|
|
<span>TTFB</span>
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<Info className="h-3.5 w-3.5 text-muted-foreground cursor-help" />
|
|
</TooltipTrigger>
|
|
<TooltipContent side="top" className="max-w-[200px]">
|
|
Time to first byte
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
</div>
|
|
);
|
|
|
|
const SlurpColumnHeader = () => (
|
|
<div className="flex items-center justify-end gap-1">
|
|
<span>Slurp</span>
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<Info className="h-3.5 w-3.5 text-muted-foreground cursor-help" />
|
|
</TooltipTrigger>
|
|
<TooltipContent side="top" className="max-w-[200px]">
|
|
Time from first byte to stream completion
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
</div>
|
|
);
|
|
|
|
export interface WorldTestingPerformanceProps {
|
|
worldId: string;
|
|
world: World;
|
|
meta: {
|
|
lastUpdated: string;
|
|
commit: string | null;
|
|
branch: string | null;
|
|
};
|
|
showBenchmarks?: boolean;
|
|
}
|
|
|
|
const statusConfig = {
|
|
passing: {
|
|
label: 'Passing',
|
|
icon: CheckCircle2,
|
|
className: 'bg-green-900 dark:bg-green-600 text-white',
|
|
},
|
|
partial: {
|
|
label: 'Partial',
|
|
icon: AlertCircle,
|
|
className: 'bg-amber-700 text-white',
|
|
},
|
|
failing: {
|
|
label: 'Failing',
|
|
icon: XCircle,
|
|
className: 'bg-red-900 dark:bg-red-800 text-white',
|
|
},
|
|
pending: {
|
|
label: 'Pending',
|
|
icon: Clock,
|
|
className: 'bg-gray-300 text-gray-1000',
|
|
},
|
|
};
|
|
|
|
export function WorldTestingPerformance({
|
|
worldId,
|
|
world,
|
|
meta,
|
|
showBenchmarks = false,
|
|
}: WorldTestingPerformanceProps) {
|
|
const [selectedMetric, setSelectedMetric] = useState<string | null>(null);
|
|
|
|
const e2e = world.e2e;
|
|
const benchmark = world.benchmark;
|
|
|
|
const hasE2E = !!e2e;
|
|
const hasBenchmarks =
|
|
benchmark?.metrics && Object.keys(benchmark.metrics).length > 0;
|
|
|
|
// E2E calculations - use nextjs-turbopack data for canonical scoring
|
|
const turbopackData = e2e?.nextjsTurbopack;
|
|
const hasFrameworkData = !!turbopackData;
|
|
|
|
// Canonical scoring based on nextjs-turbopack
|
|
const scoringPassed = turbopackData
|
|
? turbopackData.passed
|
|
: (e2e?.passed ?? 0);
|
|
const scoringFailed = turbopackData
|
|
? turbopackData.failed
|
|
: (e2e?.failed ?? 0);
|
|
const scoringTotal = turbopackData
|
|
? turbopackData.total
|
|
: (e2e?.total ?? 0) - (e2e?.skipped ?? 0);
|
|
// Skipped = total - passed - failed
|
|
const scoringSkipped = scoringTotal - scoringPassed - scoringFailed;
|
|
|
|
// For percentage: passed / (passed + failed) - excludes skipped tests
|
|
// If failed === 0, that's 100% passing regardless of skipped count
|
|
const testsRan = scoringPassed + scoringFailed;
|
|
const displayProgress =
|
|
testsRan > 0 ? Math.round((scoringPassed / testsRan) * 100) : 0;
|
|
|
|
const status = e2e?.status ?? 'pending';
|
|
const config = statusConfig[status];
|
|
const StatusIcon = config.icon;
|
|
|
|
// Benchmark calculations - separate standard and stream benchmarks
|
|
const allMetrics =
|
|
hasBenchmarks && benchmark.metrics
|
|
? Object.entries(benchmark.metrics).sort(([a], [b]) => a.localeCompare(b))
|
|
: [];
|
|
|
|
// Stream benchmarks have ttfb field
|
|
const standardMetrics = allMetrics.filter(([, metric]) => !metric.ttfb);
|
|
const streamMetrics = allMetrics.filter(([, metric]) => metric.ttfb);
|
|
|
|
// Check if we have workflow min/max data (for showing range columns)
|
|
const hasWorkflowRange = allMetrics.some(
|
|
([, metric]) =>
|
|
metric.workflowMin !== undefined && metric.workflowMax !== undefined
|
|
);
|
|
|
|
return (
|
|
<section id="testing">
|
|
<div className="space-y-8">
|
|
{/* E2E Tests Subsection */}
|
|
<div className="space-y-4">
|
|
<h3 className="text-heading-20">E2E Tests</h3>
|
|
{hasE2E ? (
|
|
<>
|
|
{/* Summary - based on nextjs-turbopack for canonical scoring */}
|
|
<div className="flex flex-wrap items-center gap-3">
|
|
<Badge
|
|
className={`gap-1 text-sm py-1 px-3 ${config.className}`}
|
|
>
|
|
<StatusIcon className="h-4 w-4" />
|
|
{config.label}
|
|
</Badge>
|
|
<span className="text-lg font-medium">
|
|
{displayProgress}% passing
|
|
</span>
|
|
</div>
|
|
|
|
{/* Disclaimer about scoring methodology */}
|
|
<p className="text-sm text-muted-foreground">
|
|
{hasFrameworkData
|
|
? 'Spec compliance is tested against Next.js (Turbopack) built in production mode and started with `next start`.'
|
|
: 'E2E test pass rate across all tests run for this world.'}
|
|
{meta.commit && (
|
|
<>
|
|
{' '}
|
|
<a
|
|
href={`https://github.com/vercel/workflow/commit/${meta.commit}/checks`}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="text-foreground hover:underline"
|
|
>
|
|
View CI run →
|
|
</a>
|
|
</>
|
|
)}
|
|
</p>
|
|
|
|
{/* Details - show pass/fail/skipped counts */}
|
|
<div className="grid gap-4 sm:grid-cols-4 text-sm">
|
|
<div className="p-4 rounded-lg border bg-card">
|
|
<div className="text-2xl font-semibold text-green-900 dark:text-green-600 font-mono">
|
|
{scoringPassed}
|
|
</div>
|
|
<div className="text-muted-foreground">Passed</div>
|
|
</div>
|
|
<div className="p-4 rounded-lg border bg-card">
|
|
<div className="text-2xl font-semibold text-red-900 dark:text-red-800 font-mono">
|
|
{scoringFailed}
|
|
</div>
|
|
<div className="text-muted-foreground">Failed</div>
|
|
</div>
|
|
<div className="p-4 rounded-lg border bg-card">
|
|
<div className="text-2xl font-semibold font-mono">
|
|
{scoringSkipped}
|
|
</div>
|
|
<div className="text-muted-foreground">Skipped</div>
|
|
</div>
|
|
<div className="p-4 rounded-lg border bg-card">
|
|
<div className="text-2xl font-semibold font-mono">
|
|
{scoringTotal}
|
|
</div>
|
|
<div className="text-muted-foreground">Total</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Show full test breakdown if available */}
|
|
{e2e.total !== scoringTotal && (
|
|
<details className="text-sm">
|
|
<summary className="cursor-pointer text-muted-foreground hover:text-foreground">
|
|
View comprehensive E2E test results against all
|
|
frameworks/configurations
|
|
</summary>
|
|
<div className="mt-3 grid gap-4 sm:grid-cols-4">
|
|
<div className="p-3 rounded-lg border bg-card">
|
|
<div className="text-xl font-semibold text-green-900 dark:text-green-600 font-mono">
|
|
{e2e.passed}
|
|
</div>
|
|
<div className="text-muted-foreground text-xs">
|
|
Passed
|
|
</div>
|
|
</div>
|
|
<div className="p-3 rounded-lg border bg-card">
|
|
<div className="text-xl font-semibold text-red-900 dark:text-red-800 font-mono">
|
|
{e2e.failed}
|
|
</div>
|
|
<div className="text-muted-foreground text-xs">
|
|
Failed
|
|
</div>
|
|
</div>
|
|
<div className="p-3 rounded-lg border bg-card">
|
|
<div className="text-xl font-semibold font-mono">
|
|
{e2e.skipped}
|
|
</div>
|
|
<div className="text-muted-foreground text-xs">
|
|
Skipped
|
|
</div>
|
|
</div>
|
|
<div className="p-3 rounded-lg border bg-card">
|
|
<div className="text-xl font-semibold font-mono">
|
|
{e2e.total}
|
|
</div>
|
|
<div className="text-muted-foreground text-xs">Total</div>
|
|
</div>
|
|
</div>
|
|
</details>
|
|
)}
|
|
</>
|
|
) : (
|
|
<p className="text-muted-foreground">
|
|
No E2E test data is currently available for this world.
|
|
</p>
|
|
)}
|
|
</div>
|
|
|
|
{/* Benchmarks — only visible on preview deployments */}
|
|
{showBenchmarks && (
|
|
<div className="space-y-4">
|
|
<h3 className="text-heading-20">Benchmarks</h3>
|
|
{hasBenchmarks ? (
|
|
<>
|
|
<p className="text-sm text-muted-foreground">
|
|
Click on a benchmark to view performance history over the last
|
|
30 commits.
|
|
</p>
|
|
|
|
{/* Standard Benchmarks */}
|
|
{standardMetrics.length > 0 && (
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow>
|
|
<TableHead>Benchmark</TableHead>
|
|
<TableHead className="text-right">
|
|
<TimeColumnHeader />
|
|
</TableHead>
|
|
{hasWorkflowRange && (
|
|
<>
|
|
<TableHead className="text-right">Min</TableHead>
|
|
<TableHead className="text-right">Max</TableHead>
|
|
</>
|
|
)}
|
|
<TableHead className="text-right">Samples</TableHead>
|
|
<TableHead className="w-[40px]"></TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{standardMetrics.map(([name, metric]) => (
|
|
<TableRow
|
|
key={name}
|
|
className="cursor-pointer hover:bg-muted/50 transition-colors"
|
|
onClick={() => setSelectedMetric(name)}
|
|
>
|
|
<TableCell className="font-medium">{name}</TableCell>
|
|
<TableCell className="text-right font-mono">
|
|
{metric.workflowTime !== undefined
|
|
? formatTime(metric.workflowTime)
|
|
: '—'}
|
|
</TableCell>
|
|
{hasWorkflowRange && (
|
|
<>
|
|
<TableCell className="text-right font-mono text-muted-foreground">
|
|
{metric.workflowMin !== undefined
|
|
? formatTime(metric.workflowMin)
|
|
: '—'}
|
|
</TableCell>
|
|
<TableCell className="text-right font-mono text-muted-foreground">
|
|
{metric.workflowMax !== undefined
|
|
? formatTime(metric.workflowMax)
|
|
: '—'}
|
|
</TableCell>
|
|
</>
|
|
)}
|
|
<TableCell className="text-right text-muted-foreground">
|
|
{metric.samples || '—'}
|
|
</TableCell>
|
|
<TableCell>
|
|
<TrendingUp className="h-4 w-4 text-muted-foreground" />
|
|
</TableCell>
|
|
</TableRow>
|
|
))}
|
|
</TableBody>
|
|
</Table>
|
|
)}
|
|
|
|
{/* Stream Benchmarks with TTFB and Slurp */}
|
|
{streamMetrics.length > 0 && (
|
|
<>
|
|
<h4 className="font-medium text-base mt-6">
|
|
Stream Benchmarks
|
|
</h4>
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow>
|
|
<TableHead>Benchmark</TableHead>
|
|
<TableHead className="text-right">
|
|
<TimeColumnHeader />
|
|
</TableHead>
|
|
<TableHead className="text-right">
|
|
<TTFBColumnHeader />
|
|
</TableHead>
|
|
<TableHead className="text-right">
|
|
<SlurpColumnHeader />
|
|
</TableHead>
|
|
{hasWorkflowRange && (
|
|
<>
|
|
<TableHead className="text-right">Min</TableHead>
|
|
<TableHead className="text-right">Max</TableHead>
|
|
</>
|
|
)}
|
|
<TableHead className="text-right">Samples</TableHead>
|
|
<TableHead className="w-[40px]"></TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{streamMetrics.map(([name, metric]) => (
|
|
<TableRow
|
|
key={name}
|
|
className="cursor-pointer hover:bg-muted/50 transition-colors"
|
|
onClick={() => setSelectedMetric(name)}
|
|
>
|
|
<TableCell className="font-medium">
|
|
{name}
|
|
</TableCell>
|
|
<TableCell className="text-right font-mono">
|
|
{metric.workflowTime !== undefined
|
|
? formatTime(metric.workflowTime)
|
|
: '—'}
|
|
</TableCell>
|
|
<TableCell className="text-right font-mono text-green-900">
|
|
{metric.ttfb ? formatTime(metric.ttfb) : '—'}
|
|
</TableCell>
|
|
<TableCell className="text-right font-mono text-purple-900">
|
|
{metric.slurp ? formatTime(metric.slurp) : '—'}
|
|
</TableCell>
|
|
{hasWorkflowRange && (
|
|
<>
|
|
<TableCell className="text-right font-mono text-muted-foreground">
|
|
{metric.workflowMin !== undefined
|
|
? formatTime(metric.workflowMin)
|
|
: '—'}
|
|
</TableCell>
|
|
<TableCell className="text-right font-mono text-muted-foreground">
|
|
{metric.workflowMax !== undefined
|
|
? formatTime(metric.workflowMax)
|
|
: '—'}
|
|
</TableCell>
|
|
</>
|
|
)}
|
|
<TableCell className="text-right text-muted-foreground">
|
|
{metric.samples || '—'}
|
|
</TableCell>
|
|
<TableCell>
|
|
<TrendingUp className="h-4 w-4 text-muted-foreground" />
|
|
</TableCell>
|
|
</TableRow>
|
|
))}
|
|
</TableBody>
|
|
</Table>
|
|
</>
|
|
)}
|
|
|
|
<BenchmarkHistoryChart
|
|
worldId={worldId}
|
|
metricName={selectedMetric ?? ''}
|
|
open={selectedMetric !== null}
|
|
onOpenChange={(open) => !open && setSelectedMetric(null)}
|
|
/>
|
|
</>
|
|
) : (
|
|
<p className="text-muted-foreground">
|
|
No benchmark data is currently available for this world.
|
|
</p>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* Metadata */}
|
|
<div className="text-sm text-muted-foreground border-t pt-4">
|
|
<p>
|
|
Last updated: {new Date(meta.lastUpdated).toLocaleString()}
|
|
{meta.commit && (
|
|
<>
|
|
{' · '}
|
|
Commit:{' '}
|
|
<a
|
|
href={`https://github.com/vercel/workflow/commit/${meta.commit}`}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="font-mono hover:underline"
|
|
>
|
|
{meta.commit.slice(0, 7)}
|
|
</a>
|
|
</>
|
|
)}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
);
|
|
}
|