Skip to content

Commit

Permalink
feat: Add ability to import/export workflows
Browse files Browse the repository at this point in the history
Closes #42
  • Loading branch information
Alorel committed Dec 15, 2022
1 parent 42381e5 commit 69b1451
Show file tree
Hide file tree
Showing 7 changed files with 165 additions and 3 deletions.
6 changes: 4 additions & 2 deletions src/lib/registries/workflow-registry.mts
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,10 @@ export default class WorkflowRegistry {
}

/** Add a new workflow to the list */
public add(workflow: Workflow): void {
this.setWorkflows([...this.workflows, workflow]);
public add(first: Workflow, ...rest: Workflow[]): void;

public add(...workflow: Workflow[]): void {
this.setWorkflows([...this.workflows, ...workflow]);
}

/** Overwrite the workflow at the given index */
Expand Down
48 changes: 48 additions & 0 deletions src/lib/util/export-modal.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import {Observable} from 'rxjs';
import type {SweetAlertOptions} from 'sweetalert2';

export function showExportModal(title: string, contents: any): void {
const html = document.createElement('textarea');
html.addEventListener('click', () => {
html.select();
});
html.textContent = JSON.stringify(contents);
html.readOnly = true;

Swal // eslint-disable-line @typescript-eslint/no-floating-promises
.fire({
confirmButtonText: 'OK',
html,
showCancelButton: false,
showConfirmButton: true,
title,
});
}

/** @return Pasted JSON */
export function showImportModal(
title: string,
validate: Exclude<SweetAlertOptions['inputValidator'], undefined>
): Observable<string> {
return new Observable<string>(subscriber => {
Swal
.fire({
cancelButtonText: 'Cancel',
confirmButtonText: 'Import',
input: 'textarea',
inputValidator: v => v ? validate(v) : 'Nothing typed in',
showCancelButton: true,
showConfirmButton: true,
title,
})
.then(rsp => {
if (rsp.isConfirmed) {
subscriber.next(rsp.value);
}
subscriber.complete();
})
.catch(e => {
subscriber.error(e);
});
});
}
2 changes: 2 additions & 0 deletions src/ui/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import SidenavIcon from './components/sidenav-icon';
import {useBorderClassHost, usePrimaryExecutionHost, useRunningHost} from './global-ctx';
import DebugPage from './pages/debug-page';
import HelpPage from './pages/help-page';
import ImportExportPage from './pages/import-export-page';
import NewWorkflow from './pages/new-workflow';
import WorkflowsDashboard from './pages/workflows-dashboard';

Expand All @@ -32,6 +33,7 @@ export default function App<T extends Element>({sidenavIcon}: AppProps<T>): VNod
<Fragment>
<NewWorkflow/>
<HelpPage/>
<ImportExportPage/>
<ProvideRunning>
<SidenavIcon container={sidenavIcon}/>
<ProvidePrimaryExecution>
Expand Down
2 changes: 1 addition & 1 deletion src/ui/components/btn.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,5 @@ export default function Btn({btnRef, kind, size, ...rest}: Props): VNode {
clazz += ` btn-${size}`;
}

return <button type={'button'} className={clazz} ref={btnRef} {...rest}></button>;
return <button type={'button'} class={clazz} ref={btnRef} {...rest}></button>;
}
93 changes: 93 additions & 0 deletions src/ui/pages/import-export-page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import type {VNode} from 'preact';
import {useCallback} from 'preact/hooks';
import {Workflow} from '../../lib/data/workflow.mjs';
import WorkflowRegistry from '../../lib/registries/workflow-registry.mjs';
import {EMPTY_ARR} from '../../lib/util.mjs';
import {showExportModal, showImportModal} from '../../lib/util/export-modal.mjs';
import {BlockDiv} from '../components/block';
import Btn from '../components/btn';
import PageContainer from '../components/page-container';
import autoId from '../util/id-gen.mjs';

export const IMPORT_PAGE_ID = autoId();
const btnSizeClass = 'sm btn-block';

export default function ImportExportPage(): VNode {
return (
<PageContainer id={IMPORT_PAGE_ID}>
<BlockDiv class={'pb-3'}>
<ExportAll/>
<ImportAll/>
<ImportOne/>
</BlockDiv>
</PageContainer>
);
}

