mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 18:12:04 +00:00
## Summary - Moved out canvas loading handling to canvas store - Tag editable routes via meta to remove router dependency from generic helpers - Replace all occurrences of `genericHelpers` mixin with composable and audit usage - Moved out `isRedirectSafe` and `getRedirectQueryParameter` out of genericHelpers to remove dependency on router Removing the router dependency is important, because `useRouter` and `useRoute` compostables are only available if called from component instance. So if composable is nested within another composable, we wouldn't be able to use these. In this case we'd always need to inject the router and pass it through several composables. That's why I moved the `readonly` logic to router meta and `isRedirectSafe` and `getRedirectQueryParameter` out as they were only used in a single component. --------- Signed-off-by: Oleg Ivaniv <me@olegivaniv.com>
393 lines
10 KiB
Vue
393 lines
10 KiB
Vue
<template>
|
|
<RunData
|
|
ref="runData"
|
|
:node="node"
|
|
:run-index="runIndex"
|
|
:linked-runs="linkedRuns"
|
|
:can-link-runs="canLinkRuns"
|
|
:too-much-data-title="$locale.baseText('ndv.output.tooMuchData.title')"
|
|
:no-data-in-branch-message="$locale.baseText('ndv.output.noOutputDataInBranch')"
|
|
:is-executing="isNodeRunning"
|
|
:executing-message="$locale.baseText('ndv.output.executing')"
|
|
:session-id="sessionId"
|
|
:block-u-i="blockUI"
|
|
:is-production-execution-preview="isProductionExecutionPreview"
|
|
:is-pane-active="isPaneActive"
|
|
pane-type="output"
|
|
:data-output-type="outputMode"
|
|
@activatePane="activatePane"
|
|
@runChange="onRunIndexChange"
|
|
@linkRun="onLinkRun"
|
|
@unlinkRun="onUnlinkRun"
|
|
@tableMounted="$emit('tableMounted', $event)"
|
|
@itemHover="$emit('itemHover', $event)"
|
|
@search="$emit('search', $event)"
|
|
>
|
|
<template #header>
|
|
<div :class="$style.titleSection">
|
|
<template v-if="hasAiMetadata">
|
|
<n8n-radio-buttons
|
|
v-model="outputMode"
|
|
:options="outputTypes"
|
|
@update:modelValue="onUpdateOutputMode"
|
|
/>
|
|
</template>
|
|
<span v-else :class="$style.title">
|
|
{{ $locale.baseText(outputPanelEditMode.enabled ? 'ndv.output.edit' : 'ndv.output') }}
|
|
</span>
|
|
<RunInfo
|
|
v-if="hasNodeRun && !pinnedData.hasData.value && runsCount === 1"
|
|
v-show="!outputPanelEditMode.enabled"
|
|
:task-data="runTaskData"
|
|
:has-stale-data="staleData"
|
|
:has-pin-data="pinnedData.hasData.value"
|
|
/>
|
|
</div>
|
|
</template>
|
|
|
|
<template #node-not-run>
|
|
<n8n-text v-if="workflowRunning && !isTriggerNode" data-test-id="ndv-output-waiting">{{
|
|
$locale.baseText('ndv.output.waitingToRun')
|
|
}}</n8n-text>
|
|
<n8n-text v-if="!workflowRunning" data-test-id="ndv-output-run-node-hint">
|
|
<template v-if="isSubNodeType.value">
|
|
{{ $locale.baseText('ndv.output.runNodeHintSubNode') }}
|
|
</template>
|
|
<template v-else>
|
|
{{ $locale.baseText('ndv.output.runNodeHint') }}
|
|
<span v-if="canPinData" @click="insertTestData">
|
|
<br />
|
|
{{ $locale.baseText('generic.or') }}
|
|
<n8n-text tag="a" size="medium" color="primary">
|
|
{{ $locale.baseText('ndv.output.insertTestData') }}
|
|
</n8n-text>
|
|
</span>
|
|
</template>
|
|
</n8n-text>
|
|
</template>
|
|
|
|
<template #no-output-data>
|
|
<n8n-text :bold="true" color="text-dark" size="large">{{
|
|
$locale.baseText('ndv.output.noOutputData.title')
|
|
}}</n8n-text>
|
|
<n8n-text>
|
|
{{ $locale.baseText('ndv.output.noOutputData.message') }}
|
|
<a @click="openSettings">{{
|
|
$locale.baseText('ndv.output.noOutputData.message.settings')
|
|
}}</a>
|
|
{{ $locale.baseText('ndv.output.noOutputData.message.settingsOption') }}
|
|
</n8n-text>
|
|
</template>
|
|
|
|
<template v-if="outputMode === 'logs'" #content>
|
|
<RunDataAi :node="node" :run-index="runIndex" />
|
|
</template>
|
|
<template #recovered-artificial-output-data>
|
|
<div :class="$style.recoveredOutputData">
|
|
<n8n-text tag="div" :bold="true" color="text-dark" size="large">{{
|
|
$locale.baseText('executionDetails.executionFailed.recoveredNodeTitle')
|
|
}}</n8n-text>
|
|
<n8n-text>
|
|
{{ $locale.baseText('executionDetails.executionFailed.recoveredNodeMessage') }}
|
|
</n8n-text>
|
|
</div>
|
|
</template>
|
|
|
|
<template v-if="!pinnedData.hasData.value && runsCount > 1" #run-info>
|
|
<RunInfo :task-data="runTaskData" />
|
|
</template>
|
|
</RunData>
|
|
</template>
|
|
|
|
<script lang="ts">
|
|
import { defineComponent } from 'vue';
|
|
import type { IExecutionResponse, INodeUi } from '@/Interface';
|
|
import type { INodeTypeDescription, IRunData, IRunExecutionData, ITaskData } from 'n8n-workflow';
|
|
import RunData from './RunData.vue';
|
|
import RunInfo from './RunInfo.vue';
|
|
import { mapStores, storeToRefs } from 'pinia';
|
|
import { useUIStore } from '@/stores/ui.store';
|
|
import { useWorkflowsStore } from '@/stores/workflows.store';
|
|
import { useNDVStore } from '@/stores/ndv.store';
|
|
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
|
import RunDataAi from './RunDataAi/RunDataAi.vue';
|
|
import { ndvEventBus } from '@/event-bus';
|
|
import { useNodeType } from '@/composables/useNodeType';
|
|
import { usePinnedData } from '@/composables/usePinnedData';
|
|
|
|
type RunDataRef = InstanceType<typeof RunData>;
|
|
|
|
const OUTPUT_TYPE = {
|
|
REGULAR: 'regular',
|
|
LOGS: 'logs',
|
|
};
|
|
|
|
export default defineComponent({
|
|
name: 'OutputPanel',
|
|
components: { RunData, RunInfo, RunDataAi },
|
|
props: {
|
|
runIndex: {
|
|
type: Number,
|
|
required: true,
|
|
},
|
|
isReadOnly: {
|
|
type: Boolean,
|
|
},
|
|
linkedRuns: {
|
|
type: Boolean,
|
|
},
|
|
canLinkRuns: {
|
|
type: Boolean,
|
|
},
|
|
sessionId: {
|
|
type: String,
|
|
},
|
|
blockUI: {
|
|
type: Boolean,
|
|
default: false,
|
|
},
|
|
isProductionExecutionPreview: {
|
|
type: Boolean,
|
|
default: false,
|
|
},
|
|
isPaneActive: {
|
|
type: Boolean,
|
|
default: false,
|
|
},
|
|
},
|
|
setup(props) {
|
|
const ndvStore = useNDVStore();
|
|
const { activeNode } = storeToRefs(ndvStore);
|
|
const { isSubNodeType } = useNodeType({
|
|
node: activeNode,
|
|
});
|
|
const pinnedData = usePinnedData(activeNode, {
|
|
runIndex: props.runIndex,
|
|
displayMode: ndvStore.getPanelDisplayMode('output'),
|
|
});
|
|
|
|
return {
|
|
pinnedData,
|
|
isSubNodeType,
|
|
};
|
|
},
|
|
data() {
|
|
return {
|
|
outputMode: 'regular',
|
|
outputTypes: [
|
|
{ label: this.$locale.baseText('ndv.output.outType.regular'), value: OUTPUT_TYPE.REGULAR },
|
|
{ label: this.$locale.baseText('ndv.output.outType.logs'), value: OUTPUT_TYPE.LOGS },
|
|
],
|
|
};
|
|
},
|
|
computed: {
|
|
...mapStores(useNodeTypesStore, useNDVStore, useUIStore, useWorkflowsStore),
|
|
node(): INodeUi | null {
|
|
return this.ndvStore.activeNode;
|
|
},
|
|
nodeType(): INodeTypeDescription | null {
|
|
if (this.node) {
|
|
return this.nodeTypesStore.getNodeType(this.node.type, this.node.typeVersion);
|
|
}
|
|
return null;
|
|
},
|
|
isTriggerNode(): boolean {
|
|
return this.nodeTypesStore.isTriggerNode(this.node.type);
|
|
},
|
|
hasAiMetadata(): boolean {
|
|
if (this.node) {
|
|
const resultData = this.workflowsStore.getWorkflowResultDataByNodeName(this.node.name);
|
|
|
|
if (!resultData || !Array.isArray(resultData) || resultData.length === 0) {
|
|
return false;
|
|
}
|
|
|
|
return !!resultData[resultData.length - 1].metadata;
|
|
}
|
|
return false;
|
|
},
|
|
isPollingTypeNode(): boolean {
|
|
return !!this.nodeType?.polling;
|
|
},
|
|
isScheduleTrigger(): boolean {
|
|
return !!(this.nodeType && this.nodeType.group.includes('schedule'));
|
|
},
|
|
isNodeRunning(): boolean {
|
|
return this.node && this.workflowsStore.isNodeExecuting(this.node.name);
|
|
},
|
|
workflowRunning(): boolean {
|
|
return this.uiStore.isActionActive('workflowRunning');
|
|
},
|
|
workflowExecution(): IExecutionResponse | null {
|
|
return this.workflowsStore.getWorkflowExecution;
|
|
},
|
|
workflowRunData(): IRunData | null {
|
|
if (this.workflowExecution === null) {
|
|
return null;
|
|
}
|
|
const executionData: IRunExecutionData | undefined = this.workflowExecution.data;
|
|
if (!executionData?.resultData?.runData) {
|
|
return null;
|
|
}
|
|
return executionData.resultData.runData;
|
|
},
|
|
hasNodeRun(): boolean {
|
|
if (this.workflowsStore.subWorkflowExecutionError) return true;
|
|
|
|
return Boolean(
|
|
this.node && this.workflowRunData && this.workflowRunData.hasOwnProperty(this.node.name),
|
|
);
|
|
},
|
|
runTaskData(): ITaskData | null {
|
|
if (!this.node || this.workflowExecution === null) {
|
|
return null;
|
|
}
|
|
|
|
const runData = this.workflowRunData;
|
|
|
|
if (runData === null || !runData.hasOwnProperty(this.node.name)) {
|
|
return null;
|
|
}
|
|
|
|
if (runData[this.node.name].length <= this.runIndex) {
|
|
return null;
|
|
}
|
|
|
|
return runData[this.node.name][this.runIndex];
|
|
},
|
|
runsCount(): number {
|
|
if (this.node === null) {
|
|
return 0;
|
|
}
|
|
|
|
const runData: IRunData | null = this.workflowRunData;
|
|
|
|
if (runData === null || !runData.hasOwnProperty(this.node.name)) {
|
|
return 0;
|
|
}
|
|
|
|
if (runData[this.node.name].length) {
|
|
return runData[this.node.name].length;
|
|
}
|
|
|
|
return 0;
|
|
},
|
|
staleData(): boolean {
|
|
if (!this.node) {
|
|
return false;
|
|
}
|
|
const updatedAt = this.workflowsStore.getParametersLastUpdate(this.node.name);
|
|
if (!updatedAt || !this.runTaskData) {
|
|
return false;
|
|
}
|
|
const runAt = this.runTaskData.startTime;
|
|
return updatedAt > runAt;
|
|
},
|
|
outputPanelEditMode(): { enabled: boolean; value: string } {
|
|
return this.ndvStore.outputPanelEditMode;
|
|
},
|
|
canPinData(): boolean {
|
|
return this.pinnedData.isValidNodeType.value && !this.isReadOnly;
|
|
},
|
|
},
|
|
methods: {
|
|
insertTestData() {
|
|
const runDataRef = this.$refs.runData as RunDataRef | undefined;
|
|
if (runDataRef) {
|
|
runDataRef.enterEditMode({
|
|
origin: 'insertTestDataLink',
|
|
});
|
|
|
|
this.$telemetry.track('User clicked ndv link', {
|
|
workflow_id: this.workflowsStore.workflowId,
|
|
session_id: this.sessionId,
|
|
node_type: this.node.type,
|
|
pane: 'output',
|
|
type: 'insert-test-data',
|
|
});
|
|
}
|
|
},
|
|
onLinkRun() {
|
|
this.$emit('linkRun');
|
|
},
|
|
onUnlinkRun() {
|
|
this.$emit('unlinkRun');
|
|
},
|
|
openSettings() {
|
|
this.$emit('openSettings');
|
|
this.$telemetry.track('User clicked ndv link', {
|
|
node_type: this.node.type,
|
|
workflow_id: this.workflowsStore.workflowId,
|
|
session_id: this.sessionId,
|
|
pane: 'output',
|
|
type: 'settings',
|
|
});
|
|
},
|
|
onRunIndexChange(run: number) {
|
|
this.$emit('runChange', run);
|
|
},
|
|
onUpdateOutputMode(outputMode: (typeof OUTPUT_TYPE)[string]) {
|
|
if (outputMode === OUTPUT_TYPE.LOGS) {
|
|
ndvEventBus.emit('setPositionByName', 'minLeft');
|
|
} else {
|
|
ndvEventBus.emit('setPositionByName', 'initial');
|
|
}
|
|
},
|
|
activatePane() {
|
|
this.$emit('activatePane');
|
|
},
|
|
},
|
|
});
|
|
</script>
|
|
|
|
<style lang="scss" module>
|
|
// The items count and displayModes are rendered in the RunData component
|
|
// this is a workaround to hide it in the output panel(for ai type) to not add unnecessary one-time props
|
|
:global([data-output-type='logs'] [class*='itemsCount']),
|
|
:global([data-output-type='logs'] [class*='displayModes']) {
|
|
display: none;
|
|
}
|
|
.outputTypeSelect {
|
|
margin-bottom: var(--spacing-4xs);
|
|
width: fit-content;
|
|
}
|
|
.titleSection {
|
|
display: flex;
|
|
align-items: center;
|
|
|
|
> * {
|
|
margin-right: var(--spacing-2xs);
|
|
}
|
|
}
|
|
|
|
.title {
|
|
text-transform: uppercase;
|
|
color: var(--color-text-light);
|
|
letter-spacing: 3px;
|
|
font-weight: var(--font-weight-bold);
|
|
font-size: var(--font-size-s);
|
|
}
|
|
|
|
.noOutputData {
|
|
max-width: 180px;
|
|
|
|
> *:first-child {
|
|
margin-bottom: var(--spacing-m);
|
|
}
|
|
|
|
> * {
|
|
margin-bottom: var(--spacing-2xs);
|
|
}
|
|
}
|
|
|
|
.recoveredOutputData {
|
|
margin: auto;
|
|
max-width: 250px;
|
|
text-align: center;
|
|
|
|
> *:first-child {
|
|
margin-bottom: var(--spacing-m);
|
|
}
|
|
}
|
|
</style>
|