Add max execution time for Workflows (#755)

* 🎉 basic setup and execution stopping

* 🚧 soft timeout for own process executions

* 🚧 add hard timeout for subprocesses

* 🚧 add soft timeout to main thread

* 🔧 set default timeout to 5 mins --> 500s

* 💡 adding documentation to configs

* 🚧 deactivate timeout by default

* 🚧 add logic of max execution timeout

*  adding timeout to settings in frontend and server

* 🎨 improve naming

* 💡 fix change in config docs

* ✔️ fixing compilation issue

* 🎨 add format for new config variables

* 👌 type cast before checking equality

*  Improve error message if NodeType is not known

* 🐳 Tag also rpi latest image

* 🐛 Fix Postgres issue with Node.js 14 #776

* 🚧 add toggle to activate workflow timeout

* 💄 improving UX of setting a timeout and its duration

Co-authored-by: Jan Oberhauser <jan.oberhauser@gmail.com>
This commit is contained in:
Ben Hesseldieck 2020-07-29 14:12:54 +02:00 committed by GitHub
parent 6e06da99fb
commit 051598d30e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 232 additions and 25 deletions

View file

@ -26,3 +26,7 @@ jobs:
if: success() if: success()
run: | run: |
docker buildx build --platform linux/arm/v7 --build-arg N8N_VERSION=${{steps.vars.outputs.tag}} -t n8nio/n8n:${{steps.vars.outputs.tag}}-rpi --output type=image,push=true docker/images/n8n-rpi docker buildx build --platform linux/arm/v7 --build-arg N8N_VERSION=${{steps.vars.outputs.tag}} -t n8nio/n8n:${{steps.vars.outputs.tag}}-rpi --output type=image,push=true docker/images/n8n-rpi
- name: Tag Docker image with latest
run: docker tag n8nio/n8n:${{steps.vars.outputs.tag}}-rpi n8nio/n8n:latest-rpi
- name: Push docker images of latest
run: docker push n8nio/n8n:latest-rpi

View file

@ -159,6 +159,30 @@ const config = convict({
env: 'EXECUTIONS_PROCESS' env: 'EXECUTIONS_PROCESS'
}, },
// A Workflow times out and gets canceled after this time (seconds).
// If the workflow is executed in the main process a soft timeout
// is executed (takes effect after the current node finishes).
// If a workflow is running in its own process is a soft timeout
// tried first, before killing the process after waiting for an
// additional fifth of the given timeout duration.
//
// To deactivate timeout set it to -1
//
// Timeout is currently not activated by default which will change
// in a future version.
timeout: {
doc: 'Max run time (seconds) before stopping the workflow execution',
format: Number,
default: -1,
env: 'EXECUTIONS_TIMEOUT'
},
maxTimeout: {
doc: 'Max execution time (seconds) that can be set for a workflow individually',
format: Number,
default: 3600,
env: 'EXECUTIONS_TIMEOUT_MAX'
},
// If a workflow executes all the data gets saved by default. This // If a workflow executes all the data gets saved by default. This
// could be a problem when a workflow gets executed a lot and processes // could be a problem when a workflow gets executed a lot and processes
// a lot of data. To not exceed the database's capacity it is possible to // a lot of data. To not exceed the database's capacity it is possible to

View file

@ -88,10 +88,11 @@ export class ActiveExecutions {
* Forces an execution to stop * Forces an execution to stop
* *
* @param {string} executionId The id of the execution to stop * @param {string} executionId The id of the execution to stop
* @param {string} timeout String 'timeout' given if stop due to timeout
* @returns {(Promise<IRun | undefined>)} * @returns {(Promise<IRun | undefined>)}
* @memberof ActiveExecutions * @memberof ActiveExecutions
*/ */
async stopExecution(executionId: string): Promise<IRun | undefined> { async stopExecution(executionId: string, timeout?: string): Promise<IRun | undefined> {
if (this.activeExecutions[executionId] === undefined) { if (this.activeExecutions[executionId] === undefined) {
// There is no execution running with that id // There is no execution running with that id
return; return;
@ -101,17 +102,17 @@ export class ActiveExecutions {
// returned that it gets then also resolved correctly. // returned that it gets then also resolved correctly.
if (this.activeExecutions[executionId].process !== undefined) { if (this.activeExecutions[executionId].process !== undefined) {
// Workflow is running in subprocess // Workflow is running in subprocess
setTimeout(() => {
if (this.activeExecutions[executionId].process!.connected) { if (this.activeExecutions[executionId].process!.connected) {
setTimeout(() => {
// execute on next event loop tick;
this.activeExecutions[executionId].process!.send({ this.activeExecutions[executionId].process!.send({
type: 'stopExecution' type: timeout ? timeout : 'stopExecution',
}); });
}, 1)
} }
}, 1);
} else { } else {
// Workflow is running in current process // Workflow is running in current process
this.activeExecutions[executionId].workflowExecution!.cancel('Canceled by user'); this.activeExecutions[executionId].workflowExecution!.cancel();
} }
return this.getPostExecutePromise(executionId); return this.getPostExecutePromise(executionId);

View file

@ -286,17 +286,17 @@ export interface IN8nUISettings {
saveDataErrorExecution: string; saveDataErrorExecution: string;
saveDataSuccessExecution: string; saveDataSuccessExecution: string;
saveManualExecutions: boolean; saveManualExecutions: boolean;
executionTimeout: number;
maxExecutionTimeout: number;
timezone: string; timezone: string;
urlBaseWebhook: string; urlBaseWebhook: string;
versionCli: string; versionCli: string;
} }
export interface IPackageVersions { export interface IPackageVersions {
cli: string; cli: string;
} }
export interface IPushData { export interface IPushData {
data: IPushDataExecutionFinished | IPushDataNodeExecuteAfter | IPushDataNodeExecuteBefore | IPushDataTestWebhook; data: IPushDataExecutionFinished | IPushDataNodeExecuteAfter | IPushDataNodeExecuteBefore | IPushDataTestWebhook;
type: IPushDataType; type: IPushDataType;
@ -304,7 +304,6 @@ export interface IPushData {
export type IPushDataType = 'executionFinished' | 'executionStarted' | 'nodeExecuteAfter' | 'nodeExecuteBefore' | 'testWebhookDeleted' | 'testWebhookReceived'; export type IPushDataType = 'executionFinished' | 'executionStarted' | 'nodeExecuteAfter' | 'nodeExecuteBefore' | 'testWebhookDeleted' | 'testWebhookReceived';
export interface IPushDataExecutionFinished { export interface IPushDataExecutionFinished {
data: IRun; data: IRun;
executionIdActive: string; executionIdActive: string;

View file

@ -29,6 +29,9 @@ class NodeTypesClass implements INodeTypes {
} }
getByName(nodeType: string): INodeType | undefined { getByName(nodeType: string): INodeType | undefined {
if (this.nodeTypes[nodeType] === undefined) {
throw new Error(`The node-type "${nodeType}" is not known!`);
}
return this.nodeTypes[nodeType].type; return this.nodeTypes[nodeType].type;
} }
} }

View file

@ -113,6 +113,8 @@ class App {
saveDataErrorExecution: string; saveDataErrorExecution: string;
saveDataSuccessExecution: string; saveDataSuccessExecution: string;
saveManualExecutions: boolean; saveManualExecutions: boolean;
executionTimeout: number;
maxExecutionTimeout: number;
timezone: string; timezone: string;
activeExecutionsInstance: ActiveExecutions.ActiveExecutions; activeExecutionsInstance: ActiveExecutions.ActiveExecutions;
push: Push.Push; push: Push.Push;
@ -133,6 +135,8 @@ class App {
this.saveDataErrorExecution = config.get('executions.saveDataOnError') as string; this.saveDataErrorExecution = config.get('executions.saveDataOnError') as string;
this.saveDataSuccessExecution = config.get('executions.saveDataOnSuccess') as string; this.saveDataSuccessExecution = config.get('executions.saveDataOnSuccess') as string;
this.saveManualExecutions = config.get('executions.saveDataManualExecutions') as boolean; this.saveManualExecutions = config.get('executions.saveDataManualExecutions') as boolean;
this.executionTimeout = config.get('executions.timeout') as number;
this.maxExecutionTimeout = config.get('executions.maxTimeout') as number;
this.timezone = config.get('generic.timezone') as string; this.timezone = config.get('generic.timezone') as string;
this.restEndpoint = config.get('endpoints.rest') as string; this.restEndpoint = config.get('endpoints.rest') as string;
@ -482,8 +486,11 @@ class App {
// Do not save when default got set // Do not save when default got set
delete newWorkflowData.settings.saveManualExecutions; delete newWorkflowData.settings.saveManualExecutions;
} }
if (parseInt(newWorkflowData.settings.executionTimeout as string) === this.executionTimeout) {
// Do not save when default got set
delete newWorkflowData.settings.executionTimeout
}
} }
newWorkflowData.updatedAt = this.getCurrentDate(); newWorkflowData.updatedAt = this.getCurrentDate();
@ -1534,6 +1541,8 @@ class App {
saveDataErrorExecution: this.saveDataErrorExecution, saveDataErrorExecution: this.saveDataErrorExecution,
saveDataSuccessExecution: this.saveDataSuccessExecution, saveDataSuccessExecution: this.saveDataSuccessExecution,
saveManualExecutions: this.saveManualExecutions, saveManualExecutions: this.saveManualExecutions,
executionTimeout: this.executionTimeout,
maxExecutionTimeout: this.maxExecutionTimeout,
timezone: this.timezone, timezone: this.timezone,
urlBaseWebhook: WebhookHelpers.getWebhookBaseUrl(), urlBaseWebhook: WebhookHelpers.getWebhookBaseUrl(),
versionCli: this.versions!.cli, versionCli: this.versions!.cli,

View file

@ -90,7 +90,6 @@ export class WorkflowRunner {
WorkflowExecuteAdditionalData.pushExecutionFinished(executionMode, fullRunData, executionId); WorkflowExecuteAdditionalData.pushExecutionFinished(executionMode, fullRunData, executionId);
} }
/** /**
* Run the workflow * Run the workflow
* *
@ -155,9 +154,27 @@ export class WorkflowRunner {
this.activeExecutions.attachWorkflowExecution(executionId, workflowExecution); this.activeExecutions.attachWorkflowExecution(executionId, workflowExecution);
// Soft timeout to stop workflow execution after current running node
let executionTimeout: NodeJS.Timeout;
let workflowTimeout = config.get('executions.timeout') as number > 0 && config.get('executions.timeout') as number; // initialize with default
if (data.workflowData.settings && data.workflowData.settings.executionTimeout) {
workflowTimeout = data.workflowData.settings!.executionTimeout as number > 0 && data.workflowData.settings!.executionTimeout as number // preference on workflow setting
}
if (workflowTimeout) {
const timeout = Math.min(workflowTimeout, config.get('executions.maxTimeout') as number) * 1000; // as seconds
executionTimeout = setTimeout(() => {
this.activeExecutions.stopExecution(executionId, 'timeout')
}, timeout)
}
workflowExecution.then((fullRunData) => { workflowExecution.then((fullRunData) => {
clearTimeout(executionTimeout);
if (workflowExecution.isCanceled) {
fullRunData.finished = false;
}
this.activeExecutions.remove(executionId, fullRunData); this.activeExecutions.remove(executionId, fullRunData);
}); })
return executionId; return executionId;
} }
@ -218,24 +235,54 @@ export class WorkflowRunner {
// Send all data to subprocess it needs to run the workflow // Send all data to subprocess it needs to run the workflow
subprocess.send({ type: 'startWorkflow', data } as IProcessMessage); subprocess.send({ type: 'startWorkflow', data } as IProcessMessage);
// Start timeout for the execution
let executionTimeout: NodeJS.Timeout;
let workflowTimeout = config.get('executions.timeout') as number > 0 && config.get('executions.timeout') as number; // initialize with default
if (data.workflowData.settings && data.workflowData.settings.executionTimeout) {
workflowTimeout = data.workflowData.settings!.executionTimeout as number > 0 && data.workflowData.settings!.executionTimeout as number // preference on workflow setting
}
if (workflowTimeout) {
const timeout = Math.min(workflowTimeout, config.get('executions.maxTimeout') as number) * 1000; // as seconds
executionTimeout = setTimeout(() => {
this.activeExecutions.stopExecution(executionId, 'timeout')
executionTimeout = setTimeout(() => subprocess.kill(), Math.max(timeout * 0.2, 5000)) // minimum 5 seconds
}, timeout)
}
// Listen to data from the subprocess // Listen to data from the subprocess
subprocess.on('message', (message: IProcessMessage) => { subprocess.on('message', (message: IProcessMessage) => {
if (message.type === 'end') { if (message.type === 'end') {
clearTimeout(executionTimeout);
this.activeExecutions.remove(executionId!, message.data.runData); this.activeExecutions.remove(executionId!, message.data.runData);
} else if (message.type === 'processError') { } else if (message.type === 'processError') {
clearTimeout(executionTimeout);
const executionError = message.data.executionError as IExecutionError; const executionError = message.data.executionError as IExecutionError;
this.processError(executionError, startedAt, data.executionMode, executionId); this.processError(executionError, startedAt, data.executionMode, executionId);
} else if (message.type === 'processHook') { } else if (message.type === 'processHook') {
this.processHookMessage(workflowHooks, message.data as IProcessMessageDataHook); this.processHookMessage(workflowHooks, message.data as IProcessMessageDataHook);
} else if (message.type === 'timeout') {
// Execution timed out and its process has been terminated
const timeoutError = { message: 'Workflow execution timed out!' } as IExecutionError;
this.processError(timeoutError, startedAt, data.executionMode, executionId);
} }
}); });
// Also get informed when the processes does exit especially when it did crash // Also get informed when the processes does exit especially when it did crash or timed out
subprocess.on('exit', (code, signal) => { subprocess.on('exit', (code, signal) => {
if (code !== 0) { if (signal === 'SIGTERM'){
// Execution timed out and its process has been terminated
const timeoutError = {
message: 'Workflow execution timed out!',
} as IExecutionError;
this.processError(timeoutError, startedAt, data.executionMode, executionId);
} else if (code !== 0) {
// Process did exit with error code, so something went wrong. // Process did exit with error code, so something went wrong.
const executionError = { const executionError = {
message: 'Workflow execution process did crash for an unknown reason!', message: 'Workflow execution process did crash for an unknown reason!',
@ -243,6 +290,7 @@ export class WorkflowRunner {
this.processError(executionError, startedAt, data.executionMode, executionId); this.processError(executionError, startedAt, data.executionMode, executionId);
} }
clearTimeout(executionTimeout);
}); });
return executionId; return executionId;

View file

@ -190,17 +190,18 @@ process.on('message', async (message: IProcessMessage) => {
// Once the workflow got executed make sure the process gets killed again // Once the workflow got executed make sure the process gets killed again
process.exit(); process.exit();
} else if (message.type === 'stopExecution') { } else if (message.type === 'stopExecution' || message.type === 'timeout') {
// The workflow execution should be stopped // The workflow execution should be stopped
let runData: IRun; let runData: IRun;
if (workflowRunner.workflowExecute !== undefined) { if (workflowRunner.workflowExecute !== undefined) {
// Workflow started already executing // Workflow started already executing
runData = workflowRunner.workflowExecute.getFullRunData(workflowRunner.startedAt); runData = workflowRunner.workflowExecute.getFullRunData(workflowRunner.startedAt);
// If there is any data send it to parent process const timeOutError = message.type === 'timeout' ? { message: 'Workflow execution timed out!' } as IExecutionError : undefined
await workflowRunner.workflowExecute.processSuccessExecution(workflowRunner.startedAt, workflowRunner.workflow!);
// If there is any data send it to parent process, if execution timedout add the error
await workflowRunner.workflowExecute.processSuccessExecution(workflowRunner.startedAt, workflowRunner.workflow!, timeOutError);
} else { } else {
// Workflow did not get started yet // Workflow did not get started yet
runData = { runData = {
@ -209,7 +210,7 @@ process.on('message', async (message: IProcessMessage) => {
runData: {}, runData: {},
}, },
}, },
finished: true, finished: message.type !== 'timeout',
mode: workflowRunner.data!.executionMode, mode: workflowRunner.data!.executionMode,
startedAt: workflowRunner.startedAt, startedAt: workflowRunner.startedAt,
stoppedAt: new Date(), stoppedAt: new Date(),
@ -218,7 +219,7 @@ process.on('message', async (message: IProcessMessage) => {
workflowRunner.sendHookToParentProcess('workflowExecuteAfter', [runData]); workflowRunner.sendHookToParentProcess('workflowExecuteAfter', [runData]);
} }
await sendToParentProcess('end', { await sendToParentProcess(message.type === 'timeout' ? message.type : 'end', {
runData, runData,
}); });

View file

@ -708,6 +708,9 @@ export class WorkflowExecute {
return Promise.resolve(); return Promise.resolve();
})() })()
.then(async () => { .then(async () => {
if (gotCancel && executionError === undefined) {
return this.processSuccessExecution(startedAt, workflow, { message: 'Workflow has been canceled!' } as IExecutionError);
}
return this.processSuccessExecution(startedAt, workflow, executionError); return this.processSuccessExecution(startedAt, workflow, executionError);
}) })
.catch(async (error) => { .catch(async (error) => {

View file

@ -397,6 +397,8 @@ export interface IN8nUISettings {
saveDataSuccessExecution: string; saveDataSuccessExecution: string;
saveManualExecutions: boolean; saveManualExecutions: boolean;
timezone: string; timezone: string;
executionTimeout: number;
maxExecutionTimeout: number;
urlBaseWebhook: string; urlBaseWebhook: string;
versionCli: string; versionCli: string;
} }
@ -407,4 +409,11 @@ export interface IWorkflowSettings extends IWorkflowSettingsWorkflow {
saveDataSuccessExecution?: string; saveDataSuccessExecution?: string;
saveManualExecutions?: boolean; saveManualExecutions?: boolean;
timezone?: string; timezone?: string;
executionTimeout?: number;
}
export interface ITimeoutHMS {
hours: number;
minutes: number;
seconds: number;
} }

View file

@ -1,6 +1,6 @@
<template> <template>
<span> <span>
<el-dialog class="workflow-settings" :visible="dialogVisible" append-to-body width="50%" title="Workflow Settings" :before-close="closeDialog"> <el-dialog class="workflow-settings" :visible="dialogVisible" append-to-body width="65%" title="Workflow Settings" :before-close="closeDialog">
<div v-loading="isLoading"> <div v-loading="isLoading">
<el-row> <el-row>
<el-col :span="10" class="setting-name"> <el-col :span="10" class="setting-name">
@ -97,6 +97,44 @@
</el-select> </el-select>
</el-col> </el-col>
</el-row> </el-row>
<el-row>
<el-col :span="10" class="setting-name">
Timeout Workflow:
<el-tooltip class="setting-info" placement="top" effect="light">
<div slot="content" v-html="helpTexts.executionTimeoutToggle"></div>
<font-awesome-icon icon="question-circle" />
</el-tooltip>
</el-col>
<el-col :span="14">
<div>
<el-switch ref="inputField" :value="workflowSettings.executionTimeout > -1" @change="toggleTimeout" active-color="#13ce66"></el-switch>
<div class="expression-info clickable" @click="expressionEditDialogVisible = true">Edit Expression</div>
</div>
</el-col>
</el-row>
<div v-if="workflowSettings.executionTimeout > -1">
<el-row>
<el-col :span="10" class="setting-name">
Timeout After:
<el-tooltip class="setting-info" placement="top" effect="light">
<div slot="content" v-html="helpTexts.executionTimeout"></div>
<font-awesome-icon icon="question-circle" />
</el-tooltip>
</el-col>
<el-col :span="4">
<el-input-number size="small" v-model="timeoutHMS.hours" :min="0" placeholder="hours" type="number" class="el-input_inner"></el-input-number></br>
<div class="timeout-setting-name">hours</div>
</el-col>
<el-col :span="4">
<el-input-number size="small" v-model="timeoutHMS.minutes" :min="0" placeholder="minutes" type="number" class="el-input_inner"></el-input-number></br>
<div class="timeout-setting-name">minutes</div>
</el-col>
<el-col :span="4">
<el-input-number size="small" v-model="timeoutHMS.seconds" :min="0" placeholder="seconds" type="number" class="el-input_inner"></el-input-number></br>
<div class="timeout-setting-name">seconds</div>
</el-col>
</el-row>
</div>
<div class="action-buttons"> <div class="action-buttons">
<el-button type="success" @click="saveSettings"> <el-button type="success" @click="saveSettings">
@ -115,6 +153,7 @@ import { restApi } from '@/components/mixins/restApi';
import { genericHelpers } from '@/components/mixins/genericHelpers'; import { genericHelpers } from '@/components/mixins/genericHelpers';
import { showMessage } from '@/components/mixins/showMessage'; import { showMessage } from '@/components/mixins/showMessage';
import { import {
ITimeoutHMS,
IWorkflowDataUpdate, IWorkflowDataUpdate,
IWorkflowSettings, IWorkflowSettings,
IWorkflowShortResponse, IWorkflowShortResponse,
@ -140,6 +179,8 @@ export default mixins(
saveDataErrorExecution: 'If data data of executions should be saved in case they failed.', saveDataErrorExecution: 'If data data of executions should be saved in case they failed.',
saveDataSuccessExecution: 'If data data of executions should be saved in case they succeed.', saveDataSuccessExecution: 'If data data of executions should be saved in case they succeed.',
saveManualExecutions: 'If data data of executions should be saved when started manually from the editor.', saveManualExecutions: 'If data data of executions should be saved when started manually from the editor.',
executionTimeoutToggle: 'Cancel workflow execution after defined time',
executionTimeout: 'After what time the workflow should timeout.',
}, },
defaultValues: { defaultValues: {
timezone: 'America/New_York', timezone: 'America/New_York',
@ -153,6 +194,9 @@ export default mixins(
timezones: [] as Array<{ key: string, value: string }>, timezones: [] as Array<{ key: string, value: string }>,
workflowSettings: {} as IWorkflowSettings, workflowSettings: {} as IWorkflowSettings,
workflows: [] as IWorkflowShortResponse[], workflows: [] as IWorkflowShortResponse[],
executionTimeout: this.$store.getters.executionTimeout,
maxExecutionTimeout: this.$store.getters.maxExecutionTimeout,
timeoutHMS: { hours: 0, minutes: 0, seconds: 0 } as ITimeoutHMS,
}; };
}, },
watch: { watch: {
@ -313,8 +357,15 @@ export default mixins(
if (workflowSettings.saveManualExecutions === undefined) { if (workflowSettings.saveManualExecutions === undefined) {
workflowSettings.saveManualExecutions = 'DEFAULT'; workflowSettings.saveManualExecutions = 'DEFAULT';
} }
if (workflowSettings.executionTimeout === undefined) {
workflowSettings.executionTimeout = this.$store.getters.executionTimeout;
}
if (workflowSettings.maxExecutionTimeout === undefined) {
workflowSettings.maxExecutionTimeout = this.$store.getters.maxExecutionTimeout;
}
Vue.set(this, 'workflowSettings', workflowSettings); Vue.set(this, 'workflowSettings', workflowSettings);
this.timeoutHMS = this.convertToHMS(workflowSettings.executionTimeout);
this.isLoading = false; this.isLoading = false;
}, },
async saveSettings () { async saveSettings () {
@ -323,6 +374,26 @@ export default mixins(
settings: this.workflowSettings, settings: this.workflowSettings,
}; };
// Convert hours, minutes, seconds into seconds for the workflow timeout
const { hours, minutes, seconds } = this.timeoutHMS;
data.settings!.executionTimeout =
data.settings!.executionTimeout !== -1
? hours * 3600 + minutes * 60 + seconds
: -1;
if (data.settings!.executionTimeout === 0) {
this.$showError(new Error('timeout is activated but set to 0'), 'Problem saving settings', 'There was a problem saving the settings:');
return;
}
// @ts-ignore
if (data.settings!.executionTimeout > this.workflowSettings.maxExecutionTimeout) {
const { hours, minutes, seconds } = this.convertToHMS(this.workflowSettings.maxExecutionTimeout as number);
this.$showError(new Error(`Maximum Timeout is: ${hours} hours, ${minutes} minutes, ${seconds} seconds`), 'Problem saving settings', 'Set timeout is exceeding the maximum timeout!');
return;
}
delete data.settings!.maxExecutionTimeout;
this.isLoading = true; this.isLoading = true;
try { try {
@ -353,6 +424,20 @@ export default mixins(
this.closeDialog(); this.closeDialog();
}, },
toggleTimeout() {
this.workflowSettings.executionTimeout = this.workflowSettings.executionTimeout === -1 ? 0 : -1;
},
convertToHMS(num: number): ITimeoutHMS {
if (num > 0) {
let remainder: number;
const hours = Math.floor(num / 3600);
remainder = num % 3600;
const minutes = Math.floor(remainder / 60);
const seconds = remainder % 60;
return { hours, minutes, seconds };
}
return { hours: 0, minutes: 0, seconds: 0 };
},
}, },
}); });
</script> </script>
@ -383,4 +468,9 @@ export default mixins(
} }
} }
.timeout-setting-name {
text-align: center;
width: calc(100% - 20px);
}
</style> </style>

View file

@ -52,6 +52,8 @@ export const store = new Vuex.Store({
saveDataSuccessExecution: 'all', saveDataSuccessExecution: 'all',
saveManualExecutions: false, saveManualExecutions: false,
timezone: 'America/New_York', timezone: 'America/New_York',
executionTimeout: -1,
maxExecutionTimeout: Number.MAX_SAFE_INTEGER,
versionCli: '0.0.0', versionCli: '0.0.0',
workflowExecutionData: null as IExecutionResponse | null, workflowExecutionData: null as IExecutionResponse | null,
lastSelectedNode: null as string | null, lastSelectedNode: null as string | null,
@ -479,6 +481,12 @@ export const store = new Vuex.Store({
setTimezone (state, timezone: string) { setTimezone (state, timezone: string) {
Vue.set(state, 'timezone', timezone); Vue.set(state, 'timezone', timezone);
}, },
setExecutionTimeout (state, executionTimeout: number) {
Vue.set(state, 'executionTimeout', executionTimeout);
},
setMaxExecutionTimeout (state, maxExecutionTimeout: number) {
Vue.set(state, 'maxExecutionTimeout', maxExecutionTimeout);
},
setVersionCli (state, version: string) { setVersionCli (state, version: string) {
Vue.set(state, 'versionCli', version); Vue.set(state, 'versionCli', version);
}, },
@ -592,6 +600,12 @@ export const store = new Vuex.Store({
timezone: (state): string => { timezone: (state): string => {
return state.timezone; return state.timezone;
}, },
executionTimeout: (state): number => {
return state.executionTimeout;
},
maxExecutionTimeout: (state): number => {
return state.maxExecutionTimeout;
},
versionCli: (state): string => { versionCli: (state): string => {
return state.versionCli; return state.versionCli;
}, },

View file

@ -1865,6 +1865,8 @@ export default mixins(
this.$store.commit('setSaveDataSuccessExecution', settings.saveDataSuccessExecution); this.$store.commit('setSaveDataSuccessExecution', settings.saveDataSuccessExecution);
this.$store.commit('setSaveManualExecutions', settings.saveManualExecutions); this.$store.commit('setSaveManualExecutions', settings.saveManualExecutions);
this.$store.commit('setTimezone', settings.timezone); this.$store.commit('setTimezone', settings.timezone);
this.$store.commit('setExecutionTimeout', settings.executionTimeout);
this.$store.commit('setMaxExecutionTimeout', settings.maxExecutionTimeout);
this.$store.commit('setVersionCli', settings.versionCli); this.$store.commit('setVersionCli', settings.versionCli);
}, },
async loadNodeTypes (): Promise<void> { async loadNodeTypes (): Promise<void> {