Skip to content

Commit 43363a7

Browse files
Allow users to import workflows from base templates (#3136)
* allow users to create workflows from base templates * update changelog * test selecting a different template * style panel buttons * top line * tailwind style * working * remove unused * add tests * run formatter * fix tests * remove extra BG --------- Co-authored-by: Taylor Downs <[email protected]>
1 parent 2ff8ae4 commit 43363a7

File tree

13 files changed

+609
-167
lines changed

13 files changed

+609
-167
lines changed

CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@ and this project adheres to
1919

2020
- AI Assistant: add metadata column to chat sessions
2121
[#3054](https://github.com/OpenFn/lightning/issues/3054)
22-
22+
- Allow users to create workflows from base templates
23+
[#3110](https://github.com/OpenFn/lightning/issues/3110)
24+
2325
### Changed
2426

2527
### Fixed

assets/js/hooks/FileDropzone.ts

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
interface FileDropzoneHook {
2+
el: HTMLElement;
3+
destroy?: () => void;
4+
mounted(): void;
5+
destroyed(): void;
6+
}
7+
8+
const FileDropzone = {
9+
mounted() {
10+
const dropzone = this.el;
11+
const targetSelector = dropzone.dataset['target'];
12+
if (!targetSelector) {
13+
console.error('No target selector specified');
14+
return;
15+
}
16+
17+
const fileInput = document.querySelector<HTMLInputElement>(targetSelector);
18+
19+
if (!fileInput) {
20+
console.error('Target file input not found:', targetSelector);
21+
return;
22+
}
23+
24+
const highlight = () => {
25+
dropzone.classList.add('border-indigo-600', 'bg-indigo-50/50');
26+
dropzone.classList.remove('border-gray-900/25');
27+
};
28+
29+
const unhighlight = () => {
30+
dropzone.classList.remove('border-indigo-600', 'bg-indigo-50/50');
31+
dropzone.classList.add('border-gray-900/25');
32+
};
33+
34+
const handleDrop = (e: DragEvent) => {
35+
e.preventDefault();
36+
e.stopPropagation();
37+
unhighlight();
38+
39+
const dt = e.dataTransfer;
40+
if (!dt?.files) return;
41+
42+
const files = dt.files;
43+
if (files.length > 0) {
44+
const file = files[0];
45+
if (!file) return;
46+
47+
// Check if file is yaml/yml
48+
if (file.name.match(/\.(ya?ml)$/i)) {
49+
// Create a new FileList-like object
50+
const dataTransfer = new DataTransfer();
51+
dataTransfer.items.add(file);
52+
fileInput.files = dataTransfer.files;
53+
54+
// Trigger change event to notify any listeners
55+
fileInput.dispatchEvent(new Event('change', { bubbles: true }));
56+
} else {
57+
console.error('Invalid file type. Please upload a YAML file.');
58+
}
59+
}
60+
};
61+
62+
const handleDragEvent = (e: DragEvent) => {
63+
e.preventDefault();
64+
e.stopPropagation();
65+
highlight();
66+
};
67+
68+
const handleDragLeave = (e: DragEvent) => {
69+
e.preventDefault();
70+
e.stopPropagation();
71+
unhighlight();
72+
};
73+
74+
dropzone.addEventListener('dragenter', handleDragEvent);
75+
dropzone.addEventListener('dragover', handleDragEvent);
76+
dropzone.addEventListener('dragleave', handleDragLeave);
77+
dropzone.addEventListener('drop', handleDrop);
78+
79+
// Cleanup
80+
this.destroy = () => {
81+
dropzone.removeEventListener('dragenter', handleDragEvent);
82+
dropzone.removeEventListener('dragover', handleDragEvent);
83+
dropzone.removeEventListener('dragleave', handleDragLeave);
84+
dropzone.removeEventListener('drop', handleDrop);
85+
};
86+
},
87+
88+
destroyed() {
89+
if (this.destroy) {
90+
this.destroy();
91+
}
92+
}
93+
} as FileDropzoneHook;
94+
95+
export default FileDropzone;

assets/js/hooks/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import type { PhoenixHook } from './PhoenixHook';
77
import LogLineHighlight from './LogLineHighlight';
88
import WorkflowToYAML from '../yaml/WorkflowToYAML';
99
import YAMLToWorkflow from '../yaml/YAMLToWorkflow';
10+
import TemplateToWorkflow from '../yaml/TemplateToWorkflow';
1011
import ElapsedIndicator from './ElapsedIndicator';
1112
import {
1213
TabbedContainer,
@@ -24,10 +25,13 @@ import {
2425
CloseNodePanelViaEscape,
2526
} from './KeyHandlers';
2627

28+
import FileDropzone from "./FileDropzone";
29+
2730
export {
2831
LogLineHighlight,
2932
WorkflowToYAML,
3033
YAMLToWorkflow,
34+
TemplateToWorkflow,
3135
ElapsedIndicator,
3236
TabbedContainer,
3337
TabbedSelector,
@@ -39,6 +43,7 @@ export {
3943
AltRunViaCtrlShiftEnter,
4044
CloseInspectorPanelViaEscape,
4145
CloseNodePanelViaEscape,
46+
FileDropzone,
4247
};
4348

4449
export { ReactComponent } from '#/react/hooks';

assets/js/workflow-editor/index.ts

Lines changed: 0 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,12 @@
11
// Hook for Workflow Editor Component
2-
import { DEFAULT_TEXT } from '../editor/Editor';
32
import type { PhoenixHook } from '../hooks/PhoenixHook';
43
import type { Lightning } from '../workflow-diagram/types';
5-
import { randomUUID } from '../common';
64
import type { mount } from './component';
75
import {
86
type Patch,
97
type PendingAction,
108
type WorkflowProps,
119
createWorkflowStore,
12-
type ChangeArgs,
1310
} from './store';
1411

1512
export type WorkflowEditorEntrypoint = PhoenixHook<
@@ -40,33 +37,6 @@ export type WorkflowEditorEntrypoint = PhoenixHook<
4037
{ baseUrl?: string | undefined }
4138
>;
4239

43-
const createNewWorkflow = (): Required<ChangeArgs> => {
44-
const triggers = [
45-
{
46-
id: randomUUID(),
47-
type: 'webhook' as 'webhook',
48-
},
49-
];
50-
const jobs = [
51-
{
52-
id: randomUUID(),
53-
name: 'New job',
54-
adaptor: '@openfn/language-common@latest',
55-
body: DEFAULT_TEXT,
56-
},
57-
];
58-
59-
const edges = [
60-
{
61-
id: randomUUID(),
62-
source_trigger_id: triggers[0].id,
63-
target_job_id: jobs[0].id,
64-
condition_type: 'always',
65-
},
66-
];
67-
return { triggers, jobs, edges };
68-
};
69-
7040
// To support temporary workflow editor metrics submissions to Lightning
7141
// server.
7242
let workflowLoadParamsStart: number | null = null;
@@ -249,12 +219,6 @@ export default {
249219
}) {
250220
this.workflowStore.setState(_state => payload);
251221

252-
if (!payload.triggers.length && !payload.jobs.length) {
253-
// Create a placeholder chart and push it back up to the server
254-
const diff = createNewWorkflow();
255-
this.workflowStore.getState().add(diff);
256-
}
257-
258222
this.maybeMountComponent();
259223
let end = new Date();
260224
console.debug('current-worflow-params processed', new Date().toISOString());

assets/js/yaml/TemplateToWorkflow.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import type { PhoenixHook } from '../hooks/PhoenixHook';
2+
import { parseWorkflowTemplate, convertWorkflowSpecToState } from './util';
3+
4+
const TemplateToWorkflow = {
5+
mounted() {
6+
this.handleEvent('template_selected', (payload: { template: string }) => {
7+
const workflowSpec = parseWorkflowTemplate(payload.template);
8+
const workflowState = convertWorkflowSpecToState(workflowSpec);
9+
this.pushEventTo(this.el, 'template-parsed', {
10+
workflow: workflowState,
11+
});
12+
});
13+
},
14+
} as PhoenixHook;
15+
16+
export default TemplateToWorkflow;

assets/js/yaml/YAMLToWorkflow.ts

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,6 @@
11
import type { PhoenixHook } from '../hooks/PhoenixHook';
22
import type { WorkflowSpec, WorkflowState } from './types';
3-
import {
4-
parseWorkflowYAML,
5-
convertWorkflowSpecToState,
6-
defaultWorkflowState,
7-
} from './util';
3+
import { parseWorkflowYAML, convertWorkflowSpecToState } from './util';
84

95
function transformServerErrors(
106
errors: Record<string, any>,
@@ -75,13 +71,6 @@ const YAMLToWorkflow = {
7571
this.validateYAML(yamlString);
7672
});
7773
},
78-
destroyed() {
79-
// reset server state
80-
this.updateServerState(defaultWorkflowState());
81-
},
82-
updateServerState(state: WorkflowState) {
83-
this.pushEvent('validate', { workflow: state });
84-
},
8574
validateYAML(workflowYAML: string) {
8675
this.errorEl.classList.add('hidden');
8776
this.errorEl.textContent = '';
@@ -100,8 +89,6 @@ const YAMLToWorkflow = {
10089
const errors = transformServerErrors(response['errors']);
10190
this.errorEl.textContent = errors[0];
10291
this.errorEl.classList.remove('hidden');
103-
} else {
104-
this.updateServerState(this.workflowState);
10592
}
10693
}
10794
);

assets/js/yaml/util.ts

Lines changed: 6 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -17,38 +17,6 @@ const hyphenate = (str: string) => {
1717
return str.replace(/\s+/g, '-');
1818
};
1919

20-
export const defaultWorkflowState = (): WorkflowState => {
21-
const trigger: StateTrigger = {
22-
id: randomUUID(),
23-
type: 'webhook',
24-
enabled: true,
25-
};
26-
27-
// intentionally left out adaptor because of the @latest stuff
28-
const job: StateJob = {
29-
id: randomUUID(),
30-
name: 'New Job',
31-
body: '// job body here',
32-
};
33-
34-
const edge: StateEdge = {
35-
id: randomUUID(),
36-
source_trigger_id: trigger.id,
37-
target_job_id: job.id,
38-
enabled: true,
39-
condition_type: 'always',
40-
};
41-
42-
const workflowState: WorkflowState = {
43-
name: '',
44-
jobs: [job],
45-
edges: [edge],
46-
triggers: [trigger],
47-
};
48-
49-
return workflowState;
50-
};
51-
5220
export const convertWorkflowStateToSpec = (
5321
workflowState: WorkflowState
5422
): WorkflowSpec => {
@@ -237,6 +205,12 @@ export const parseWorkflowYAML = (yamlString: string): WorkflowSpec => {
237205
return parsedYAML as WorkflowSpec;
238206
};
239207

208+
export const parseWorkflowTemplate = (code: string): WorkflowSpec => {
209+
const parsedYAML = YAML.parse(code);
210+
211+
return parsedYAML as WorkflowSpec;
212+
};
213+
240214
const humanizeAjvError = (error: ErrorObject): string => {
241215
switch (error.keyword) {
242216
case 'required':

lib/lightning_web/components/layout_components.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ defmodule LightningWeb.LayoutComponents do
9999
}
100100
}
101101
/>
102-
<div class="flex-none bg-white shadow-xs">
102+
<div class="flex-none bg-white shadow-xs border-b border-gray-200">
103103
<div class={[@title_class, @title_height]}>
104104
<%= if @current_user do %>
105105
<nav class="flex" aria-label="Breadcrumb">

lib/lightning_web/live/workflow_live/edit.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@ defmodule LightningWeb.WorkflowLive.Edit do
177177
:if={@show_new_workflow_panel}
178178
id="new-workflow-panel"
179179
module={LightningWeb.WorkflowLive.NewWorkflowComponent}
180-
workflow_form={@workflow_form}
180+
workflow={@workflow}
181181
project_id={@project.id}
182182
/>
183183
<div class="relative h-full flex grow" id={"workflow-edit-#{@workflow.id}"}>

0 commit comments

Comments
 (0)