Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
ca8d80d
Add `workflow web` command as an alias to `workflow inspect runs --web`
VaguelySerious Oct 28, 2025
2e502b1
Hide trace viewer search bar if there are less than 10 spans
VaguelySerious Oct 28, 2025
08c8a3a
Delete dead code
VaguelySerious Oct 28, 2025
cac18b9
Extract package
VaguelySerious Oct 28, 2025
9d65f5e
Add UI picture in docs getting started guide
VaguelySerious Oct 28, 2025
80f923e
Fix up
VaguelySerious Oct 28, 2025
02f1d72
Add UI picture in docs getting started guide
VaguelySerious Oct 28, 2025
8301bc7
Merge branch 'peter/more-web-commands' into peter/move
VaguelySerious Oct 28, 2025
59b04d1
Changeset
VaguelySerious Oct 28, 2025
58e24e2
Merge branch 'peter/more-web-commands' into peter/move
VaguelySerious Oct 28, 2025
5266745
Fix build
VaguelySerious Oct 28, 2025
e8449ed
Merge branch 'main' into peter/move
VaguelySerious Oct 28, 2025
dc23d55
Don't ship test command
VaguelySerious Oct 29, 2025
eb03e44
Changeset
VaguelySerious Oct 29, 2025
a67d3ac
Merge branch 'main' into peter/move
VaguelySerious Oct 29, 2025
9b3fa9a
Merge branch 'main' into peter/move
VaguelySerious Oct 29, 2025
e024f4f
Undo image change
VaguelySerious Oct 29, 2025
8c9876a
Smol fix
VaguelySerious Oct 29, 2025
5885bb3
Readme
VaguelySerious Oct 30, 2025
507d391
Merge branch 'main' into peter/move
VaguelySerious Oct 30, 2025
c293841
Fix build
VaguelySerious Oct 30, 2025
5f9fe2f
Fix build?
VaguelySerious Oct 30, 2025
bfd9a40
Update readme
VaguelySerious Oct 30, 2025
c3cf6e7
License
VaguelySerious Oct 30, 2025
71948e3
Fix hydration to recreating runs
VaguelySerious Oct 30, 2025
28a5603
So pedantic
VaguelySerious Oct 30, 2025
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
1 change: 1 addition & 0 deletions .changeset/pre.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"@workflow/tsconfig": "4.0.0",
"@workflow/typescript-plugin": "4.0.0",
"@workflow/web": "4.0.0",
"@workflow/web-shared": "4.0.0",
"workflow": "4.0.0",
"@workflow/world": "4.0.0",
"@workflow/world-local": "4.0.0",
Expand Down
6 changes: 6 additions & 0 deletions .changeset/warm-files-attack.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@workflow/web-shared": patch
"@workflow/web": patch
---

Extract reusable web UI code into shared package
12 changes: 7 additions & 5 deletions packages/nitro/src/builders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,11 +71,13 @@ export class LocalBuilder extends BaseBuilder {
}

export function getWorkflowDirs(nitro: Nitro) {
return unique([
...(nitro.options.workflow?.dirs ?? []),
join(nitro.options.rootDir, 'workflows'),
...nitro.options.scanDirs.map((dir) => join(dir, 'workflows')),
].map((dir) => resolve(nitro.options.rootDir, dir))).sort();
return unique(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this just a formatting change?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes - must have originally not been formatted with biome

[
...(nitro.options.workflow?.dirs ?? []),
join(nitro.options.rootDir, 'workflows'),
...nitro.options.scanDirs.map((dir) => join(dir, 'workflows')),
].map((dir) => resolve(nitro.options.rootDir, dir))
).sort();
}

