feat: add subexecution count

This commit is contained in:
Mutasem Aldmour 2024-11-12 10:43:11 +01:00
parent 597aa3ee3b
commit f32039647b
No known key found for this signature in database
GPG key ID: 3DFA8122BB7FD6B8
5 changed files with 125 additions and 62 deletions

View file

@ -514,13 +514,13 @@ const subWorkflowData = computed((): ITaskMetadata | null => {
return null; return null;
} }
const metadata = get(workflowRunData.value, [node.value.name, props.runIndex, 'metadata'], null); const metadata = get(workflowRunData.value, [node.value.name, props.runIndex, 'metadata'], null);
if (!metadata?.parentExecution && !metadata?.subExecution) {
return null;
}
return metadata; return metadata;
}); });
const hasReleatedExectuion = computed((): boolean => {
return Boolean(subWorkflowData.value?.subExecution || subWorkflowData.value?.parentExecution);
});
const hasInputOverwrite = computed((): boolean => { const hasInputOverwrite = computed((): boolean => {
if (!node.value) { if (!node.value) {
return false; return false;
@ -977,6 +977,10 @@ function onDisplayModeChange(newDisplayMode: IRunDataDisplayMode) {
} }
function getRunLabel(option: number) { function getRunLabel(option: number) {
if (!node.value) {
return;
}
let itemsCount = 0; let itemsCount = 0;
for (let i = 0; i <= maxOutputIndex.value; i++) { for (let i = 0; i <= maxOutputIndex.value; i++) {
itemsCount += getPinDataOrLiveData(getRawInputData(option - 1, i)).length; itemsCount += getPinDataOrLiveData(getRawInputData(option - 1, i)).length;
@ -985,7 +989,18 @@ function getRunLabel(option: number) {
adjustToNumber: itemsCount, adjustToNumber: itemsCount,
interpolate: { count: itemsCount }, interpolate: { count: itemsCount },
}); });
const itemsLabel = itemsCount > 0 ? ` (${items})` : '';
const metadata = get(workflowRunData.value, [node.value.name, props.runIndex, 'metadata'], null);
const subexecutions = metadata?.subExecutionsCount
? i18n.baseText('ndv.output.andSubExecutions', {
adjustToNumber: metadata.subExecutionsCount,
interpolate: {
count: metadata.subExecutionsCount,
},
})
: '';
const itemsLabel = itemsCount > 0 ? ` (${items}${subexecutions})` : '';
return option + i18n.baseText('ndv.output.of') + (maxRunIndex.value + 1) + itemsLabel; return option + i18n.baseText('ndv.output.of') + (maxRunIndex.value + 1) + itemsLabel;
} }
@ -1335,42 +1350,59 @@ defineExpose({ enterEditMode });
v-show="!editMode.enabled" v-show="!editMode.enabled"
:class="$style.runSelector" :class="$style.runSelector"
> >
<slot v-if="inputSelectLocation === 'runs'" name="input-select"></slot> <div :class="$style.runSelectorInner">
<slot v-if="inputSelectLocation === 'runs'" name="input-select"></slot>
<N8nSelect <N8nSelect
:model-value="runIndex" :model-value="runIndex"
:class="$style.runSelectorInner" :class="$style.runSelectorSelect"
size="small"
teleported
data-test-id="run-selector"
@update:model-value="onRunIndexChange"
@click.stop
>
<template #prepend>{{ $locale.baseText('ndv.output.run') }}</template>
<N8nOption
v-for="option in maxRunIndex + 1"
:key="option"
:label="getRunLabel(option)"
:value="option - 1"
></N8nOption>
</N8nSelect>
<N8nTooltip v-if="canLinkRuns" placement="right">
<template #content>
{{ $locale.baseText(linkedRuns ? 'runData.unlinking.hint' : 'runData.linking.hint') }}
</template>
<N8nIconButton
:icon="linkedRuns ? 'unlink' : 'link'"
class="linkRun"
text
type="tertiary"
size="small" size="small"
data-test-id="link-run" teleported
@click="toggleLinkRuns" data-test-id="run-selector"
/> @update:model-value="onRunIndexChange"
</N8nTooltip> @click.stop
>
<template #prepend>{{ $locale.baseText('ndv.output.run') }}</template>
<N8nOption
v-for="option in maxRunIndex + 1"
:key="option"
:label="getRunLabel(option)"
:value="option - 1"
></N8nOption>
</N8nSelect>
<slot name="run-info"></slot> <N8nTooltip v-if="canLinkRuns" placement="right">
<template #content>
{{ $locale.baseText(linkedRuns ? 'runData.unlinking.hint' : 'runData.linking.hint') }}
</template>
<N8nIconButton
:icon="linkedRuns ? 'unlink' : 'link'"
class="linkRun"
text
type="tertiary"
size="small"
data-test-id="link-run"
@click="toggleLinkRuns"
/>
</N8nTooltip>
<slot name="run-info"></slot>
</div>
<a
v-if="
subWorkflowData && hasReleatedExectuion && !(paneType === 'input' && hasInputOverwrite)
"
:class="$style.parentExecutionInfo"
@click.stop="openRelatedExecution(subWorkflowData, displayMode)"
>
<N8nIcon icon="external-link-alt" size="xsmall" />
{{
subWorkflowData.parentExecution
? $locale.baseText('runData.openParentExecution')
: $locale.baseText('runData.openSubExecution')
}}
</a>
</div> </div>
<slot v-if="!displaysMultipleNodes" name="before-data" /> <slot v-if="!displaysMultipleNodes" name="before-data" />
@ -1424,20 +1456,31 @@ defineExpose({ enterEditMode });
}} }}
</N8nText> </N8nText>
<N8nText v-else :class="$style.itemsText"> <N8nText v-else :class="$style.itemsText">
{{ <span>
$locale.baseText('ndv.output.items', { {{
adjustToNumber: dataCount, $locale.baseText('ndv.output.items', {
interpolate: { count: dataCount }, adjustToNumber: dataCount,
}) interpolate: { count: dataCount },
}} })
}}
</span>
<span v-if="subWorkflowData?.subExecutionsCount">
{{
$locale.baseText('ndv.output.andSubExecutions', {
adjustToNumber: subWorkflowData.subExecutionsCount,
interpolate: { count: subWorkflowData.subExecutionsCount },
})
}}
</span>
</N8nText> </N8nText>
</div>
<div <a
v-if="subWorkflowData && !(paneType === 'input' && hasInputOverwrite)" v-if="
:class="$style.parentExecutionInfo" subWorkflowData && hasReleatedExectuion && !(paneType === 'input' && hasInputOverwrite)
> "
<a @click.stop="openRelatedExecution(subWorkflowData, displayMode)"> :class="$style.parentExecutionInfo"
@click.stop="openRelatedExecution(subWorkflowData, displayMode)"
>
<N8nIcon icon="external-link-alt" size="xsmall" /> <N8nIcon icon="external-link-alt" size="xsmall" />
{{ {{
subWorkflowData.parentExecution subWorkflowData.parentExecution
@ -1630,6 +1673,7 @@ defineExpose({ enterEditMode });
:total-runs="maxRunIndex" :total-runs="maxRunIndex"
:has-default-hover-state="paneType === 'input' && !search" :has-default-hover-state="paneType === 'input' && !search"
:search="search" :search="search"
:sub-execution-override="subWorkflowData?.subExecution"
@mounted="emit('tableMounted', $event)" @mounted="emit('tableMounted', $event)"
@active-row-changed="onItemHover" @active-row-changed="onItemHover"
@display-mode-change="onDisplayModeChange" @display-mode-change="onDisplayModeChange"
@ -1911,6 +1955,7 @@ defineExpose({ enterEditMode });
padding-left: var(--spacing-s); padding-left: var(--spacing-s);
padding-right: var(--spacing-s); padding-right: var(--spacing-s);
padding-bottom: var(--spacing-s); padding-bottom: var(--spacing-s);
flex-flow: wrap;
.itemsText { .itemsText {
flex-shrink: 0; flex-shrink: 0;
@ -1932,24 +1977,31 @@ defineExpose({ enterEditMode });
} }
.runSelector { .runSelector {
display: flex;
align-items: center;
flex-flow: wrap;
padding-left: var(--spacing-s); padding-left: var(--spacing-s);
padding-right: var(--spacing-s); padding-right: var(--spacing-s);
padding-bottom: var(--spacing-s); margin-bottom: var(--spacing-s);
display: flex; gap: var(--spacing-3xs);
gap: var(--spacing-4xs);
align-items: center;
:global(.el-input--suffix .el-input__inner) { :global(.el-input--suffix .el-input__inner) {
padding-right: var(--spacing-l); padding-right: var(--spacing-l);
} }
} }
.search { .runSelectorInner {
margin-left: auto; display: flex;
gap: var(--spacing-4xs);
align-items: center;
} }
.runSelectorInner { .runSelectorSelect {
max-width: 172px; max-width: 205px;
}
.search {
margin-left: auto;
} }
.pagination { .pagination {
@ -2112,7 +2164,7 @@ defineExpose({ enterEditMode });
.parentExecutionInfo { .parentExecutionInfo {
font-size: var(--font-size-xs); font-size: var(--font-size-xs);
padding: 0 0 var(--spacing-s) var(--spacing-s); margin-left: var(--spacing-3xs);
svg { svg {
padding-bottom: 2px; padding-bottom: 2px;

View file

@ -6,7 +6,7 @@ import { useWorkflowsStore } from '@/stores/workflows.store';
import { getMappedExpression } from '@/utils/mappingUtils'; import { getMappedExpression } from '@/utils/mappingUtils';
import { getPairedItemId } from '@/utils/pairedItemUtils'; import { getPairedItemId } from '@/utils/pairedItemUtils';
import { shorten } from '@/utils/typesUtils'; import { shorten } from '@/utils/typesUtils';
import type { GenericValue, IDataObject, INodeExecutionData } from 'n8n-workflow'; import type { GenericValue, IDataObject, INodeExecutionData, RelatedExecution } from 'n8n-workflow';
import { computed, onMounted, ref, watch } from 'vue'; import { computed, onMounted, ref, watch } from 'vue';
import Draggable from './Draggable.vue'; import Draggable from './Draggable.vue';
import MappingPill from './MappingPill.vue'; import MappingPill from './MappingPill.vue';
@ -32,6 +32,7 @@ type Props = {
mappingEnabled?: boolean; mappingEnabled?: boolean;
hasDefaultHoverState?: boolean; hasDefaultHoverState?: boolean;
search?: string; search?: string;
subExecutionOverride?: RelatedExecution;
}; };
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
@ -341,7 +342,10 @@ function convertToTable(inputData: INodeExecutionData[]): ITableData {
leftEntryColumns = entryColumns; leftEntryColumns = entryColumns;
} }
if (data.metadata?.subExecution) { if (props.subExecutionOverride) {
metadata.data.push({ subExecution: props.subExecutionOverride });
metadata.hasExecutionIds = true;
} else if (data.metadata?.subExecution) {
metadata.data.push(data.metadata); metadata.data.push(data.metadata);
metadata.hasExecutionIds = true; metadata.hasExecutionIds = true;
} else { } else {

View file

@ -957,6 +957,7 @@
"ndv.output.branch": "Branch", "ndv.output.branch": "Branch",
"ndv.output.executing": "Executing node...", "ndv.output.executing": "Executing node...",
"ndv.output.items": "{count} item | {count} items", "ndv.output.items": "{count} item | {count} items",
"ndv.output.andSubExecutions": ", {count} sub execution | , {count} sub executions",
"ndv.output.noOutputData.message": "n8n stops executing the workflow when a node has no output data. You can change this default behaviour via", "ndv.output.noOutputData.message": "n8n stops executing the workflow when a node has no output data. You can change this default behaviour via",
"ndv.output.noOutputData.message.settings": "Settings", "ndv.output.noOutputData.message.settings": "Settings",
"ndv.output.noOutputData.message.settingsOption": "> \"Always Output Data\".", "ndv.output.noOutputData.message.settingsOption": "> \"Always Output Data\".",
@ -2614,7 +2615,7 @@
"executionUsage.button.upgrade": "Upgrade plan", "executionUsage.button.upgrade": "Upgrade plan",
"executionUsage.expired.text": "Your trial is over. Upgrade now to keep your data.", "executionUsage.expired.text": "Your trial is over. Upgrade now to keep your data.",
"executionUsage.ranOutOfExecutions.text": "Youre out of executions. Upgrade your plan to keep automating.", "executionUsage.ranOutOfExecutions.text": "Youre out of executions. Upgrade your plan to keep automating.",
"openExecution.missingExeuctionId": "Could not find execution. Make sure workflow saving is turned on.", "openExecution.missingExeuctionId": "Make sure this workflow saves executions via the settings.",
"type.string": "String", "type.string": "String",
"type.number": "Number", "type.number": "Number",
"type.dateTime": "Date & Time", "type.dateTime": "Date & Time",

View file

@ -304,6 +304,10 @@ export class ExecuteWorkflow implements INodeType {
} }
} }
this.setMetadata({
subExecutionsCount: items.length,
});
return returnData; return returnData;
} else { } else {
try { try {
@ -334,6 +338,7 @@ export class ExecuteWorkflow implements INodeType {
executionId: executionResult.executionId, executionId: executionResult.executionId,
workflowId: workflowInfo.id ?? (workflowProxy.$workflow.id as string), workflowId: workflowInfo.id ?? (workflowProxy.$workflow.id as string),
}, },
subExecutionsCount: 1,
}); });
if (!waitForSubWorkflow) { if (!waitForSubWorkflow) {

View file

@ -2150,6 +2150,7 @@ export interface ITaskMetadata {
subRun?: ITaskSubRunMetadata[]; subRun?: ITaskSubRunMetadata[];
parentExecution?: RelatedExecution; parentExecution?: RelatedExecution;
subExecution?: RelatedExecution; subExecution?: RelatedExecution;
subExecutionsCount?: number;
} }
// The data that gets returned when a node runs // The data that gets returned when a node runs