n8n/packages/editor-ui/src/components/ExecutionsView/ExecutionPreview.vue
Michael Auerswald d143f3f2ec
feat(core): Add execution runData recovery and status field (#5112)
* adds ExecutionEvents view modal to ExecutionList

* fix time rendering and remove wf column

* checks for unfinished executions and fails them

* prevent re-setting stoppedAt for execution

* some cleanup / manually create rundata after crash

* quicksave

* remove Threads lib, log worker rewrite

* cleanup comment

* fix sentry destination return value

* test for tests...

* run tests with single worker

* fix tests

* remove console log

* add endpoint for execution data recovery

* lint cleanup and some refactoring

* fix accidental recursion

* remove cyclic imports

* add rundata recovery to Workflowrunner

* remove comments

* cleanup and refactor

* adds a status field to executions

* setExecutionStatus on queued worker

* fix onWorkflowPostExecute

* set waiting from worker

* get crashed status into frontend

* remove comment

* merge fix

* cleanup

* catch empty rundata in recovery

* refactor IExecutionsSummary and inject nodeExecution Errors

* reduce default event log size to 10mb from 100mb

* add per node execution status

* lint fix

* merge and lint fix

* phrasing change

* improve preview rendering and messaging

* remove debug

* Improve partial rundata recovery

* fix labels

* fix line through

* send manual rundata to ui at crash

* some type and msg push fixes

* improve recovered item rendering in preview

* update workflowStatistics on recover

* merge fix

* review fixes

* merge fix

* notify eventbus when ui is back up

* add a small timeout to make sure the UI is back up

* increase reconnect timeout to 30s

* adjust recover timeout and ui connection lost msg

* do not stop execution in editor after x reconnects

* add executionRecovered push event

* fix recovered connection not green

* remove reconnect toast and  merge existing rundata

* merge editor and recovered data for own mode
2023-02-17 10:54:07 +01:00

254 lines
6.6 KiB
Vue

<template>
<div v-if="executionUIDetails?.name === 'running'" :class="$style.runningInfo">
<div :class="$style.spinner">
<n8n-spinner type="ring" />
</div>
<n8n-text :class="$style.runningMessage" color="text-light">
{{ $locale.baseText('executionDetails.runningMessage') }}
</n8n-text>
<n8n-button class="mt-l" type="tertiary" size="medium" @click="handleStopClick">
{{ $locale.baseText('executionsList.stopExecution') }}
</n8n-button>
</div>
<div v-else :class="$style.previewContainer">
<div
:class="{ [$style.executionDetails]: true, [$style.sidebarCollapsed]: sidebarCollapsed }"
v-if="activeExecution"
:data-test-id="`execution-preview-details-${executionId}`"
>
<div>
<n8n-text size="large" color="text-base" :bold="true" data-test-id="execution-time">{{
executionUIDetails?.startTime
}}</n8n-text
><br />
<n8n-spinner
v-if="executionUIDetails?.name === 'running'"
size="small"
:class="[$style.spinner, 'mr-4xs']"
/>
<n8n-text
size="medium"
:class="[$style.status, $style[executionUIDetails.name]]"
data-test-id="execution-preview-label"
>
{{ executionUIDetails.label }}
</n8n-text>
<n8n-text v-if="executionUIDetails.name === 'running'" color="text-base" size="medium">
{{
$locale.baseText('executionDetails.runningTimeRunning', {
interpolate: { time: executionUIDetails?.runningTime },
})
}}
| ID#{{ activeExecution.id }}
</n8n-text>
<n8n-text
v-else-if="executionUIDetails.name !== 'waiting'"
color="text-base"
size="medium"
data-test-id="execution-preview-id"
>
{{
$locale.baseText('executionDetails.runningTimeFinished', {
interpolate: { time: executionUIDetails?.runningTime ?? 'unknown' },
})
}}
| ID#{{ activeExecution.id }}
</n8n-text>
<n8n-text
v-else-if="executionUIDetails?.name === 'waiting'"
color="text-base"
size="medium"
>
| ID#{{ activeExecution.id }}
</n8n-text>
<br /><n8n-text v-if="activeExecution.mode === 'retry'" color="text-base" size="medium">
{{ $locale.baseText('executionDetails.retry') }}
<router-link
:class="$style.executionLink"
:to="{
name: VIEWS.EXECUTION_PREVIEW,
params: {
workflowId: activeExecution.workflowId,
executionId: activeExecution.retryOf,
},
}"
>
#{{ activeExecution.retryOf }}
</router-link>
</n8n-text>
</div>
<div>
<el-dropdown
v-if="executionUIDetails?.name === 'error'"
trigger="click"
class="mr-xs"
@command="handleRetryClick"
ref="retryDropdown"
>
<span class="retry-button">
<n8n-icon-button
size="large"
type="tertiary"
:title="$locale.baseText('executionsList.retryExecution')"
icon="redo"
data-test-id="execution-preview-retry-button"
@blur="onRetryButtonBlur"
/>
</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="current-workflow">
{{ $locale.baseText('executionsList.retryWithCurrentlySavedWorkflow') }}
</el-dropdown-item>
<el-dropdown-item command="original-workflow">
{{ $locale.baseText('executionsList.retryWithOriginalWorkflow') }}
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<n8n-icon-button
:title="$locale.baseText('executionDetails.deleteExecution')"
icon="trash"
size="large"
type="tertiary"
data-test-id="execution-preview-delete-button"
@click="onDeleteExecution"
/>
</div>
</div>
<workflow-preview
mode="execution"
loaderType="spinner"
:executionId="executionId"
:executionMode="executionMode"
/>
</div>
</template>
<script lang="ts">
import mixins from 'vue-typed-mixins';
import { restApi } from '@/mixins/restApi';
import { showMessage } from '@/mixins/showMessage';
import WorkflowPreview from '@/components/WorkflowPreview.vue';
import { executionHelpers, IExecutionUIData } from '@/mixins/executionsHelpers';
import { VIEWS } from '@/constants';
import { mapStores } from 'pinia';
import { useUIStore } from '@/stores/ui';
import { Dropdown as ElDropdown } from 'element-ui';
import { IAbstractEventMessage } from 'n8n-workflow';
export default mixins(restApi, showMessage, executionHelpers).extend({
name: 'execution-preview',
components: {
ElDropdown,
WorkflowPreview,
},
data() {
return {
VIEWS,
};
},
computed: {
...mapStores(useUIStore),
executionUIDetails(): IExecutionUIData | null {
return this.activeExecution ? this.getExecutionUIDetails(this.activeExecution) : null;
},
sidebarCollapsed(): boolean {
return this.uiStore.sidebarMenuCollapsed;
},
executionMode(): string {
return this.activeExecution?.mode || '';
},
},
methods: {
async onDeleteExecution(): Promise<void> {
const deleteConfirmed = await this.confirmMessage(
this.$locale.baseText('executionDetails.confirmMessage.message'),
this.$locale.baseText('executionDetails.confirmMessage.headline'),
'warning',
this.$locale.baseText('executionDetails.confirmMessage.confirmButtonText'),
'',
);
if (!deleteConfirmed) {
return;
}
this.$emit('deleteCurrentExecution');
},
handleRetryClick(command: string): void {
this.$emit('retryExecution', { execution: this.activeExecution, command });
},
handleStopClick(): void {
this.$emit('stopExecution');
},
onRetryButtonBlur(event: FocusEvent): void {
// Hide dropdown when clicking outside of current document
const retryDropdown = this.$refs.retryDropdown as (Vue & { hide: () => void }) | undefined;
if (retryDropdown && event.relatedTarget === null) {
retryDropdown.hide();
}
},
},
});
</script>
<style module lang="scss">
.previewContainer {
height: calc(100% - $header-height);
overflow: hidden;
}
.executionDetails {
position: absolute;
padding: var(--spacing-m);
padding-right: var(--spacing-xl);
width: calc(100% - 510px);
display: flex;
justify-content: space-between;
transition: all 150ms ease-in-out;
pointer-events: none;
& * {
pointer-events: all;
}
&.sidebarCollapsed {
width: calc(100% - 375px);
}
}
.spinner {
div div {
width: 30px;
height: 30px;
border-width: 2px;
}
}
.running,
.spinner {
color: var(--color-warning);
}
.waiting {
color: var(--color-secondary);
}
.success {
color: var(--color-success);
}
.error {
color: var(--color-danger);
}
.runningInfo {
display: flex;
flex-direction: column;
align-items: center;
margin-top: var(--spacing-4xl);
}
.runningMessage {
width: 200px;
margin-top: var(--spacing-l);
text-align: center;
}
</style>