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
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
// Copyright (C) 2026 Yota Hamada
// SPDX-License-Identifier: GPL-3.0-or-later

import React, { useContext, useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { Maximize2, X } from 'lucide-react';
Expand Down Expand Up @@ -239,6 +242,7 @@ function DAGDetailsPanel({
isModal={true}
navigateToStatusTab={() => setActiveTab('status')}
localDags={data.localDags}
editorHints={data.editorHints}
/>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
// Copyright (C) 2026 Yota Hamada
// SPDX-License-Identifier: GPL-3.0-or-later

import React from 'react';
import { createPortal } from 'react-dom';
import { useNavigate } from 'react-router-dom';
Expand Down Expand Up @@ -386,6 +389,7 @@ function DAGDetailsSidePanel({
isModal={true}
navigateToStatusTab={navigateToStatusTab}
localDags={data.localDags}
editorHints={data.editorHints}
onEnqueue={onEnqueue ? handleEnqueue : undefined}
forceEnqueue={forceEnqueue}
autoOpenStartModal={false}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
// Copyright (C) 2026 Yota Hamada
// SPDX-License-Identifier: GPL-3.0-or-later

import { render, screen } from '@testing-library/react';
import React from 'react';
import { MemoryRouter } from 'react-router-dom';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { AppBarContext } from '@/contexts/AppBarContext';
import { PageContextProvider } from '@/contexts/PageContext';
import { useQuery } from '@/hooks/api';
import { useDAGSSE } from '@/hooks/useDAGSSE';
import DAGDetailsPanel from '../DAGDetailsPanel';

vi.mock('@/hooks/api', () => ({
useQuery: vi.fn(),
}));

vi.mock('@/hooks/useDAGSSE', () => ({
useDAGSSE: vi.fn(),
}));

vi.mock('@/hooks/useSSECacheSync', () => ({
sseFallbackOptions: vi.fn(() => ({})),
useSSECacheSync: vi.fn(),
}));

vi.mock('../DAGDetailsContent', () => ({
default: ({
dag,
activeTab,
editorHints,
}: {
dag: { name: string };
activeTab: string;
editorHints?: { inheritedCustomStepTypes?: unknown[] };
}) => (
<div>
<div>
Previewing {dag.name} [{activeTab}]
</div>
<div>Inherited hints: {editorHints?.inheritedCustomStepTypes?.length ?? 0}</div>
</div>
),
}));

const appBarValue = {
title: 'DAGs',
setTitle: vi.fn(),
remoteNodes: ['local'],
setRemoteNodes: vi.fn(),
selectedRemoteNode: 'local',
selectRemoteNode: vi.fn(),
};

const liveState = {
data: null,
error: null,
isConnected: false,
isConnecting: false,
shouldUseFallback: true,
};

const useQueryMock = useQuery as unknown as {
mockImplementation: (fn: (path: string, init?: unknown) => unknown) => void;
};

function renderPanel() {
return render(
<MemoryRouter>
<PageContextProvider>
<AppBarContext.Provider value={appBarValue}>
<DAGDetailsPanel
fileName="example"
onClose={vi.fn()}
/>
</AppBarContext.Provider>
</PageContextProvider>
</MemoryRouter>
);
}

afterEach(() => {
vi.clearAllMocks();
});

describe('DAGDetailsPanel', () => {
it('passes editor hints through to the dag list detail panel spec flow', () => {
vi.mocked(useDAGSSE).mockReturnValue(liveState);
useQueryMock.mockImplementation((path) => {
if (path === '/dags/{fileName}') {
return {
data: {
dag: { name: 'example-dag' },
filePath: '/tmp/example.yaml',
latestDAGRun: undefined,
localDags: [],
editorHints: {
inheritedCustomStepTypes: [{ name: 'greet' }],
},
},
error: undefined,
mutate: vi.fn(),
} as never;
}

return {
data: undefined,
error: undefined,
mutate: vi.fn(),
} as never;
});

renderPanel();

expect(screen.getByText('Previewing example-dag [status]')).toBeInTheDocument();
expect(screen.getByText('Inherited hints: 1')).toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,14 @@ vi.mock('../DAGDetailsContent', () => ({
dag,
activeTab,
dagRunId,
editorHints,
forceEnqueue,
onEnqueue,
}: {
dag: { name: string };
activeTab: string;
dagRunId?: string;
editorHints?: { inheritedCustomStepTypes?: unknown[] };
forceEnqueue?: boolean;
onEnqueue?: (
params: string,
Expand All @@ -51,6 +53,7 @@ vi.mock('../DAGDetailsContent', () => ({
Previewing {dag.name} [{activeTab}]{' '}
{forceEnqueue ? 'forced' : 'default'} {dagRunId || 'latest'}
</div>
<div>Inherited hints: {editorHints?.inheritedCustomStepTypes?.length ?? 0}</div>
{onEnqueue ? (
<button
type="button"
Expand Down Expand Up @@ -230,6 +233,36 @@ describe('DAGDetailsSidePanel', () => {
).toBeInTheDocument();
});

it('passes editor hints through to the modal DAG spec flow', () => {
vi.mocked(useDAGSSE).mockReturnValue(liveState);
vi.mocked(useDAGRunSSE).mockReturnValue(liveState);
useQueryMock.mockImplementation((path) => {
if (path === '/dags/{fileName}') {
return {
data: {
dag: { name: 'example-dag' },
filePath: '/tmp/example.yaml',
latestDAGRun: undefined,
localDags: [],
editorHints: {
inheritedCustomStepTypes: [{ name: 'greet' }],
},
},
error: undefined,
mutate: vi.fn(),
} as never;
}

return {
data: undefined,
} as never;
});

renderPanel();

expect(screen.getByText('Inherited hints: 1')).toBeInTheDocument();
});

it('tracks the returned dag run, switches to status, and revalidates after enqueue', async () => {
const mutate = vi.fn().mockResolvedValue(undefined);
const onEnqueue = vi.fn().mockResolvedValue('queued-run');
Expand Down
23 changes: 22 additions & 1 deletion ui/src/features/dags/components/dag-editor/DAGEditor.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
// Copyright (C) 2026 Yota Hamada
// SPDX-License-Identifier: GPL-3.0-or-later

/**
* DAGEditor component provides a Monaco editor for editing DAG YAML definitions.
*
* @module features/dags/components/dag-editor
*/
import type { JSONSchema } from '@/lib/schema-utils';
import { cn } from '@/lib/utils';
import MonacoEditor, { loader } from '@monaco-editor/react';
import * as monaco from 'monaco-editor';
Expand All @@ -11,7 +15,6 @@ import {
type JSONSchema as MonacoJSONSchema,
} from 'monaco-yaml';
import { useEffect, useRef } from 'react';
import type { JSONSchema } from '@/lib/schema-utils';

// Get schema URL from config (getConfig() is available at module load time)
declare function getConfig(): { basePath: string };
Expand Down Expand Up @@ -177,6 +180,23 @@ function DAGEditor({
const editorDidMount = (editor: monaco.editor.IStandaloneCodeEditor) => {
editorRef.current = editor;

if (!readOnly) {
editor.addAction({
id: 'dagu.triggerSuggest',
label: 'Trigger Autocomplete',
precondition:
'!editorReadonly && editorHasCompletionItemProvider && !suggestWidgetVisible',
keybindings: [
monaco.KeyMod.CtrlCmd | monaco.KeyCode.Space,
monaco.KeyMod.WinCtrl | monaco.KeyCode.Space,
],
keybindingContext: 'textInputFocus',
run: async (activeEditor) => {
await activeEditor.getAction('editor.action.triggerSuggest')?.run();
},
});
}

// Format document after a short delay
setTimeout(() => {
editor.getAction('editor.action.formatDocument')?.run();
Expand Down Expand Up @@ -236,6 +256,7 @@ function DAGEditor({
quickSuggestions: readOnly
? false
: { other: true, comments: false, strings: true },
suggestOnTriggerCharacters: !readOnly,
formatOnType: !readOnly,
formatOnPaste: !readOnly,
renderValidationDecorations: readOnly ? 'off' : 'on',
Expand Down
5 changes: 4 additions & 1 deletion ui/src/features/dags/components/dag-editor/DAGSpec.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
// Copyright (C) 2026 Yota Hamada
// SPDX-License-Identifier: GPL-3.0-or-later

/**
* DAGSpec component displays and allows editing of a DAG specification.
*
Expand Down Expand Up @@ -27,14 +30,14 @@ import LoadingIndicator from '../../../../ui/LoadingIndicator';
import { DAGContext } from '../../contexts/DAGContext';
import { DAGStepTable } from '../dag-details';
import { FlowchartType, Graph } from '../visualization';
import DAGAttributes from './DAGAttributes';
import {
buildAugmentedDAGSchema,
customStepTypeHintsEqual,
extractLocalCustomStepTypeHints,
mergeCustomStepTypeHints,
toInheritedCustomStepTypeHints,
} from './customStepSchema';
import DAGAttributes from './DAGAttributes';
import DAGEditorWithDocs from './DAGEditorWithDocs';
import ExternalChangeDialog from './ExternalChangeDialog';

Expand Down
Loading