Introduce FE external hooks (#1332)

*  Introduce FE external hooks

*  update hooks

*  add data from frontend settings to hooks

* re-organize and update

* cleanup

* 👌

*  cleanup workflowSave mixin, add events

* avoid alert on new workflow save as

*  update workflow active events

* rename externalhooks method

*  Rename frontend hooks

Co-authored-by: Jan Oberhauser <jan.oberhauser@gmail.com>
This commit is contained in:
Ahsan Virani 2021-01-19 23:48:30 +01:00 committed by GitHub
parent fd1f60bbbe
commit 4d446229c3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 85 additions and 113 deletions

View file

@ -301,6 +301,9 @@ export interface IN8nUISettings {
timezone: string;
urlBaseWebhook: string;
versionCli: string;
n8nMetadata?: {
[key: string]: string | number | undefined;
};
}
export interface IPackageVersions {

View file

@ -117,6 +117,10 @@ export interface INodeTypesMaxCount {
};
}
export interface IExternalHooks {
run(eventName: string, metadata?: IDataObject): Promise<void>;
}
export interface IRestApi {
getActiveWorkflows(): Promise<string[]>;
getActivationError(id: string): Promise<IActivationError | undefined >;
@ -406,6 +410,9 @@ export interface IN8nUISettings {
};
urlBaseWebhook: string;
versionCli: string;
n8nMetadata?: {
[key: string]: string | number | undefined;
};
}
export interface IWorkflowSettings extends IWorkflowSettingsWorkflow {

View file

@ -119,6 +119,7 @@
import Vue from 'vue';
import { copyPaste } from '@/components/mixins/copyPaste';
import { externalHooks } from '@/components/mixins/externalHooks';
import { restApi } from '@/components/mixins/restApi';
import { nodeHelpers } from '@/components/mixins/nodeHelpers';
import { showMessage } from '@/components/mixins/showMessage';
@ -147,6 +148,7 @@ import mixins from 'vue-typed-mixins';
export default mixins(
copyPaste,
externalHooks,
nodeHelpers,
restApi,
showMessage,
@ -336,6 +338,8 @@ export default mixins(
this.$emit('credentialsCreated', {data: result, options: { closeDialog }});
this.$externalHooks().run('credentials.create', { credentialTypeData: this.credentialTypeData });
return result;
},
async oAuthCredentialAuthorize () {

View file

@ -181,7 +181,6 @@ import { restApi } from '@/components/mixins/restApi';
import { showMessage } from '@/components/mixins/showMessage';
import { titleChange } from '@/components/mixins/titleChange';
import { workflowHelpers } from '@/components/mixins/workflowHelpers';
import { workflowSave } from '@/components/mixins/workflowSave';
import { workflowRun } from '@/components/mixins/workflowRun';
import { saveAs } from 'file-saver';
@ -195,7 +194,6 @@ export default mixins(
titleChange,
workflowHelpers,
workflowRun,
workflowSave,
)
.extend({
name: 'MainHeader',

View file

@ -22,6 +22,7 @@
<script lang="ts">
import { externalHooks } from '@/components/mixins/externalHooks';
import { genericHelpers } from '@/components/mixins/genericHelpers';
import { restApi } from '@/components/mixins/restApi';
import { showMessage } from '@/components/mixins/showMessage';
@ -33,6 +34,7 @@ import {
import mixins from 'vue-typed-mixins';
export default mixins(
externalHooks,
genericHelpers,
restApi,
showMessage,
@ -121,10 +123,12 @@ export default mixins(
}
const currentWorkflowId = this.$store.getters.workflowId;
let activationEventName = 'workflow.activeChange';
if (currentWorkflowId === this.workflowId) {
// If the status of the current workflow got changed
// commit it specifically
this.$store.commit('setActive', newActiveState);
activationEventName = 'workflow.activeChangeCurrent';
}
if (newActiveState === true) {
@ -133,6 +137,8 @@ export default mixins(
this.$store.commit('setWorkflowInactive', this.workflowId);
}
this.$externalHooks().run(activationEventName, { workflowId: this.workflowId, active: newActiveState });
this.$emit('workflowActiveChanged', { id: this.workflowId, active: newActiveState });
this.loading = false;
},

View file

@ -0,0 +1,39 @@
import { IExternalHooks } from '@/Interface';
import { IDataObject } from 'n8n-workflow';
import Vue from 'vue';
import { Store } from 'vuex';
export async function runExternalHook(
eventName: string,
store: Store<IDataObject>,
metadata?: IDataObject,
) {
// @ts-ignore
if (!window.n8nExternalHooks) {
return;
}
const [resource, operator] = eventName.split('.');
// @ts-ignore
if (window.n8nExternalHooks[resource] && window.n8nExternalHooks[resource][operator]) {
// @ts-ignore
const hookMethods = window.n8nExternalHooks[resource][operator];
for (const hookmethod of hookMethods) {
await hookmethod(store, metadata);
}
}
}
export const externalHooks = Vue.extend({
methods: {
$externalHooks(): IExternalHooks {
return {
run: async (eventName: string, metadata?: IDataObject): Promise<void> => {
await runExternalHook.call(this, eventName, this.$store, metadata);
},
};
},
},
});

View file

@ -27,6 +27,7 @@ import {
XYPositon,
} from '../../Interface';
import { externalHooks } from '@/components/mixins/externalHooks';
import { restApi } from '@/components/mixins/restApi';
import { nodeHelpers } from '@/components/mixins/nodeHelpers';
import { showMessage } from '@/components/mixins/showMessage';
@ -36,6 +37,7 @@ import { isEqual } from 'lodash';
import mixins from 'vue-typed-mixins';
export const workflowHelpers = mixins(
externalHooks,
nodeHelpers,
restApi,
showMessage,
@ -422,6 +424,7 @@ export const workflowHelpers = mixins(
this.$store.commit('setWorkflowId', workflowData.id);
this.$store.commit('setWorkflowName', {newName: workflowData.name, setStateDirty: false});
this.$store.commit('setWorkflowSettings', workflowData.settings || {});
this.$store.commit('setStateDirty', false);
} else {
// Workflow exists already so update it
await this.restApi().updateWorkflow(currentWorkflow, workflowData);
@ -441,6 +444,7 @@ export const workflowHelpers = mixins(
message: `The workflow "${workflowData.name}" got saved!`,
type: 'success',
});
this.$externalHooks().run('workflow.afterUpdate', { workflowData });
} catch (e) {
this.$store.commit('removeActiveAction', 'workflowSaving');

View file

@ -10,6 +10,7 @@ import {
NodeHelpers,
} from 'n8n-workflow';
import { externalHooks } from '@/components/mixins/externalHooks';
import { restApi } from '@/components/mixins/restApi';
import { workflowHelpers } from '@/components/mixins/workflowHelpers';
@ -17,6 +18,7 @@ import mixins from 'vue-typed-mixins';
import { titleChange } from './titleChange';
export const workflowRun = mixins(
externalHooks,
restApi,
workflowHelpers,
titleChange,
@ -82,6 +84,7 @@ export const workflowRun = mixins(
duration: 0,
});
this.$titleSet(workflow.name as string, 'ERROR');
this.$externalHooks().run('workflow.runError', { errorMessages });
return;
}
}

View file

@ -1,110 +0,0 @@
import {
IWorkflowData,
} from '../../Interface';
import { restApi } from '@/components/mixins/restApi';
import { nodeHelpers } from '@/components/mixins/nodeHelpers';
import { showMessage } from '@/components/mixins/showMessage';
import { workflowHelpers } from '@/components/mixins/workflowHelpers';
import mixins from 'vue-typed-mixins';
export const workflowSave = mixins(
nodeHelpers,
restApi,
showMessage,
workflowHelpers,
)
.extend({
methods: {
// Saves the currently loaded workflow to the database.
async saveCurrentWorkflow (withNewName = false) {
const currentWorkflow = this.$route.params.name;
let workflowName: string | null | undefined = '';
if (currentWorkflow === undefined || withNewName === true) {
// Currently no workflow name is set to get it from user
workflowName = await this.$prompt(
'Enter workflow name',
'Name',
{
confirmButtonText: 'Save',
cancelButtonText: 'Cancel',
},
)
.then((data) => {
// @ts-ignore
return data.value;
})
.catch(() => {
// User did cancel
return undefined;
});
if (workflowName === undefined) {
// User did cancel
return;
} else if (['', null].includes(workflowName)) {
// User did not enter a name
this.$showMessage({
title: 'Name missing',
message: `No name for the workflow got entered and could so not be saved!`,
type: 'error',
});
return;
}
}
try {
this.$store.commit('addActiveAction', 'workflowSaving');
let workflowData: IWorkflowData = await this.getWorkflowDataToSave();
if (currentWorkflow === undefined || withNewName === true) {
// Workflow is new or is supposed to get saved under a new name
// so create a new entry in database
workflowData.name = workflowName!.trim() as string;
if (withNewName === true) {
// If an existing workflow gets resaved with a new name
// make sure that the new ones is not active
workflowData.active = false;
}
workflowData = await this.restApi().createNewWorkflow(workflowData);
this.$store.commit('setActive', workflowData.active || false);
this.$store.commit('setWorkflowId', workflowData.id);
this.$store.commit('setWorkflowName', {newName: workflowData.name, setStateDirty: false});
this.$store.commit('setWorkflowSettings', workflowData.settings || {});
} else {
// Workflow exists already so update it
await this.restApi().updateWorkflow(currentWorkflow, workflowData);
}
// Set dirty = false before pushing route so unsaved changes message doesnt trigger.
this.$store.commit('setStateDirty', false);
if (this.$route.params.name !== workflowData.id) {
this.$router.push({
name: 'NodeViewExisting',
params: { name: workflowData.id as string, action: 'workflowSave' },
});
}
this.$store.commit('removeActiveAction', 'workflowSaving');
this.$showMessage({
title: 'Workflow saved',
message: `The workflow "${workflowData.name}" got saved!`,
type: 'success',
});
} catch (e) {
this.$store.commit('removeActiveAction', 'workflowSaving');
this.$showMessage({
title: 'Problem saving workflow',
message: `There was a problem saving the workflow: "${e.message}"`,
type: 'error',
});
}
},
},
});

View file

@ -17,6 +17,8 @@ import './n8n-theme.scss';
import App from '@/App.vue';
import router from './router';
import { runExternalHook } from './components/mixins/externalHooks';
import { library } from '@fortawesome/fontawesome-svg-core';
import {
faAngleDoubleLeft,
@ -172,6 +174,9 @@ library.add(faUsers);
Vue.component('font-awesome-icon', FontAwesomeIcon);
Vue.config.productionTip = false;
router.afterEach((to, from) => {
runExternalHook('main.routeChange', store, { from, to });
});
new Vue({
router,

View file

@ -58,6 +58,7 @@ export const store = new Vuex.Store({
maxExecutionTimeout: Number.MAX_SAFE_INTEGER,
versionCli: '0.0.0',
oauthCallbackUrls: {},
n8nMetadata: {},
workflowExecutionData: null as IExecutionResponse | null,
lastSelectedNode: null as string | null,
lastSelectedNodeOutputIndex: null as number | null,
@ -530,7 +531,9 @@ export const store = new Vuex.Store({
setOauthCallbackUrls(state, urls: IDataObject) {
Vue.set(state, 'oauthCallbackUrls', urls);
},
setN8nMetadata(state, metadata: IDataObject) {
Vue.set(state, 'n8nMetadata', metadata);
},
setActiveNode (state, nodeName: string) {
state.activeNode = nodeName;
},
@ -653,6 +656,9 @@ export const store = new Vuex.Store({
oauthCallbackUrls: (state): object => {
return state.oauthCallbackUrls;
},
n8nMetadata: (state): object => {
return state.n8nMetadata;
},
// Push Connection
pushConnectionActive: (state): boolean => {

View file

@ -114,6 +114,7 @@ import { MessageBoxInputData } from 'element-ui/types/message-box';
import { jsPlumb, Endpoint, OnConnectionBindInfo } from 'jsplumb';
import { NODE_NAME_PREFIX, PLACEHOLDER_EMPTY_WORKFLOW_ID } from '@/constants';
import { copyPaste } from '@/components/mixins/copyPaste';
import { externalHooks } from '@/components/mixins/externalHooks';
import { genericHelpers } from '@/components/mixins/genericHelpers';
import { mouseSelect } from '@/components/mixins/mouseSelect';
import { moveNodeWorkflow } from '@/components/mixins/moveNodeWorkflow';
@ -164,6 +165,7 @@ import {
export default mixins(
copyPaste,
externalHooks,
genericHelpers,
mouseSelect,
moveNodeWorkflow,
@ -375,6 +377,8 @@ export default mixins(
this.$store.commit('setStateDirty', false);
this.$externalHooks().run('workflow.open', { workflowId, workflowName: data.name });
return data;
},
touchTap (e: MouseEvent | TouchEvent) {
@ -1969,6 +1973,7 @@ export default mixins(
this.$store.commit('setMaxExecutionTimeout', settings.maxExecutionTimeout);
this.$store.commit('setVersionCli', settings.versionCli);
this.$store.commit('setOauthCallbackUrls', settings.oauthCallbackUrls);
this.$store.commit('setN8nMetadata', settings.n8nMetadata || {});
},
async loadNodeTypes (): Promise<void> {
const nodeTypes = await this.restApi().getNodeTypes();
@ -2033,6 +2038,8 @@ export default mixins(
}
this.stopLoading();
});
this.$externalHooks().run('nodeView.mount');
},
destroyed () {