mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -08:00
feat: bring back UI
This commit is contained in:
parent
67e09e2988
commit
152ca97719
|
@ -182,6 +182,7 @@ export interface IAiDataContent {
|
||||||
metadata: {
|
metadata: {
|
||||||
executionTime: number;
|
executionTime: number;
|
||||||
startTime: number;
|
startTime: number;
|
||||||
|
executionId?: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -58,12 +58,13 @@ import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||||
import { useNodeHelpers } from '@/composables/useNodeHelpers';
|
import { useNodeHelpers } from '@/composables/useNodeHelpers';
|
||||||
import { useNodeType } from '@/composables/useNodeType';
|
import { useNodeType } from '@/composables/useNodeType';
|
||||||
import { useToast } from '@/composables/useToast';
|
import { useToast } from '@/composables/useToast';
|
||||||
import { isEqual, isObject } from 'lodash-es';
|
import { get, isEqual, isObject } from 'lodash-es';
|
||||||
import { useExternalHooks } from '@/composables/useExternalHooks';
|
import { useExternalHooks } from '@/composables/useExternalHooks';
|
||||||
import { useSourceControlStore } from '@/stores/sourceControl.store';
|
import { useSourceControlStore } from '@/stores/sourceControl.store';
|
||||||
import { useRootStore } from '@/stores/root.store';
|
import { useRootStore } from '@/stores/root.store';
|
||||||
import RunDataPinButton from '@/components/RunDataPinButton.vue';
|
import RunDataPinButton from '@/components/RunDataPinButton.vue';
|
||||||
import { getGenericHints } from '@/utils/nodeViewUtils';
|
import { getGenericHints } from '@/utils/nodeViewUtils';
|
||||||
|
import { useExecutionHelpers } from '@/composables/useExecutionHelpers';
|
||||||
|
|
||||||
const LazyRunDataTable = defineAsyncComponent(
|
const LazyRunDataTable = defineAsyncComponent(
|
||||||
async () => await import('@/components/RunDataTable.vue'),
|
async () => await import('@/components/RunDataTable.vue'),
|
||||||
|
@ -180,6 +181,7 @@ export default defineComponent({
|
||||||
const { isSubNodeType } = useNodeType({
|
const { isSubNodeType } = useNodeType({
|
||||||
node,
|
node,
|
||||||
});
|
});
|
||||||
|
const { openExecutionInNewTab } = useExecutionHelpers();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...useToast(),
|
...useToast(),
|
||||||
|
@ -187,6 +189,7 @@ export default defineComponent({
|
||||||
nodeHelpers,
|
nodeHelpers,
|
||||||
pinnedData,
|
pinnedData,
|
||||||
isSubNodeType,
|
isSubNodeType,
|
||||||
|
openExecutionInNewTab,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
|
@ -220,6 +223,43 @@ export default defineComponent({
|
||||||
useSourceControlStore,
|
useSourceControlStore,
|
||||||
useRootStore,
|
useRootStore,
|
||||||
),
|
),
|
||||||
|
subWorkflowData(): { executionId: string; workflowId?: string } | null {
|
||||||
|
if (!this.node) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const metadata = get(this.workflowRunData, [this.node.name, this.runIndex, 'metadata'], null);
|
||||||
|
if (metadata?.executionId) {
|
||||||
|
return {
|
||||||
|
executionId: metadata?.executionId,
|
||||||
|
workflowId: metadata?.workflowId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
itemSubWorkflowData(): Array<{
|
||||||
|
metadata?: { executionId?: string; workflowId?: string };
|
||||||
|
itemIndex: number;
|
||||||
|
}> {
|
||||||
|
const items = this.getData(this.runIndex, this.currentOutputIndex);
|
||||||
|
return items
|
||||||
|
.map((item, itemIndex) => {
|
||||||
|
return {
|
||||||
|
metadata: item.metadata,
|
||||||
|
itemIndex,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.filter((item) => {
|
||||||
|
return !!item.metadata;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
hasInputOverwrite(): boolean {
|
||||||
|
if (!this.node) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const taskData = this.nodeHelpers.getNodeTaskData(this.node, this.runIndex);
|
||||||
|
if (taskData === null) return false;
|
||||||
|
return !!taskData.inputOverride;
|
||||||
|
},
|
||||||
isReadOnlyRoute() {
|
isReadOnlyRoute() {
|
||||||
return this.$route?.meta?.readOnlyCanvas === true;
|
return this.$route?.meta?.readOnlyCanvas === true;
|
||||||
},
|
},
|
||||||
|
@ -654,6 +694,15 @@ export default defineComponent({
|
||||||
this.hidePinDataDiscoveryTooltip();
|
this.hidePinDataDiscoveryTooltip();
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
getData(
|
||||||
|
runIndex: number,
|
||||||
|
outputIndex: number,
|
||||||
|
connectionType: NodeConnectionType = NodeConnectionType.Main,
|
||||||
|
) {
|
||||||
|
const rawInputData = this.getRawInputData(runIndex, outputIndex, connectionType);
|
||||||
|
const pinOrLiveData = this.getPinDataOrLiveData(rawInputData);
|
||||||
|
return this.getFilteredData(pinOrLiveData);
|
||||||
|
},
|
||||||
getResolvedNodeOutputs() {
|
getResolvedNodeOutputs() {
|
||||||
if (this.node && this.nodeType) {
|
if (this.node && this.nodeType) {
|
||||||
const workflowNode = this.workflow.getNode(this.node.name);
|
const workflowNode = this.workflow.getNode(this.node.name);
|
||||||
|
@ -1383,6 +1432,32 @@ export default defineComponent({
|
||||||
<n8n-text size="small" v-n8n-html="hint.message"></n8n-text>
|
<n8n-text size="small" v-n8n-html="hint.message"></n8n-text>
|
||||||
</n8n-callout>
|
</n8n-callout>
|
||||||
|
|
||||||
|
<div :class="$style.execution">
|
||||||
|
<n8n-button
|
||||||
|
v-if="subWorkflowData && !(paneType === 'input' && hasInputOverwrite)"
|
||||||
|
:label="`Open ${
|
||||||
|
nodeType?.group?.includes('trigger') ? 'parent' : 'sub'
|
||||||
|
} execution ${subWorkflowData.executionId}`"
|
||||||
|
type="secondary"
|
||||||
|
@click.stop="openExecutionInNewTab(subWorkflowData.executionId, subWorkflowData.workflowId)"
|
||||||
|
/>
|
||||||
|
<div v-if="itemSubWorkflowData.length">
|
||||||
|
<n8n-select
|
||||||
|
size="small"
|
||||||
|
:class="$style.longSelect"
|
||||||
|
model-value="Select exectuion"
|
||||||
|
@change="openExecutionInNewTab($event.metadata.executionId, $event.metadata.workflowId)"
|
||||||
|
>
|
||||||
|
<n8n-option
|
||||||
|
v-for="subWorkflow in itemSubWorkflowData"
|
||||||
|
:key="subWorkflow.metadata?.executionId"
|
||||||
|
:value="subWorkflow"
|
||||||
|
:label="`item#${subWorkflow.itemIndex} exec#${subWorkflow.metadata?.executionId}`"
|
||||||
|
/>
|
||||||
|
</n8n-select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-if="maxOutputIndex > 0 && branches.length > 1 && !displaysMultipleNodes"
|
v-if="maxOutputIndex > 0 && branches.length > 1 && !displaysMultipleNodes"
|
||||||
:class="$style.outputs"
|
:class="$style.outputs"
|
||||||
|
|
|
@ -61,6 +61,7 @@ function getReferencedData(
|
||||||
metadata: {
|
metadata: {
|
||||||
executionTime: taskData.executionTime,
|
executionTime: taskData.executionTime,
|
||||||
startTime: taskData.startTime,
|
startTime: taskData.startTime,
|
||||||
|
executionId: taskData.metadata?.executionId,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -11,6 +11,7 @@ import type {
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
import NodeIcon from '@/components/NodeIcon.vue';
|
import NodeIcon from '@/components/NodeIcon.vue';
|
||||||
import AiRunContentBlock from './AiRunContentBlock.vue';
|
import AiRunContentBlock from './AiRunContentBlock.vue';
|
||||||
|
import { useExecutionHelpers } from '@/composables/useExecutionHelpers';
|
||||||
|
|
||||||
interface RunMeta {
|
interface RunMeta {
|
||||||
startTimeMs: number;
|
startTimeMs: number;
|
||||||
|
@ -18,6 +19,7 @@ interface RunMeta {
|
||||||
node: INodeTypeDescription | null;
|
node: INodeTypeDescription | null;
|
||||||
type: 'input' | 'output';
|
type: 'input' | 'output';
|
||||||
connectionType: NodeConnectionType;
|
connectionType: NodeConnectionType;
|
||||||
|
executionId?: string;
|
||||||
}
|
}
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
inputData: IAiData;
|
inputData: IAiData;
|
||||||
|
@ -27,6 +29,8 @@ const props = defineProps<{
|
||||||
const nodeTypesStore = useNodeTypesStore();
|
const nodeTypesStore = useNodeTypesStore();
|
||||||
const workflowsStore = useWorkflowsStore();
|
const workflowsStore = useWorkflowsStore();
|
||||||
|
|
||||||
|
const { openExecutionInNewTab } = useExecutionHelpers();
|
||||||
|
|
||||||
type TokenUsageData = {
|
type TokenUsageData = {
|
||||||
completionTokens: number;
|
completionTokens: number;
|
||||||
promptTokens: number;
|
promptTokens: number;
|
||||||
|
@ -75,6 +79,7 @@ function extractRunMeta(run: IAiDataContent) {
|
||||||
node: nodeType,
|
node: nodeType,
|
||||||
type: run.inOut,
|
type: run.inOut,
|
||||||
connectionType: run.type,
|
connectionType: run.type,
|
||||||
|
executionId: run.metadata.executionId,
|
||||||
};
|
};
|
||||||
|
|
||||||
return runMeta;
|
return runMeta;
|
||||||
|
@ -131,6 +136,11 @@ const outputError = computed(() => {
|
||||||
}}
|
}}
|
||||||
</n8n-tooltip>
|
</n8n-tooltip>
|
||||||
</li>
|
</li>
|
||||||
|
<li v-if="runMeta?.executionId">
|
||||||
|
<a @click="openExecutionInNewTab(runMeta?.executionId)"
|
||||||
|
>EXECUTION ID {{ runMeta?.executionId }}</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
<li v-if="(consumedTokensSum?.totalTokens ?? 0) > 0" :class="$style.tokensUsage">
|
<li v-if="(consumedTokensSum?.totalTokens ?? 0) > 0" :class="$style.tokensUsage">
|
||||||
{{
|
{{
|
||||||
$locale.baseText('runData.aiContentBlock.tokens', {
|
$locale.baseText('runData.aiContentBlock.tokens', {
|
||||||
|
|
|
@ -12,7 +12,6 @@ import {
|
||||||
CHAT_TRIGGER_NODE_TYPE,
|
CHAT_TRIGGER_NODE_TYPE,
|
||||||
MANUAL_CHAT_TRIGGER_NODE_TYPE,
|
MANUAL_CHAT_TRIGGER_NODE_TYPE,
|
||||||
MODAL_CONFIRM,
|
MODAL_CONFIRM,
|
||||||
VIEWS,
|
|
||||||
WORKFLOW_LM_CHAT_MODAL_KEY,
|
WORKFLOW_LM_CHAT_MODAL_KEY,
|
||||||
} from '@/constants';
|
} from '@/constants';
|
||||||
|
|
||||||
|
@ -58,6 +57,7 @@ import { usePinnedData } from '@/composables/usePinnedData';
|
||||||
import { get, last } from 'lodash-es';
|
import { get, last } from 'lodash-es';
|
||||||
import { isEmpty } from '@/utils/typesUtils';
|
import { isEmpty } from '@/utils/typesUtils';
|
||||||
import { chatEventBus } from '@n8n/chat/event-buses';
|
import { chatEventBus } from '@n8n/chat/event-buses';
|
||||||
|
import { useExecutionHelpers } from '@/composables/useExecutionHelpers';
|
||||||
|
|
||||||
const LazyRunDataAi = defineAsyncComponent(
|
const LazyRunDataAi = defineAsyncComponent(
|
||||||
async () => await import('@/components/RunDataAi/RunDataAi.vue'),
|
async () => await import('@/components/RunDataAi/RunDataAi.vue'),
|
||||||
|
@ -82,6 +82,8 @@ const workflowsStore = useWorkflowsStore();
|
||||||
const nodeTypesStore = useNodeTypesStore();
|
const nodeTypesStore = useNodeTypesStore();
|
||||||
const uiStore = useUIStore();
|
const uiStore = useUIStore();
|
||||||
|
|
||||||
|
const executionHelpers = useExecutionHelpers();
|
||||||
|
|
||||||
const { showError } = useToast();
|
const { showError } = useToast();
|
||||||
const messages: Ref<ChatMessage[]> = ref([]);
|
const messages: Ref<ChatMessage[]> = ref([]);
|
||||||
const currentSessionId = ref<string>(String(Date.now()));
|
const currentSessionId = ref<string>(String(Date.now()));
|
||||||
|
@ -428,11 +430,7 @@ async function sendMessage(message: string, files?: File[]) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function displayExecution(executionId: string) {
|
function displayExecution(executionId: string) {
|
||||||
const route = router.resolve({
|
executionHelpers.openExecutionInNewTab(executionId, workflow.value.id);
|
||||||
name: VIEWS.EXECUTION_PREVIEW,
|
|
||||||
params: { name: workflow.value.id, executionId },
|
|
||||||
});
|
|
||||||
window.open(route.href, '_blank');
|
|
||||||
}
|
}
|
||||||
function isTextMessage(message: ChatMessage): message is ChatMessageText {
|
function isTextMessage(message: ChatMessage): message is ChatMessageText {
|
||||||
return message.type === 'text' || !message.type;
|
return message.type === 'text' || !message.type;
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
import { ref, computed, useCssModule } from 'vue';
|
import { ref, computed, useCssModule } from 'vue';
|
||||||
import type { ExecutionSummary } from 'n8n-workflow';
|
import type { ExecutionSummary } from 'n8n-workflow';
|
||||||
import { useI18n } from '@/composables/useI18n';
|
import { useI18n } from '@/composables/useI18n';
|
||||||
import { VIEWS, WAIT_TIME_UNLIMITED } from '@/constants';
|
import { WAIT_TIME_UNLIMITED } from '@/constants';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
import { convertToDisplayDate } from '@/utils/formatters/dateFormatter';
|
import { convertToDisplayDate } from '@/utils/formatters/dateFormatter';
|
||||||
import { i18n as locale } from '@/plugins/i18n';
|
import { i18n as locale } from '@/plugins/i18n';
|
||||||
|
@ -138,11 +138,7 @@ function formatDate(fullDate: Date | string | number) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function displayExecution() {
|
function displayExecution() {
|
||||||
const route = router.resolve({
|
executionHelpers.openExecutionInNewTab(props.execution.id, props.execution.workflowId);
|
||||||
name: VIEWS.EXECUTION_PREVIEW,
|
|
||||||
params: { name: props.execution.workflowId, executionId: props.execution.id },
|
|
||||||
});
|
|
||||||
window.open(route.href, '_blank');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function onStopExecution() {
|
function onStopExecution() {
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
import type { ExecutionSummary } from 'n8n-workflow';
|
import type { ExecutionSummary } from 'n8n-workflow';
|
||||||
import { convertToDisplayDate } from '@/utils/formatters/dateFormatter';
|
import { convertToDisplayDate } from '@/utils/formatters/dateFormatter';
|
||||||
import { useI18n } from '@/composables/useI18n';
|
import { useI18n } from '@/composables/useI18n';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
import { VIEWS } from '@/constants';
|
||||||
|
|
||||||
export interface IExecutionUIData {
|
export interface IExecutionUIData {
|
||||||
name: string;
|
name: string;
|
||||||
|
@ -14,6 +16,7 @@ export interface IExecutionUIData {
|
||||||
|
|
||||||
export function useExecutionHelpers() {
|
export function useExecutionHelpers() {
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
function getUIDetails(execution: ExecutionSummary): IExecutionUIData {
|
function getUIDetails(execution: ExecutionSummary): IExecutionUIData {
|
||||||
const status = {
|
const status = {
|
||||||
|
@ -69,9 +72,18 @@ export function useExecutionHelpers() {
|
||||||
return ['crashed', 'error'].includes(execution.status) && !execution.retrySuccessId;
|
return ['crashed', 'error'].includes(execution.status) && !execution.retrySuccessId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openExecutionInNewTab(executionId: string, workflowId?: string) {
|
||||||
|
const route = router.resolve({
|
||||||
|
name: VIEWS.EXECUTION_PREVIEW,
|
||||||
|
params: { name: workflowId, executionId },
|
||||||
|
});
|
||||||
|
window.open(route.href, '_blank');
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
getUIDetails,
|
getUIDetails,
|
||||||
formatDate,
|
formatDate,
|
||||||
isExecutionRetriable,
|
isExecutionRetriable,
|
||||||
|
openExecutionInNewTab,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -565,6 +565,29 @@ export function useNodeHelpers() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getNodeTaskData(node: INodeUi | null, runIndex = 0) {
|
||||||
|
if (node === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (workflowsStore.getWorkflowExecution === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const executionData = workflowsStore.getWorkflowExecution.data;
|
||||||
|
if (!executionData?.resultData) {
|
||||||
|
// unknown status
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const runData = executionData.resultData.runData;
|
||||||
|
|
||||||
|
const taskData = get(runData, [node.name, runIndex]);
|
||||||
|
if (!taskData) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return taskData;
|
||||||
|
}
|
||||||
|
|
||||||
function getNodeInputData(
|
function getNodeInputData(
|
||||||
node: INodeUi | null,
|
node: INodeUi | null,
|
||||||
runIndex = 0,
|
runIndex = 0,
|
||||||
|
@ -582,22 +605,8 @@ export function useNodeHelpers() {
|
||||||
runIndex = runIndex - 1;
|
runIndex = runIndex - 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (node === null) {
|
const taskData = getNodeTaskData(node, runIndex);
|
||||||
return [];
|
if (taskData === null) {
|
||||||
}
|
|
||||||
if (workflowsStore.getWorkflowExecution === null) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const executionData = workflowsStore.getWorkflowExecution.data;
|
|
||||||
if (!executionData?.resultData) {
|
|
||||||
// unknown status
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
const runData = executionData.resultData.runData;
|
|
||||||
|
|
||||||
const taskData = get(runData, [node.name, runIndex]);
|
|
||||||
if (!taskData) {
|
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1281,5 +1290,6 @@ export function useNodeHelpers() {
|
||||||
removeConnectionByConnectionInfo,
|
removeConnectionByConnectionInfo,
|
||||||
addPinDataConnections,
|
addPinDataConnections,
|
||||||
removePinDataConnections,
|
removePinDataConnections,
|
||||||
|
getNodeTaskData,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue