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
18 changes: 18 additions & 0 deletions .github/workflows/frontend-ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ on:
paths:
- "ui/**"
- ".github/workflows/frontend-ci.yaml"
# The golden round-trip test of the UI reads these YAML corpora.
- "examples/**"
- "internal/**/*.y*ml"
- "conformance/**/*.y*ml"
- "specs/**"
pull_request:
# Keep this workflow directly rerunnable from PR-only maintenance commits.
types:
Expand All @@ -17,6 +22,11 @@ on:
paths:
- "ui/**"
- ".github/workflows/frontend-ci.yaml"
# The golden round-trip test of the UI reads these YAML corpora.
- "examples/**"
- "internal/**/*.y*ml"
- "conformance/**/*.y*ml"
- "specs/**"
workflow_dispatch:

# Restrict to read-only: this workflow only runs tests and builds the frontend,
Expand All @@ -25,6 +35,14 @@ on:
permissions:
contents: read

concurrency:
# A new commit to a pull request cancels the run of the commit it
# replaces, so superseded runs stop holding the org's runners. Runs for
# main and manual dispatches each get a group of their own and always
# finish.
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
cancel-in-progress: true

jobs:
build:
name: Build
Expand Down
8 changes: 4 additions & 4 deletions ui/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -300,12 +300,12 @@ This document outlines the design principles and guidelines for the Dagu UI base

### Color & Styling
- **Consistent Metadata Styling**: Use uniform backgrounds for similar information
- Example: `bg-slate-200 dark:bg-slate-700` for metadata blocks
- Example: `bg-muted` for metadata blocks
- **Subtle Borders**: Use `border` class consistently, avoid thick borders
- **Text Hierarchy**: Use size and weight, not excessive color variation
- Primary text: `text-slate-800 dark:text-slate-200`
- Secondary text: `text-slate-600 dark:text-slate-400`
- Muted text: `text-slate-500 dark:text-slate-500`
- Primary text: `text-foreground`
- Secondary and muted text: `text-muted-foreground`
- **Theme Tokens**: Use semantic tokens, not palette classes such as `text-slate-*`; they follow dark mode without `dark:` variants

