mirror of
https://github.com/n8n-io/n8n.git
synced 2024-11-10 06:34:05 -08:00
✨ Push active executions to clients to remove manual reload
This commit is contained in:
parent
a9453806b8
commit
2d8038669a
|
@ -153,7 +153,8 @@ export interface IExecutionsStopData {
|
|||
}
|
||||
|
||||
export interface IExecutionsSummary {
|
||||
id: string;
|
||||
id?: string; // executionIdDb
|
||||
idActive?: string; // executionIdActive
|
||||
mode: WorkflowExecuteMode;
|
||||
finished?: boolean;
|
||||
retryOf?: string;
|
||||
|
@ -238,14 +239,23 @@ export interface IPushData {
|
|||
type: IPushDataType;
|
||||
}
|
||||
|
||||
export type IPushDataType = 'executionFinished' | 'nodeExecuteAfter' | 'nodeExecuteBefore' | 'testWebhookDeleted' | 'testWebhookReceived';
|
||||
export type IPushDataType = 'executionFinished' | 'executionStarted' | 'nodeExecuteAfter' | 'nodeExecuteBefore' | 'testWebhookDeleted' | 'testWebhookReceived';
|
||||
|
||||
|
||||
export interface IPushDataExecutionFinished {
|
||||
data: IRun;
|
||||
executionId: string;
|
||||
executionIdActive: string;
|
||||
executionIdDb?: string;
|
||||
}
|
||||
|
||||
export interface IPushDataExecutionStarted {
|
||||
executionId: string;
|
||||
mode: WorkflowExecuteMode;
|
||||
startedAt: Date;
|
||||
retryOf?: string;
|
||||
workflowId: string;
|
||||
workflowName?: string;
|
||||
}
|
||||
|
||||
export interface IPushDataNodeExecuteAfter {
|
||||
data: ITaskData;
|
||||
|
|
|
@ -59,8 +59,11 @@ export class Push {
|
|||
* @param {*} data
|
||||
* @memberof Push
|
||||
*/
|
||||
send(sessionId: string, type: IPushDataType, data: any) { // tslint:disable-line:no-any
|
||||
if (this.connections[sessionId] === undefined) {
|
||||
|
||||
|
||||
|
||||
send(type: IPushDataType, data: any, sessionId?: string) { // tslint:disable-line:no-any
|
||||
if (sessionId !== undefined && this.connections[sessionId] === undefined) {
|
||||
// TODO: Log that properly!
|
||||
console.error(`The session "${sessionId}" is not registred.`);
|
||||
return;
|
||||
|
@ -71,8 +74,15 @@ export class Push {
|
|||
data,
|
||||
};
|
||||
|
||||
if (sessionId === undefined) {
|
||||
// Send to all connected clients
|
||||
this.channel.send(JSON.stringify(sendData));
|
||||
} else {
|
||||
// Send only to a specific client
|
||||
this.channel.send(JSON.stringify(sendData), [this.connections[sessionId]]);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
let activePushInstance: Push | undefined;
|
||||
|
|
|
@ -848,7 +848,7 @@ class App {
|
|||
}
|
||||
returnData.push(
|
||||
{
|
||||
id: data.id.toString(),
|
||||
idActive: data.id.toString(),
|
||||
workflowId: data.workflowId,
|
||||
mode:data.mode,
|
||||
startedAt: new Date(data.startedAt),
|
||||
|
|
|
@ -91,7 +91,7 @@ export class TestWebhooks {
|
|||
|
||||
// Inform editor-ui that webhook got received
|
||||
if (this.testWebhookData[webhookKey].sessionId !== undefined) {
|
||||
pushInstance.send(this.testWebhookData[webhookKey].sessionId!, 'testWebhookReceived', { workflowId: webhookData.workflow.id });
|
||||
pushInstance.send('testWebhookReceived', { workflowId: webhookData.workflow.id }, this.testWebhookData[webhookKey].sessionId!);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
|
@ -167,7 +167,7 @@ export class TestWebhooks {
|
|||
// Inform editor-ui that webhook got received
|
||||
if (this.testWebhookData[webhookKey].sessionId !== undefined) {
|
||||
try {
|
||||
pushInstance.send(this.testWebhookData[webhookKey].sessionId!, 'testWebhookDeleted', { workflowId });
|
||||
pushInstance.send('testWebhookDeleted', { workflowId }, this.testWebhookData[webhookKey].sessionId!);
|
||||
} catch (error) {
|
||||
// Could not inform editor, probably is not connected anymore. So sipmly go on.
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ import {
|
|||
IExecutionDb,
|
||||
IExecutionFlattedDb,
|
||||
IPushDataExecutionFinished,
|
||||
IPushDataExecutionStarted,
|
||||
IPushDataNodeExecuteAfter,
|
||||
IPushDataNodeExecuteBefore,
|
||||
IWorkflowBase,
|
||||
|
@ -60,41 +61,14 @@ function executeErrorWorkflow(workflowData: IWorkflowBase, fullRunData: IRun, mo
|
|||
}
|
||||
|
||||
|
||||
const hooks = (mode: WorkflowExecuteMode, workflowData: IWorkflowBase, workflowInstance: Workflow, sessionId?: string, retryOf?: string) => {
|
||||
return {
|
||||
nodeExecuteBefore: [
|
||||
async (executionId: string, nodeName: string): Promise<void> => {
|
||||
if (sessionId === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sendData: IPushDataNodeExecuteBefore = {
|
||||
executionId,
|
||||
nodeName,
|
||||
};
|
||||
|
||||
pushInstance.send(sessionId, 'nodeExecuteBefore', sendData);
|
||||
},
|
||||
],
|
||||
nodeExecuteAfter: [
|
||||
async (executionId: string, nodeName: string, data: ITaskData): Promise<void> => {
|
||||
if (sessionId === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sendData: IPushDataNodeExecuteAfter = {
|
||||
executionId,
|
||||
nodeName,
|
||||
data,
|
||||
};
|
||||
|
||||
pushInstance.send(sessionId, 'nodeExecuteAfter', sendData);
|
||||
},
|
||||
],
|
||||
workflowExecuteAfter: [
|
||||
async (fullRunData: IRun, executionId: string): Promise<void> => {
|
||||
try {
|
||||
if (sessionId !== undefined) {
|
||||
/**
|
||||
* Pushes the execution out to all connected clients
|
||||
*
|
||||
* @param {IRun} fullRunData The RunData of the finished execution
|
||||
* @param {string} executionIdActive The id of the finished execution
|
||||
* @param {string} [executionIdDb] The database id of finished execution
|
||||
*/
|
||||
function pushExecutionFinished(fullRunData: IRun, executionIdActive: string, executionIdDb?: string) {
|
||||
// Clone the object except the runData. That one is not supposed
|
||||
// to be send. Because that data got send piece by piece after
|
||||
// each node which finished executing
|
||||
|
@ -111,13 +85,65 @@ const hooks = (mode: WorkflowExecuteMode, workflowData: IWorkflowBase, workflowI
|
|||
|
||||
// Push data to editor-ui once workflow finished
|
||||
const sendData: IPushDataExecutionFinished = {
|
||||
executionId,
|
||||
executionIdActive,
|
||||
executionIdDb,
|
||||
data: pushRunData,
|
||||
};
|
||||
|
||||
pushInstance.send(sessionId, 'executionFinished', sendData);
|
||||
pushInstance.send('executionFinished', sendData);
|
||||
}
|
||||
|
||||
|
||||
const hooks = (mode: WorkflowExecuteMode, workflowData: IWorkflowBase, workflowInstance: Workflow, sessionId?: string, retryOf?: string) => {
|
||||
return {
|
||||
nodeExecuteBefore: [
|
||||
async (executionId: string, nodeName: string): Promise<void> => {
|
||||
if (sessionId === undefined) {
|
||||
// Only push data to the session which started it
|
||||
return;
|
||||
}
|
||||
|
||||
const sendData: IPushDataNodeExecuteBefore = {
|
||||
executionId,
|
||||
nodeName,
|
||||
};
|
||||
|
||||
pushInstance.send('nodeExecuteBefore', sendData, sessionId);
|
||||
},
|
||||
],
|
||||
nodeExecuteAfter: [
|
||||
async (executionId: string, nodeName: string, data: ITaskData): Promise<void> => {
|
||||
if (sessionId === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sendData: IPushDataNodeExecuteAfter = {
|
||||
executionId,
|
||||
nodeName,
|
||||
data,
|
||||
};
|
||||
|
||||
pushInstance.send('nodeExecuteAfter', sendData, sessionId);
|
||||
},
|
||||
],
|
||||
workflowExecuteBefore: [
|
||||
async (executionId: string): Promise<void> => {
|
||||
// Push data to editor-ui once workflow finished
|
||||
const sendData: IPushDataExecutionStarted = {
|
||||
executionId,
|
||||
mode,
|
||||
startedAt: new Date(),
|
||||
retryOf,
|
||||
workflowId: workflowData.id as string,
|
||||
workflowName: workflowData.name,
|
||||
};
|
||||
|
||||
pushInstance.send('executionStarted', sendData);
|
||||
}
|
||||
],
|
||||
workflowExecuteAfter: [
|
||||
async (fullRunData: IRun, executionId: string): Promise<void> => {
|
||||
try {
|
||||
const workflowSavePromise = WorkflowHelpers.saveStaticData(workflowInstance);
|
||||
|
||||
let saveManualExecutions = config.get('executions.saveDataManualExecutions') as boolean;
|
||||
|
@ -132,6 +158,7 @@ const hooks = (mode: WorkflowExecuteMode, workflowData: IWorkflowBase, workflowI
|
|||
await workflowSavePromise;
|
||||
}
|
||||
|
||||
pushExecutionFinished(fullRunData, executionId);
|
||||
executeErrorWorkflow(workflowData, fullRunData, mode);
|
||||
return;
|
||||
}
|
||||
|
@ -148,6 +175,7 @@ const hooks = (mode: WorkflowExecuteMode, workflowData: IWorkflowBase, workflowI
|
|||
if (workflowDidSucceed === true && saveDataSuccessExecution === 'none' ||
|
||||
workflowDidSucceed === false && saveDataErrorExecution === 'none'
|
||||
) {
|
||||
pushExecutionFinished(fullRunData, executionId);
|
||||
executeErrorWorkflow(workflowData, fullRunData, mode);
|
||||
return;
|
||||
}
|
||||
|
@ -185,8 +213,10 @@ const hooks = (mode: WorkflowExecuteMode, workflowData: IWorkflowBase, workflowI
|
|||
await workflowSavePromise;
|
||||
}
|
||||
|
||||
pushExecutionFinished(fullRunData, executionId, executionResult.id as string);
|
||||
executeErrorWorkflow(workflowData, fullRunData, mode, executionResult ? executionResult.id as string : undefined);
|
||||
} catch (error) {
|
||||
pushExecutionFinished(fullRunData, executionId);
|
||||
executeErrorWorkflow(workflowData, fullRunData, mode);
|
||||
}
|
||||
},
|
||||
|
|
|
@ -294,7 +294,8 @@ export interface IExecutionsListResponse {
|
|||
}
|
||||
|
||||
export interface IExecutionsCurrentSummaryExtended {
|
||||
id: string;
|
||||
id?: string;
|
||||
idActive: string;
|
||||
finished?: boolean;
|
||||
mode: WorkflowExecuteMode;
|
||||
startedAt: Date;
|
||||
|
@ -311,7 +312,8 @@ export interface IExecutionsStopData {
|
|||
}
|
||||
|
||||
export interface IExecutionsSummary {
|
||||
id: string;
|
||||
id?: string; // executionIdDb
|
||||
idActive?: string; // executionIdActive
|
||||
mode: WorkflowExecuteMode;
|
||||
finished?: boolean;
|
||||
retryOf?: string;
|
||||
|
@ -333,10 +335,25 @@ export interface IPushData {
|
|||
type: IPushDataType;
|
||||
}
|
||||
|
||||
export type IPushDataType = 'executionFinished' | 'nodeExecuteAfter' | 'nodeExecuteBefore' | 'testWebhookDeleted' | 'testWebhookReceived';
|
||||
export type IPushDataType = 'executionFinished' | 'executionStarted' | 'nodeExecuteAfter' | 'nodeExecuteBefore' | 'testWebhookDeleted' | 'testWebhookReceived';
|
||||
|
||||
export interface IPushDataExecutionStarted {
|
||||
executionId: string;
|
||||
mode: WorkflowExecuteMode;
|
||||
startedAt: Date;
|
||||
retryOf?: string;
|
||||
workflowId: string;
|
||||
workflowName?: string;
|
||||
}
|
||||
|
||||
|
||||
export interface IPushDataExecutionFinished {
|
||||
data: IRun;
|
||||
executionIdActive: string;
|
||||
executionIdDb?: string;
|
||||
}
|
||||
|
||||
export interface IPushDataExecutionStarted {
|
||||
executionId: string;
|
||||
}
|
||||
|
||||
|
|
64
packages/editor-ui/src/components/ExecutionTime.vue
Normal file
64
packages/editor-ui/src/components/ExecutionTime.vue
Normal file
|
@ -0,0 +1,64 @@
|
|||
<template>
|
||||
<span>
|
||||
{{time}}
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
|
||||
import Vue from 'vue';
|
||||
|
||||
import { genericHelpers } from '@/components/mixins/genericHelpers';
|
||||
|
||||
import mixins from 'vue-typed-mixins';
|
||||
|
||||
export default mixins(
|
||||
genericHelpers,
|
||||
)
|
||||
.extend({
|
||||
name: 'ExecutionTime',
|
||||
props: [
|
||||
'startTime',
|
||||
],
|
||||
computed: {
|
||||
time (): string {
|
||||
if (!this.startTime) {
|
||||
return '...';
|
||||
}
|
||||
const msPassed = this.nowTime - new Date(this.startTime).getTime();
|
||||
return this.displayTimer(msPassed);
|
||||
},
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
nowTime: -1,
|
||||
intervalTimer: null as null | NodeJS.Timeout,
|
||||
};
|
||||
},
|
||||
mounted () {
|
||||
this.setNow();
|
||||
this.intervalTimer = setInterval(() => {
|
||||
this.setNow();
|
||||
}, 1000);
|
||||
},
|
||||
destroyed () {
|
||||
// Make sure that the timer gets destroyed once no longer needed
|
||||
if (this.intervalTimer !== null) {
|
||||
clearInterval(this.intervalTimer);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
setNow () {
|
||||
this.nowTime = (new Date()).getTime();
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
// .data-display-wrapper {
|
||||
|
||||
// }
|
||||
|
||||
</style>
|
|
@ -16,25 +16,7 @@
|
|||
</el-option>
|
||||
</el-select>
|
||||
</el-col>
|
||||
<el-col :span="3">
|
||||
</el-col>
|
||||
<el-col :span="3" class="filter-headline">
|
||||
Auto-Refresh:
|
||||
</el-col>
|
||||
<el-col :span="4">
|
||||
<el-select v-model="autoRefresh.time" placeholder="Select Refresh Time" size="small" filterable @change="handleRefreshTimeChanged">
|
||||
<el-option
|
||||
v-for="item in autoRefresh.options"
|
||||
:key="item.value"
|
||||
:label="item.name"
|
||||
:value="item.value">
|
||||
</el-option>
|
||||
</el-select>
|
||||
</el-col>
|
||||
<el-col :span="4">
|
||||
<el-button title="Refresh" @click="refreshData()" :disabled="isDataLoading" size="small" type="success" class="refresh-button">
|
||||
<font-awesome-icon icon="sync" /> Manual Refresh
|
||||
</el-button>
|
||||
<el-col :span="14">
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
|
@ -46,26 +28,27 @@
|
|||
</span>
|
||||
</div>
|
||||
|
||||
<el-table :data="combinedExecutions" stripe v-loading="isDataLoading" :row-class-name="getRowClass" @row-click="handleRowClick">
|
||||
<el-table :data="combinedExecutions" stripe v-loading="isDataLoading" :row-class-name="getRowClass">
|
||||
<el-table-column label="" width="30">
|
||||
<!-- eslint-disable-next-line vue/no-unused-vars -->
|
||||
<template slot="header" slot-scope="scope">
|
||||
<el-checkbox :indeterminate="isIndeterminate" v-model="checkAll" @change="handleCheckAllChange">Check all</el-checkbox>
|
||||
</template>
|
||||
<template slot-scope="scope">
|
||||
<el-checkbox v-if="scope.row.stoppedAt !== undefined" :value="selectedItems[scope.row.id.toString()] || checkAll" @change="handleCheckboxChanged(scope.row.id)" >Check all</el-checkbox>
|
||||
<el-checkbox v-if="scope.row.stoppedAt !== undefined && scope.row.id" :value="selectedItems[scope.row.id.toString()] || checkAll" @change="handleCheckboxChanged(scope.row.id)" >Check all</el-checkbox>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column property="startedAt" label="Started At / ID" width="205">
|
||||
<template slot-scope="scope">
|
||||
{{convertToDisplayDate(scope.row.startedAt)}}<br />
|
||||
<small>ID: {{scope.row.id}}</small>
|
||||
<small v-if="scope.row.id">ID: {{scope.row.id}}</small>
|
||||
<small v-if="scope.row.idActive && scope.row.id === undefined && scope.row.stoppedAt === undefined">Active-ID: {{scope.row.idActive}}</small>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column property="workflowName" label="Name">
|
||||
<template slot-scope="scope">
|
||||
<span class="workflow-name">
|
||||
{{scope.row.workflowName}}
|
||||
{{scope.row.workflowName || '[UNSAVED WORKFLOW]'}}
|
||||
</span>
|
||||
|
||||
<span v-if="scope.row.stoppedAt === undefined">
|
||||
|
@ -107,21 +90,21 @@
|
|||
<template slot-scope="scope">
|
||||
<span v-if="scope.row.stoppedAt === undefined">
|
||||
<font-awesome-icon icon="spinner" spin />
|
||||
{{(new Date().getTime() - new Date(scope.row.startedAt).getTime())/1000}} sec.
|
||||
<execution-time :start-time="scope.row.startedAt"/>
|
||||
</span>
|
||||
<span v-else>
|
||||
{{(new Date(scope.row.stoppedAt).getTime() - new Date(scope.row.startedAt).getTime()) / 1000}} sec.
|
||||
{{ displayTimer(new Date(scope.row.stoppedAt).getTime() - new Date(scope.row.startedAt).getTime(), true) }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="" width="100" align="center">
|
||||
<template slot-scope="scope">
|
||||
<span v-if="scope.row.stoppedAt === undefined">
|
||||
<el-button circle title="Stop Execution" @click.stop="stopExecution(scope.row.id)" :loading="stoppingExecutions.includes(scope.row.id)" size="mini">
|
||||
<span v-if="scope.row.stoppedAt === undefined && scope.row.idActive">
|
||||
<el-button circle title="Stop Execution" @click.stop="stopExecution(scope.row.idActive)" :loading="stoppingExecutions.includes(scope.row.idActive)" size="mini">
|
||||
<font-awesome-icon icon="stop" />
|
||||
</el-button>
|
||||
</span>
|
||||
<span v-else>
|
||||
<span v-else-if="scope.row.id">
|
||||
<el-button circle title="Open Past Execution" @click.stop="displayExecution(scope.row)" size="mini">
|
||||
<font-awesome-icon icon="folder-open" />
|
||||
</el-button>
|
||||
|
@ -143,6 +126,7 @@
|
|||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
|
||||
import ExecutionTime from '@/components/ExecutionTime.vue';
|
||||
import WorkflowActivator from '@/components/WorkflowActivator.vue';
|
||||
|
||||
import { restApi } from '@/components/mixins/restApi';
|
||||
|
@ -174,52 +158,16 @@ export default mixins(
|
|||
'dialogVisible',
|
||||
],
|
||||
components: {
|
||||
ExecutionTime,
|
||||
WorkflowActivator,
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
activeExecutions: [] as IExecutionsCurrentSummaryExtended[],
|
||||
|
||||
finishedExecutions: [] as IExecutionsSummary[],
|
||||
finishedExecutionsCount: 0,
|
||||
|
||||
checkAll: false,
|
||||
|
||||
autoRefresh: {
|
||||
timer: undefined as NodeJS.Timeout | undefined,
|
||||
time: -1,
|
||||
options: [
|
||||
{
|
||||
name: 'Deactivated',
|
||||
value: -1,
|
||||
},
|
||||
{
|
||||
name: '5 Seconds',
|
||||
value: 5,
|
||||
},
|
||||
{
|
||||
name: '10 Seconds',
|
||||
value: 10,
|
||||
},
|
||||
{
|
||||
name: '15 Seconds',
|
||||
value: 15,
|
||||
},
|
||||
{
|
||||
name: '30 Seconds',
|
||||
value: 30,
|
||||
},
|
||||
{
|
||||
name: '1 Minute',
|
||||
value: 60,
|
||||
},
|
||||
{
|
||||
name: '5 Minutes',
|
||||
value: 300,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
filter: {
|
||||
workflowId: 'ALL',
|
||||
},
|
||||
|
@ -236,15 +184,13 @@ export default mixins(
|
|||
};
|
||||
},
|
||||
computed: {
|
||||
activeExecutions (): IExecutionsCurrentSummaryExtended[] {
|
||||
return this.$store.getters.getActiveExecutions;
|
||||
},
|
||||
combinedExecutions (): IExecutionsSummary[] {
|
||||
const returnData: IExecutionsSummary[] = [];
|
||||
|
||||
// The active executions do not have the workflow-names yet so add them
|
||||
for (const executionData of this.activeExecutions) {
|
||||
executionData.workflowName = this.getWorkflowName(executionData.workflowId);
|
||||
returnData.push(executionData);
|
||||
}
|
||||
|
||||
returnData.push.apply(returnData, this.activeExecutions);
|
||||
returnData.push.apply(returnData, this.finishedExecutions);
|
||||
|
||||
return returnData;
|
||||
|
@ -281,8 +227,6 @@ export default mixins(
|
|||
dialogVisible (newValue, oldValue) {
|
||||
if (newValue) {
|
||||
this.openDialog();
|
||||
} else {
|
||||
this.handleRefreshTimeChanged(-1);
|
||||
}
|
||||
},
|
||||
},
|
||||
|
@ -355,23 +299,30 @@ export default mixins(
|
|||
this.refreshData();
|
||||
},
|
||||
getRowClass (data: IDataObject): string {
|
||||
const classes: string[] = ['clickable'];
|
||||
const classes: string[] = [];
|
||||
if ((data.row as IExecutionsSummary).stoppedAt === undefined) {
|
||||
classes.push('currently-running');
|
||||
}
|
||||
|
||||
return classes.join(' ');
|
||||
},
|
||||
getWorkflowName (workflowId: string): string {
|
||||
getWorkflowName (workflowId: string): string | undefined {
|
||||
const workflow = this.workflows.find((data) => data.id === workflowId);
|
||||
if (workflow === undefined) {
|
||||
return '<UNSAVED WORKFLOW>';
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return workflow.name;
|
||||
},
|
||||
async loadActiveExecutions (): Promise<void> {
|
||||
this.activeExecutions = await this.restApi().getCurrentExecutions(this.workflowFilter);
|
||||
const activeExecutions = await this.restApi().getCurrentExecutions(this.workflowFilter);
|
||||
for (const activeExecution of activeExecutions) {
|
||||
if (activeExecution.workflowId !== undefined && activeExecution.workflowName === undefined) {
|
||||
activeExecution.workflowName = this.getWorkflowName(activeExecution.workflowId);
|
||||
}
|
||||
}
|
||||
|
||||
this.$store.commit('setActiveExecutions', activeExecutions);
|
||||
},
|
||||
async loadFinishedExecutions (): Promise<void> {
|
||||
const data = await this.restApi().getPastExecutions(this.workflowFilter, this.requestItemsPerRequest);
|
||||
|
@ -379,11 +330,6 @@ export default mixins(
|
|||
this.finishedExecutionsCount = data.count;
|
||||
},
|
||||
async loadMore () {
|
||||
// Deactivate the auto-refresh because else the newly displayed
|
||||
// data would be lost with the next automatic refresh
|
||||
this.autoRefresh.time = -1;
|
||||
this.handleRefreshTimeChanged();
|
||||
|
||||
this.isDataLoading = true;
|
||||
|
||||
const filter = this.workflowFilter;
|
||||
|
@ -432,32 +378,13 @@ export default mixins(
|
|||
this.$showError(error, 'Problem loading workflows', 'There was a problem loading the workflows:');
|
||||
}
|
||||
},
|
||||
openDialog () {
|
||||
async openDialog () {
|
||||
Vue.set(this, 'selectedItems', {});
|
||||
this.filter.workflowId = 'ALL';
|
||||
this.checkAll = false;
|
||||
|
||||
this.loadWorkflows();
|
||||
this.refreshData();
|
||||
this.handleRefreshTimeChanged();
|
||||
},
|
||||
handleRefreshTimeChanged (manualOverwrite?: number) {
|
||||
if (this.autoRefresh.timer !== undefined) {
|
||||
// Make sure the old timer gets removed
|
||||
clearInterval(this.autoRefresh.timer);
|
||||
this.autoRefresh.timer = undefined;
|
||||
}
|
||||
|
||||
const timerValue = manualOverwrite !== undefined ? manualOverwrite : this.autoRefresh.time;
|
||||
if (timerValue === -1) {
|
||||
// No timer should be set
|
||||
return;
|
||||
}
|
||||
|
||||
// Create the new interval timer
|
||||
this.autoRefresh.timer = setInterval(() => {
|
||||
this.refreshData();
|
||||
}, timerValue * 1000);
|
||||
await this.loadWorkflows();
|
||||
await this.refreshData();
|
||||
},
|
||||
async retryExecution (execution: IExecutionShortResponse) {
|
||||
this.isDataLoading = true;
|
||||
|
@ -501,18 +428,6 @@ export default mixins(
|
|||
|
||||
this.isDataLoading = false;
|
||||
},
|
||||
handleRowClick (entry: IExecutionsSummary, event: Event, column: any) { // tslint:disable-line:no-any
|
||||
if (column.label === '') {
|
||||
// Ignore all clicks in the first and last row
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.selectedItems[entry.id]) {
|
||||
Vue.delete(this.selectedItems, entry.id);
|
||||
} else {
|
||||
Vue.set(this.selectedItems, entry.id, true);
|
||||
}
|
||||
},
|
||||
statusTooltipText (entry: IExecutionsSummary): string {
|
||||
if (entry.stoppedAt === undefined) {
|
||||
return 'The worklow is currently executing.';
|
||||
|
@ -526,21 +441,21 @@ export default mixins(
|
|||
return 'The workflow execution did fail.';
|
||||
}
|
||||
},
|
||||
async stopExecution (executionId: string) {
|
||||
async stopExecution (activeExecutionId: string) {
|
||||
try {
|
||||
// Add it to the list of currently stopping executions that we
|
||||
// can show the user in the UI that it is in progress
|
||||
this.stoppingExecutions.push(executionId);
|
||||
this.stoppingExecutions.push(activeExecutionId);
|
||||
|
||||
const stopData: IExecutionsStopData = await this.restApi().stopCurrentExecution(executionId);
|
||||
const stopData: IExecutionsStopData = await this.restApi().stopCurrentExecution(activeExecutionId);
|
||||
|
||||
// Remove it from the list of currently stopping executions
|
||||
const index = this.stoppingExecutions.indexOf(executionId);
|
||||
const index = this.stoppingExecutions.indexOf(activeExecutionId);
|
||||
this.stoppingExecutions.splice(index, 1);
|
||||
|
||||
this.$showMessage({
|
||||
title: 'Execution stopped',
|
||||
message: `The execution with the id "${executionId}" got stopped!`,
|
||||
message: `The execution with the id "${activeExecutionId}" got stopped!`,
|
||||
type: 'success',
|
||||
});
|
||||
|
||||
|
|
|
@ -23,7 +23,21 @@ export const genericHelpers = mixins(showMessage).extend({
|
|||
convertToDisplayDate (epochTime: number) {
|
||||
return dateformat(epochTime, 'yyyy-mm-dd HH:MM:ss');
|
||||
},
|
||||
displayTimer (msPassed: number, showMs = false): string {
|
||||
if (msPassed < 60000) {
|
||||
if (showMs === false) {
|
||||
return `${Math.floor(msPassed / 1000)} sec.`;
|
||||
}
|
||||
|
||||
return `${msPassed / 1000} sec.`;
|
||||
}
|
||||
|
||||
const secondsPassed = Math.floor(msPassed / 1000);
|
||||
const minutesPassed = Math.floor(secondsPassed / 60);
|
||||
const secondsLeft = (secondsPassed - (minutesPassed * 60)).toString().padStart(2, '0');
|
||||
|
||||
return `${minutesPassed}:${secondsLeft} min.`;
|
||||
},
|
||||
editAllowedCheck (): boolean {
|
||||
if (this.isReadOnly) {
|
||||
this.$showMessage({
|
||||
|
|
|
@ -161,7 +161,6 @@ export const mouseSelect = mixins(nodeIndex).extend({
|
|||
const nodeElement = `node-${this.getNodeIndex(node.name)}`;
|
||||
// @ts-ignore
|
||||
this.instance.removeFromDragSelection(nodeElement);
|
||||
|
||||
},
|
||||
nodeSelected (node: INodeUi) {
|
||||
this.$store.commit('addSelectedNode', node);
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import {
|
||||
IExecutionsCurrentSummaryExtended,
|
||||
IPushData,
|
||||
IPushDataExecutionFinished,
|
||||
IPushDataExecutionStarted,
|
||||
IPushDataNodeExecuteAfter,
|
||||
IPushDataNodeExecuteBefore,
|
||||
IPushDataTestWebhook,
|
||||
|
@ -100,7 +102,7 @@ export const pushConnection = mixins(
|
|||
return;
|
||||
}
|
||||
|
||||
if (['executionFinished', 'nodeExecuteAfter', 'nodeExecuteBefore'].includes(receivedData.type)) {
|
||||
if (['nodeExecuteAfter', 'nodeExecuteBefore'].includes(receivedData.type)) {
|
||||
if (this.$store.getters.isActionActive('workflowRunning') === false) {
|
||||
// No workflow is running so ignore the messages
|
||||
return;
|
||||
|
@ -119,6 +121,19 @@ export const pushConnection = mixins(
|
|||
// The workflow finished executing
|
||||
const pushData = receivedData.data as IPushDataExecutionFinished;
|
||||
|
||||
this.$store.commit('finishActiveExecution', pushData);
|
||||
|
||||
if (this.$store.getters.isActionActive('workflowRunning') === false) {
|
||||
// No workflow is running so ignore the messages
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.$store.getters.activeExecutionId !== pushData.executionIdActive) {
|
||||
// The workflow which did finish execution did not get started
|
||||
// by this session so ignore
|
||||
return;
|
||||
}
|
||||
|
||||
const runDataExecuted = pushData.data;
|
||||
|
||||
if (runDataExecuted.finished !== true) {
|
||||
|
@ -149,6 +164,19 @@ export const pushConnection = mixins(
|
|||
// Set the node execution issues on all the nodes which produced an error so that
|
||||
// it can be displayed in the node-view
|
||||
this.updateNodesExecutionIssues();
|
||||
} else if (receivedData.type === 'executionStarted') {
|
||||
const pushData = receivedData.data as IPushDataExecutionStarted;
|
||||
|
||||
const executionData: IExecutionsCurrentSummaryExtended = {
|
||||
idActive: pushData.executionId,
|
||||
finished: false,
|
||||
mode: pushData.mode,
|
||||
startedAt: pushData.startedAt,
|
||||
workflowId: pushData.workflowId,
|
||||
workflowName: pushData.workflowName,
|
||||
};
|
||||
|
||||
this.$store.commit('addActiveExecution', executionData);
|
||||
} else if (receivedData.type === 'nodeExecuteAfter') {
|
||||
// A node finished to execute. Add its data
|
||||
const pushData = receivedData.data as IPushDataNodeExecuteAfter;
|
||||
|
|
|
@ -19,6 +19,8 @@ import {
|
|||
import {
|
||||
ICredentialsResponse,
|
||||
IExecutionResponse,
|
||||
IExecutionsCurrentSummaryExtended,
|
||||
IPushDataExecutionFinished,
|
||||
IPushDataNodeExecuteAfter,
|
||||
IWorkflowDb,
|
||||
INodeUi,
|
||||
|
@ -34,6 +36,7 @@ Vue.use(Vuex);
|
|||
export const store = new Vuex.Store({
|
||||
strict: process.env.NODE_ENV !== 'production',
|
||||
state: {
|
||||
activeExecutions: [] as IExecutionsCurrentSummaryExtended[],
|
||||
activeWorkflows: [] as string[],
|
||||
activeActions: [] as string[],
|
||||
activeNode: null as string | null,
|
||||
|
@ -84,6 +87,45 @@ export const store = new Vuex.Store({
|
|||
}
|
||||
},
|
||||
|
||||
// Active Executions
|
||||
addActiveExecution (state, newActiveExecution: IExecutionsCurrentSummaryExtended) {
|
||||
// Check if the execution exists already
|
||||
const activeExecution = state.activeExecutions.find(execution => {
|
||||
return execution.idActive === newActiveExecution.idActive;
|
||||
});
|
||||
|
||||
if (activeExecution !== undefined) {
|
||||
// Exists already so no need to add it again
|
||||
if (activeExecution.workflowName === undefined) {
|
||||
activeExecution.workflowName = newActiveExecution.workflowName;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
state.activeExecutions.unshift(newActiveExecution);
|
||||
},
|
||||
finishActiveExecution (state, finishedActiveExecution: IPushDataExecutionFinished) {
|
||||
// Find the execution to set to finished
|
||||
const activeExecution = state.activeExecutions.find(execution => {
|
||||
return execution.idActive === finishedActiveExecution.executionIdActive;
|
||||
});
|
||||
|
||||
if (activeExecution === undefined) {
|
||||
// The execution could not be found
|
||||
return;
|
||||
}
|
||||
|
||||
if (finishedActiveExecution.executionIdDb !== undefined) {
|
||||
Vue.set(activeExecution, 'id', finishedActiveExecution.executionIdDb);
|
||||
}
|
||||
|
||||
Vue.set(activeExecution, 'finished', finishedActiveExecution.data.finished);
|
||||
Vue.set(activeExecution, 'stoppedAt', finishedActiveExecution.data.stoppedAt);
|
||||
},
|
||||
setActiveExecutions (state, newActiveExecutions: IExecutionsCurrentSummaryExtended[]) {
|
||||
Vue.set(state, 'activeExecutions', newActiveExecutions);
|
||||
},
|
||||
|
||||
// Active Workflows
|
||||
setActiveWorkflows (state, newActiveWorkflows: string[]) {
|
||||
state.activeWorkflows = newActiveWorkflows;
|
||||
|
@ -506,6 +548,10 @@ export const store = new Vuex.Store({
|
|||
return state.activeActions.includes(action);
|
||||
},
|
||||
|
||||
getActiveExecutions: (state): IExecutionsCurrentSummaryExtended[] => {
|
||||
return state.activeExecutions;
|
||||
},
|
||||
|
||||
getBaseUrl: (state): string => {
|
||||
return state.baseUrl;
|
||||
},
|
||||
|
|
Loading…
Reference in a new issue