function ImportAll(): VNode {
const onClick = useCallback(() => {
showImportModal('Import workflow', json => {
try {
const parsed = JSON.parse(json);
if (!Array.isArray(parsed)) {
return 'Expected a JSON array';
} else if (!parsed.length) {
return 'The array is empty';
}

for (let i = 0; i < parsed.length; ++i) {
if (!Workflow.fromJSON(parsed[i])) {
return `Invalid workflow at index ${i}`;
}
}

return null;
} catch (e) {
return e.message;
}
})
.subscribe(rsp => {
const parsed = (JSON.parse(rsp) as any[]).map(Workflow.fromJSON);
WorkflowRegistry.inst.add(...parsed as [Workflow, ...Workflow[]]);
alertDone();
});
}, EMPTY_ARR);

return <Btn kind={'info'} size={btnSizeClass} onClick={onClick}>Import workflows</Btn>;
}

function ImportOne(): VNode {
const onClick = useCallback(() => {
showImportModal('Import workflow', json => {
try {
return Workflow.fromJSON(JSON.parse(json)) ? null : 'Invalid workflow';
} catch (e) {
return e.message;
}
})
.subscribe(rsp => {
WorkflowRegistry.inst.add(Workflow.fromJSON(JSON.parse(rsp))!);
alertDone();
});
}, EMPTY_ARR);

return <Btn kind={'info'} size={btnSizeClass} onClick={onClick}>Import workflow</Btn>;
}

function ExportAll(): VNode {
const onClick = useCallback(() => {
showExportModal('Export workflows', WorkflowRegistry.inst.workflows);
}, EMPTY_ARR);

return <Btn kind={'primary'} size={btnSizeClass} onClick={onClick}>Export workflows</Btn>;
}

function alertDone() {
Swal // eslint-disable-line @typescript-eslint/no-floating-promises
.fire({
confirmButtonText: 'OK',
showCancelButton: false,
showConfirmButton: true,
text: 'Done!',
});
}
7 changes: 7 additions & 0 deletions src/ui/pages/workflows-dashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {Workflow} from '../../lib/data/workflow.mjs';
import WorkflowRegistry from '../../lib/registries/workflow-registry.mjs';
import {EMPTY_ARR} from '../../lib/util.mjs';
import {alertConfirm} from '../../lib/util/alert';
import {showExportModal} from '../../lib/util/export-modal.mjs';
import swapElements from '../../lib/util/swap-elements.mjs';
import {BorderedBlock} from '../components/block';
import Btn from '../components/btn';
Expand Down Expand Up @@ -260,11 +261,17 @@ function BtnsNotRunning({refresh}: SelectedWorkflowBtnsProps): VNode {
activeWorkflow.value = cloned;
}, [activeWorkflow]);

const doExport = useCallback(() => {
const wf = activeWorkflow.peek()!;
showExportModal(`Export ${wf.name}`, wf);
}, [activeWorkflow]);

return (
<div class={'btn-group btn-group-sm'}>
<Btn kind={'success'} onClick={run}>{'Run'}</Btn>
<Btn kind={'primary'} onClick={startEditing}>{'Edit'}</Btn>
<Btn kind={'primary'} onClick={clone}>{'Copy'}</Btn>
<Btn kind={'info'} onClick={doExport}>{'Export'}</Btn>
<Btn kind={'danger'} onClick={del}>{'Delete'}</Btn>
</div>
);
Expand Down
10 changes: 10 additions & 0 deletions src/ui/ui.mts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type {Obj} from '../public_api';
import './assets/styles.scss';
import {DEBUG_PAGE_ID} from './pages/debug-page';
import {HELP_PAGE_ID} from './pages/help-page';
import {IMPORT_PAGE_ID} from './pages/import-export-page';
import {NEW_WORKFLOW_PAGE_ID} from './pages/new-workflow';
import {WORKFLOWS_DASHBOARD_ID} from './pages/workflows-dashboard';

Expand Down Expand Up @@ -51,6 +52,15 @@ interface BaseCat {
cat({name: 'New Action Workflow'}),
],
},
{
...pageCommon,
containerID: IMPORT_PAGE_ID,
customName: heading('Import/export'),
id: 'importExportWorkflows',
sidebarSubItems: [
cat({name: 'Import/export'}),
],
},
{
...pageCommon,
containerID: HELP_PAGE_ID,
Expand Down

0 comments on commit 69b1451

Please sign in to comment.