### Layout Principles
- **Flexbox First**: Use flexbox for dynamic layouts that fill available space
Expand Down
55 changes: 41 additions & 14 deletions ui/e2e/auth-flows.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,11 @@ test.describe('auth flows', () => {
await loginViaUI(page, username, oldPassword);

// Open user menu and start password change
await page.locator('aside').getByRole('button').filter({ hasText: username }).click();
await page
.locator('aside')
.getByRole('button')
.filter({ hasText: username })
.click();
await page.getByRole('menuitem', { name: 'Change Password' }).click();

const dialog = page.getByRole('dialog');
Expand All @@ -52,7 +56,9 @@ test.describe('auth flows', () => {
await dialog.getByLabel('Confirm New Password').fill(newPassword);
await dialog.getByRole('button', { name: 'Change Password' }).click();

await expect(dialog.getByText('Password changed successfully')).toBeVisible();
await expect(
dialog.getByText('Password changed successfully')
).toBeVisible();

// Clear session and login with new password
await clearSession(page);
Expand All @@ -66,7 +72,10 @@ test.describe('auth flows', () => {
expect(failResponse.status()).toBe(401);
});

test('manager can create DAGs but cannot manage users', async ({ page, request }) => {
test('manager can create DAGs but cannot manage users', async ({
page,
request,
}) => {
const stack = await loadStack();
const adminToken = await loginViaAPI(
request,
Expand Down Expand Up @@ -94,7 +103,9 @@ test.describe('auth flows', () => {
await expect(dialog).toBeVisible();
await dialog.getByLabel('Workflow name').fill(dagName);
await dialog.getByRole('button', { name: 'Create' }).click();
await expect(page).toHaveURL(new RegExp(`/dags/${dagName}/spec$`));
await expect
.poll(() => new URL(page.url()).pathname)
.toMatch(new RegExp(`^/dags/${dagName}(/spec)?$`));
await expect(
page.getByRole('heading', { level: 1, name: dagName, exact: true })
).toBeVisible();
Expand All @@ -104,7 +115,10 @@ test.describe('auth flows', () => {
await expect(page).not.toHaveURL(/\/users$/);
});

test('operator cannot write DAGs but can execute them', async ({ page, request }) => {
test('operator cannot write DAGs but can execute them', async ({
page,
request,
}) => {
const stack = await loadStack();
const adminToken = await loginViaAPI(
request,
Expand Down Expand Up @@ -137,22 +151,32 @@ steps:
// Operator should not see Create button
await useDefaultWorkspaceScope(page);
await page.goto('/dags/');
await expect(page.getByRole('button', { name: 'Create new DAG' })).toBeHidden();
await expect(
page.getByRole('button', { name: 'Create new DAG' })
).toBeHidden();

// Operator should not see Rename/Delete on spec tab
await page.goto(`/dags/${encodeURIComponent(fileName)}/spec`);
// Operator should not see Rename/Delete in the workflow bar
await page.goto(`/dags/${encodeURIComponent(fileName)}`);
await expect(
page.getByRole('heading', { level: 1, name: dagName, exact: true })
).toBeVisible();
const bar = page.getByTestId('ww-bar');
await expect(
page.getByRole('button', { name: 'Rename', exact: true })
).toBeHidden();
bar.getByRole('button', { name: 'Refresh', exact: true })
).toBeVisible();
await expect(
page.getByRole('button', { name: 'Delete', exact: true })
).toBeHidden();
bar.getByRole('button', { name: 'Rename', exact: true })
).toHaveCount(0);
await expect(
bar.getByRole('button', { name: 'Delete', exact: true })
).toHaveCount(0);

// Operator can still execute via API
const operatorToken = await loginViaAPI(request, operatorUser, operatorPass);
const operatorToken = await loginViaAPI(
request,
operatorUser,
operatorPass
);
const startResponse = await request.post(
`/api/v1/dags/${encodeURIComponent(fileName)}/start?remoteNode=local`,
{
Expand All @@ -166,7 +190,10 @@ steps:
expect(startResponse.ok()).toBeTruthy();
});

test('admin resets user password from management page', async ({ page, request }) => {
test('admin resets user password from management page', async ({
page,
request,
}) => {
const stack = await loadStack();
const adminToken = await loginViaAPI(
request,
Expand Down
35 changes: 35 additions & 0 deletions ui/e2e/fixtures/dags/visual-http-jq.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Demo: fetch JSON over HTTP, filter it with jq, log the result.
description: Fetch a JSON document, extract its title with jq and log it.
steps:
- id: fetch
name: fetch
action: http.request
with:
method: GET
url: https://httpbin.org/json
format: json
silent: true
output:
body:
from: stdout
decode: json
select: .body

- id: title
name: extract title
depends: [fetch]
action: jq.filter
with:
filter: .slideshow.title
data: ${steps.fetch.outputs.body}
raw: true
output:
text:
from: stdout
decode: text # plain text, no JSON quotes

- name: show title
depends: [title]
action: log.write
with:
message: "Title: ${steps.title.outputs.text}"
23 changes: 23 additions & 0 deletions ui/e2e/fixtures/dags/visual-params.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Demo: params, consts, declared outputs and continue_on.
description: Use params and consts, tolerate one failing step, then read a param and an output.
params:
- GREETING: hello
- TARGET: world
consts:
- SUFFIX: "!"
steps:
- id: prepare
name: prepare
run: printf 'token=abc123\n' >> "$DAGU_OUTPUT_FILE"
outputs:
- name: token

- name: flaky
depends: [prepare]
run: exit 1
continue_on:
failure: true

- name: report
depends: [flaky]
run: echo "${params.GREETING} ${params.TARGET}${consts.SUFFIX} token=${steps.prepare.outputs.token}"
32 changes: 32 additions & 0 deletions ui/e2e/fixtures/dags/visual-router.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Demo: route on a value to one of two handler steps.
description: Produce a status code, route it to a handler, and run lifecycle handlers.
type: graph
steps:
- id: produce
name: produce code
run: echo 200
output:
code:
from: stdout
decode: text

- name: route code
depends: [produce]
action: router.route
with:
value: "${steps.produce.outputs.code}"
routes:
"re:^2\\d\\d$": [handle_ok]
"re:^[^2]\\d\\d$": [handle_other]

- name: handle_ok
run: echo "ok branch"

- name: handle_other
run: echo "other branch"

handler_on:
success:
run: echo "dag succeeded"
failure:
run: echo "dag failed"
Loading