n8n/packages/editor-ui/src/views/WorkflowExecutionsView.vue

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

323 lines
8.9 KiB
Vue
Raw Normal View History

<script lang="ts" setup>
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';
import WorkflowExecutionsList from '@/components/executions/workflow/WorkflowExecutionsList.vue';
import { useExecutionsStore } from '@/stores/executions.store';
import { useI18n } from '@/composables/useI18n';
import type { ExecutionFilterType, IWorkflowDb } from '@/Interface';
import { useWorkflowsStore } from '@/stores/workflows.store';
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
import { NO_NETWORK_ERROR_CODE } from '@/utils/apiUtils';
import { useToast } from '@/composables/useToast';
import { PLACEHOLDER_EMPTY_WORKFLOW_ID, VIEWS } from '@/constants';
import { useRoute, useRouter } from 'vue-router';
import type { ExecutionSummary } from 'n8n-workflow';
import { useDebounce } from '@/composables/useDebounce';
import { useTelemetry } from '@/composables/useTelemetry';
import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers';
import { useNodeHelpers } from '@/composables/useNodeHelpers';
const executionsStore = useExecutionsStore();
const workflowsStore = useWorkflowsStore();
const nodeTypesStore = useNodeTypesStore();
const i18n = useI18n();
const telemetry = useTelemetry();
const route = useRoute();
const router = useRouter();
const toast = useToast();
const { callDebounced } = useDebounce();
const workflowHelpers = useWorkflowHelpers({ router });
const nodeHelpers = useNodeHelpers();
const loading = ref(false);
const loadingMore = ref(false);
const workflow = ref<IWorkflowDb | undefined>();
const workflowId = computed(() => {
return (route.params.name as string) || workflowsStore.workflowId;
});
const executionId = computed(() => route.params.executionId as string);
const executions = computed(() => [
...(executionsStore.currentExecutionsByWorkflowId[workflowId.value] ?? []),
...(executionsStore.executionsByWorkflowId[workflowId.value] ?? []),
]);
const execution = computed(() => {
return executions.value.find((e) => e.id === executionId.value) ?? currentExecution.value;
});
const currentExecution = ref<ExecutionSummary | undefined>();
watch(
() => workflowId.value,
async () => {
await fetchWorkflow();
},
);
watch(
() => executionId.value,
async () => {
await fetchExecution();
},
);
onMounted(async () => {
await nodeTypesStore.loadNodeTypesIfNotLoaded();
await Promise.all([
nodeTypesStore.loadNodeTypesIfNotLoaded(),
fetchWorkflow(),
executionsStore.initialize(workflowId.value),
]);
await fetchExecution();
await initializeRoute();
document.addEventListener('visibilitychange', onDocumentVisibilityChange);
});
onBeforeUnmount(() => {
executionsStore.reset();
document.removeEventListener('visibilitychange', onDocumentVisibilityChange);
});
async function fetchExecution() {
if (!executionId.value) {
return;
}
try {
currentExecution.value = (await executionsStore.fetchExecution(
executionId.value,
)) as ExecutionSummary;
executionsStore.activeExecution = currentExecution.value;
} catch (error) {
toast.showError(error, i18n.baseText('nodeView.showError.openExecution.title'));
}
}
function onDocumentVisibilityChange() {
if (document.visibilityState === 'hidden') {
executionsStore.stopAutoRefreshInterval();
} else {
void executionsStore.startAutoRefreshInterval(workflowId.value);
}
}
async function initializeRoute() {
if (route.name === VIEWS.EXECUTION_HOME && executions.value.length > 0 && workflow.value) {
await router
.push({
name: VIEWS.EXECUTION_PREVIEW,
params: { name: workflow.value.id, executionId: executions.value[0].id },
})
.catch(() => {});
}
}
async function fetchWorkflow() {
// Check if the workflow already has an ID
// In other words: are we coming from the Editor tab or browser loaded the Executions tab directly
if (workflowsStore.workflow.id === PLACEHOLDER_EMPTY_WORKFLOW_ID) {
try {
await workflowsStore.fetchActiveWorkflows();
const data = await workflowsStore.fetchWorkflow(workflowId.value);
workflowHelpers.initState(data);
await nodeHelpers.addNodes(data.nodes, data.connections);
} catch (error) {
toast.showError(error, i18n.baseText('nodeView.showError.openWorkflow.title'));
}
}
workflow.value = workflowsStore.workflow;
}
async function onAutoRefreshToggle(value: boolean) {
if (value) {
await executionsStore.startAutoRefreshInterval(workflowId.value);
} else {
executionsStore.stopAutoRefreshInterval();
}
}
async function onRefreshData() {
if (!workflowId.value) {
return;
}
try {
await executionsStore.fetchExecutions({
...executionsStore.executionsFilters,
workflowId: workflowId.value,
});
} catch (error) {
if (error.errorCode === NO_NETWORK_ERROR_CODE) {
toast.showMessage(
{
title: i18n.baseText('executionsList.showError.refreshData.title'),
message: error.message,
type: 'error',
duration: 3500,
},
false,
);
} else {
toast.showError(error, i18n.baseText('executionsList.showError.refreshData.title'));
}
}
}
async function onUpdateFilters(newFilters: ExecutionFilterType) {
executionsStore.reset();
executionsStore.setFilters(newFilters);
await executionsStore.initialize(workflowId.value);
}
async function onExecutionStop(id: string) {
try {
await executionsStore.stopCurrentExecution(id);
toast.showMessage({
title: i18n.baseText('executionsList.showMessage.stopExecution.title'),
message: i18n.baseText('executionsList.showMessage.stopExecution.message', {
interpolate: { activeExecutionId: id },
}),
type: 'success',
});
await onRefreshData();
} catch (error) {
toast.showError(error, i18n.baseText('executionsList.showError.stopExecution.title'));
}
}
async function onExecutionDelete(id: string) {
loading.value = true;
try {
const executionIndex = executions.value.findIndex((e: ExecutionSummary) => e.id === id);
const nextExecution =
executions.value[executionIndex + 1] ||
executions.value[executionIndex - 1] ||
executions.value[0];
await executionsStore.deleteExecutions({
ids: [id],
});
if (workflow.value) {
if (executions.value.length > 0) {
await router
.replace({
name: VIEWS.EXECUTION_PREVIEW,
params: { name: workflow.value.id, executionId: nextExecution.id },
})
.catch(() => {});
} else {
// If there are no executions left, show empty state
await router.replace({
name: VIEWS.EXECUTION_HOME,
params: { name: workflow.value.id },
});
}
}
} catch (error) {
loading.value = false;
toast.showError(error, i18n.baseText('executionsList.showError.handleDeleteSelected.title'));
return;
}
loading.value = false;
toast.showMessage({
title: i18n.baseText('executionsList.showMessage.handleDeleteSelected.title'),
type: 'success',
});
}
async function onExecutionRetry(payload: { id: string; loadWorkflow: boolean }) {
toast.showMessage({
title: i18n.baseText('executionDetails.runningMessage'),
type: 'info',
duration: 2000,
});
await retryExecution(payload);
await onRefreshData();
telemetry.track('User clicked retry execution button', {
workflow_id: workflow.value?.id,
execution_id: payload.id,
retry_type: payload.loadWorkflow ? 'current' : 'original',
});
}
async function retryExecution(payload: { id: string; loadWorkflow: boolean }) {
try {
const retrySuccessful = await executionsStore.retryExecution(payload.id, payload.loadWorkflow);
if (retrySuccessful) {
toast.showMessage({
title: i18n.baseText('executionsList.showMessage.retrySuccessfulTrue.title'),
type: 'success',
});
} else {
toast.showMessage({
title: i18n.baseText('executionsList.showMessage.retrySuccessfulFalse.title'),
type: 'error',
});
}
} catch (error) {
toast.showError(error, i18n.baseText('executionsList.showError.retryExecution.title'));
}
}
async function onLoadMore(): Promise<void> {
if (!loadingMore.value) {
await callDebounced(loadMore, { debounceTime: 1000 });
}
}
async function loadMore(): Promise<void> {
if (
!!executionsStore.executionsFilters.status?.includes('running') ||
executions.value.length >= executionsStore.executionsCount
) {
return;
}
loadingMore.value = true;
let lastId: string | undefined;
if (executions.value.length !== 0) {
const lastItem = executions.value.slice(-1)[0];
lastId = lastItem.id;
}
try {
await executionsStore.fetchExecutions(executionsStore.executionsFilters, lastId);
} catch (error) {
loadingMore.value = false;
toast.showError(error, i18n.baseText('executionsList.showError.loadMore.title'));
return;
}
loadingMore.value = false;
}
</script>
<template>
<WorkflowExecutionsList
v-if="workflow"
:executions="executions"
:execution="execution"
:workflow="workflow"
:loading="loading"
:loading-more="loadingMore"
@execution:stop="onExecutionStop"
@execution:delete="onExecutionDelete"
@execution:retry="onExecutionRetry"
@update:filters="onUpdateFilters"
@update:auto-refresh="onAutoRefreshToggle"
@load-more="onLoadMore"
@reload="onRefreshData"
/>
</template>