function unique<T>(array: T[]): T[] {
Expand Down
2 changes: 1 addition & 1 deletion packages/nitro/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export interface ModuleOptions {
export interface ModuleOptions {
/**
* Directories to scan for workflows and steps.
*
Expand Down
20 changes: 11 additions & 9 deletions packages/nitro/test/dirs.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@ import { describe, expect, test } from 'vitest';
import { getWorkflowDirs } from '../src/builders.ts';

const nitroMock = (dirs: string[]) => {
return ({
options: {
rootDir: '/root',
scanDirs: ['/root/server/'],
workflow: { dirs: dirs },
},
}) as unknown as Nitro;
}
return {
options: {
rootDir: '/root',
scanDirs: ['/root/server/'],
workflow: { dirs: dirs },
},
} as unknown as Nitro;
};

describe('nitro:getWorkflowDirs', () => {
test('default dirs', () => {
Expand All @@ -19,7 +19,9 @@ describe('nitro:getWorkflowDirs', () => {
});

test('custom dirs', () => {
const result = getWorkflowDirs(nitroMock(['./relative/dir1', '/custom/dir2']));
const result = getWorkflowDirs(
nitroMock(['./relative/dir1', '/custom/dir2'])
);
expect(result).toEqual([
'/custom/dir2',
'/root/relative/dir1',
Expand Down
42 changes: 42 additions & 0 deletions packages/web-shared/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions

# testing
/coverage

# next.js
/.next/
/out/

# production
/build

# misc
.DS_Store
*.pem

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*

# env files (can opt-in for committing if needed)
.env*

# vercel
.vercel

# typescript
*.tsbuildinfo
next-env.d.ts
.env*.local
1 change: 1 addition & 0 deletions packages/web-shared/LICENSE.md
74 changes: 74 additions & 0 deletions packages/web-shared/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# @workflow/web-shared

Workflow Observability tools for NextJS. See [Workflow DevKit](https://useworkflow.dev/docs/observability) for more information.

## Usage

This package contains client and server code to interact with the Workflow API.
You can use it like so to display your own runs list:

```tsx
import { useWorkflowRuns } from '@workflow/web-shared';

export default function MyRunsList() {
const {
data,
error,
nextPage,
previousPage,
hasNextPage,
hasPreviousPage,
reload,
pageInfo,
} = useWorkflowRuns(env, {
sortOrder,
workflowName: workflowNameFilter === 'all' ? undefined : workflowNameFilter,
status: status === 'all' ? undefined : status,
});

// Shows an interactive trace viewer for the given run
return <div>{runs.map((run) => (
<div key={run.runId}>
{run.workflowName}
{run.status}
{run.startedAt}
{run.completedAt}
</div>
))}</div>;
}
```

It also comes with a pre-styled interactive trace viewer that you can use to display the trace for a given run:

```tsx
import { RunTraceView } from '@workflow/web-shared';

export default function MyRunDetailView({ env, runId }: { env: EnvMap, runId: string }) {
// ... your other code

// Shows an interactive trace viewer for the given run
return <RunTraceView env={env} runId={runId} />;
}
```

## Environment Variables

For API calls to work, you'll need to pass the same environment variables that are used by the Workflow CLI.
See `npx workflow inspect --help` for more information.

If you're deploying this as part of your Vercel NextJS app, setting `WORKFLOW_TARGET_WORLD` to `vercel` is enough
to infer your other project details from the Vercel environment variables.

## Styling

In order for tailwind classes to be picked up correctly, you might need to configure your NextJS app
to use the correct CSS processor. E.g. if you're using PostCSS with TailwindCSS, you can do the following:

```tsx
// postcss.config.mjs in your NextJS app
const config = {
plugins: ['@tailwindcss/postcss'],
};

export default config;
```
59 changes: 59 additions & 0 deletions packages/web-shared/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
{
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you also symlink the LICENSE from the repo root please like we do in all the other packages?

"name": "@workflow/web-shared",
"description": "Shared components for Workflow Observability UI",
"version": "4.0.1-beta.4",
"private": false,
"files": [
"dist"
],
"publishConfig": {
"access": "public"
},
"license": "MIT",
"exports": {
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"./server": {
"types": "./dist/api/workflow-server-actions.d.ts",
"default": "./dist/api/workflow-server-actions.js"
}
},
"repository": {
"type": "git",
"url": "https://github.com/vercel/workflow.git",
"directory": "packages/web-shared"
},
"scripts": {
"build": "tsc && cp -r src/trace-viewer/*.css dist/trace-viewer/",
"dev": "tsc --watch",
"clean": "tsc --build --clean && rm -r dist ||:",
"typecheck": "tsc --noEmit",
"lint": "biome check",
"format": "biome format --write"
},
"dependencies": {
"@tailwindcss/postcss": "^4",
"@workflow/core": "workspace:*",
"@workflow/world": "workspace:*",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"date-fns": "^4.1.0",
"lucide-react": "^0.469.0",
"react": "19.1.0",
"react-dom": "19.1.0",
"sonner": "^2.0.7",
"swr": "^2.3.6",
"tailwind-merge": "^2.5.5",
"tailwindcss": "^4"
},
"devDependencies": {
"@biomejs/biome": "catalog:",
"@types/node": "catalog:",
"@types/react": "^19",
"@types/react-dom": "^19",
"typescript": "catalog:",
"@workflow/tsconfig": "workspace:*"
}
}
5 changes: 5 additions & 0 deletions packages/web-shared/postcss.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
const config = {
plugins: ['@tailwindcss/postcss'],
};

export default config;
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import type {
WorkflowRunStatus,
} from '@workflow/world';
import { useCallback, useEffect, useRef, useState } from 'react';
import { getPaginationDisplay } from '@/lib/utils';
import { getPaginationDisplay } from '../lib/utils';
import type { EnvMap, ServerActionError } from './workflow-server-actions';
import {
cancelRun as cancelRunServerAction,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,9 @@ export async function fetchRun(
try {
const world = getWorldFromEnv(worldEnv);
const run = await world.runs.get(runId, { resolveData });
return createResponse(hydrate(run as WorkflowRun));
const hydratedRun = hydrate(run as WorkflowRun);
console.log('hydratedRun', hydratedRun.input);
return createResponse(hydratedRun);
} catch (error) {
console.error('Failed to fetch run:', error);
return {
Expand Down Expand Up @@ -416,11 +418,15 @@ export async function recreateRun(
try {
const world = getWorldFromEnv({ ...worldEnv });
const run = await world.runs.get(runId);
const args = run.input;
const hydratedRun = hydrate(run as WorkflowRun);
const deploymentId = run.deploymentId;
const newRun = await start({ workflowId: run.workflowName }, args, {
deploymentId,
});
const newRun = await start(
{ workflowId: run.workflowName },
hydratedRun.input,
{
deploymentId,
}
);
return createResponse(newRun.runId);
} catch (error) {
console.error('Failed to start run:', error);
Expand Down
59 changes: 59 additions & 0 deletions packages/web-shared/src/components/ui/alert.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { cva, type VariantProps } from 'class-variance-authority';
import * as React from 'react';

import { cn } from '../../lib/utils';

const alertVariants = cva(
'relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground',
{
variants: {
variant: {
default: 'bg-background text-foreground',
destructive:
'border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive',
},
},
defaultVariants: {
variant: 'default',
},
}
);

const Alert = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
>(({ className, variant, ...props }, ref) => (
<div
ref={ref}
role="alert"
className={cn(alertVariants({ variant }), className)}
{...props}
/>
));
Alert.displayName = 'Alert';

const AlertTitle = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
<h5
ref={ref}
className={cn('mb-1 font-medium leading-none tracking-tight', className)}
{...props}
/>
));
AlertTitle.displayName = 'AlertTitle';

const AlertDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn('text-sm [&_p]:leading-relaxed', className)}
{...props}
/>
));
AlertDescription.displayName = 'AlertDescription';

export { Alert, AlertTitle, AlertDescription };
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@ export {

export type { Event, Hook, Step, WorkflowRun } from '@workflow/world';
export * from './api/workflow-api-client';
export { RunTraceView } from './run-trace-view';
export type { Span, SpanEvent } from './trace-viewer/types';
export { WorkflowTraceViewer } from './workflow-trace-view';
24 changes: 24 additions & 0 deletions packages/web-shared/src/lib/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { type ClassValue, clsx } from 'clsx';
import { twMerge } from 'tailwind-merge';

export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}

/**
* Returns a formatted pagination display string
* @param currentPage - The current page number
* @param totalPages - The total number of pages visited so far
* @param hasMore - Whether there are more pages available
* @returns Formatted string like "Page 1 of 3+" or "Page 2 of 2"
*/
export function getPaginationDisplay(
currentPage: number,
totalPages: number,
hasMore: boolean
): string {
if (hasMore) {
return `Page ${currentPage} of ${totalPages}+`;
}
return `Page ${currentPage} of ${totalPages}`;
}
Loading