mirror of
https://github.com/n8n-io/n8n.git
synced 2024-11-14 00:24:07 -08:00
harmonies node statuses
Co-authored-by: Federico Meini <fedme@users.noreply.github.com>
This commit is contained in:
parent
d7ba206b30
commit
5a715a2fc4
|
@ -64,6 +64,8 @@ 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 { retry } from '../__tests__/utils';
|
||||||
|
import { continueOnFail } from '../../../core/src/NodeExecuteFunctions';
|
||||||
|
|
||||||
const LazyRunDataTable = defineAsyncComponent(
|
const LazyRunDataTable = defineAsyncComponent(
|
||||||
async () => await import('@/components/RunDataTable.vue'),
|
async () => await import('@/components/RunDataTable.vue'),
|
||||||
|
@ -727,6 +729,69 @@ export default defineComponent({
|
||||||
|
|
||||||
return [];
|
return [];
|
||||||
},
|
},
|
||||||
|
getNodeSettingsHints(): NodeHint[] {
|
||||||
|
const hints: NodeHint[] = [];
|
||||||
|
if (this.node?.disabled) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
message: 'This node is disabled, and will simply pass the input through.',
|
||||||
|
type: 'info',
|
||||||
|
whenToDisplay: 'beforeExecution',
|
||||||
|
location: 'outputPane',
|
||||||
|
icon: 'ban',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
this.canPinData &&
|
||||||
|
this.pinnedData.hasData.value &&
|
||||||
|
!this.editMode.enabled &&
|
||||||
|
!this.isProductionExecutionPreview
|
||||||
|
) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
if (this.node && this.node.alwaysOutputData) {
|
||||||
|
hints.push({
|
||||||
|
message: 'This node will output an empty item if nothing would normally be returned.',
|
||||||
|
type: 'info',
|
||||||
|
whenToDisplay: 'beforeExecution',
|
||||||
|
location: 'outputPane',
|
||||||
|
icon: 'circle',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (this.node && this.node.executeOnce) {
|
||||||
|
hints.push({
|
||||||
|
message: 'This node will execute only once, no matter how many input items there are.',
|
||||||
|
type: 'info',
|
||||||
|
whenToDisplay: 'beforeExecution',
|
||||||
|
location: 'outputPane',
|
||||||
|
icon: 'dice-one',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (this.node && this.node.retryOnFail) {
|
||||||
|
hints.push({
|
||||||
|
message: 'This node will automatically retry if it fails.',
|
||||||
|
type: 'info',
|
||||||
|
whenToDisplay: 'beforeExecution',
|
||||||
|
location: 'outputPane',
|
||||||
|
icon: 'retweet',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
this.node &&
|
||||||
|
(this.node.onError === 'continueRegularOutput' ||
|
||||||
|
this.node.onError === 'continueErrorOutput')
|
||||||
|
) {
|
||||||
|
hints.push({
|
||||||
|
message: 'The workflow will continue executing even if the node fails.',
|
||||||
|
type: 'info',
|
||||||
|
whenToDisplay: 'beforeExecution',
|
||||||
|
location: 'outputPane',
|
||||||
|
icon: 'arrow-right',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return hints;
|
||||||
|
},
|
||||||
onItemHover(itemIndex: number | null) {
|
onItemHover(itemIndex: number | null) {
|
||||||
if (itemIndex === null) {
|
if (itemIndex === null) {
|
||||||
this.$emit('itemHover', null);
|
this.$emit('itemHover', null);
|
||||||
|
@ -1208,41 +1273,6 @@ export default defineComponent({
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div :class="['run-data', $style.container]" @mouseover="activatePane">
|
<div :class="['run-data', $style.container]" @mouseover="activatePane">
|
||||||
<n8n-callout
|
|
||||||
v-if="
|
|
||||||
canPinData && pinnedData.hasData.value && !editMode.enabled && !isProductionExecutionPreview
|
|
||||||
"
|
|
||||||
theme="secondary"
|
|
||||||
icon="thumbtack"
|
|
||||||
:class="$style.pinnedDataCallout"
|
|
||||||
>
|
|
||||||
{{ $locale.baseText('runData.pindata.thisDataIsPinned') }}
|
|
||||||
<span v-if="!isReadOnlyRoute && !readOnlyEnv" class="ml-4xs">
|
|
||||||
<n8n-link
|
|
||||||
theme="secondary"
|
|
||||||
size="small"
|
|
||||||
underline
|
|
||||||
bold
|
|
||||||
data-test-id="ndv-unpin-data"
|
|
||||||
@click.stop="onTogglePinData({ source: 'banner-link' })"
|
|
||||||
>
|
|
||||||
{{ $locale.baseText('runData.pindata.unpin') }}
|
|
||||||
</n8n-link>
|
|
||||||
</span>
|
|
||||||
<template #trailingContent>
|
|
||||||
<n8n-link
|
|
||||||
:to="dataPinningDocsUrl"
|
|
||||||
size="small"
|
|
||||||
theme="secondary"
|
|
||||||
bold
|
|
||||||
underline
|
|
||||||
@click="onClickDataPinningDocsLink"
|
|
||||||
>
|
|
||||||
{{ $locale.baseText('runData.pindata.learnMore') }}
|
|
||||||
</n8n-link>
|
|
||||||
</template>
|
|
||||||
</n8n-callout>
|
|
||||||
|
|
||||||
<BinaryDataDisplay
|
<BinaryDataDisplay
|
||||||
v-if="binaryDataDisplayData"
|
v-if="binaryDataDisplayData"
|
||||||
:window-visible="binaryDataDisplayVisible"
|
:window-visible="binaryDataDisplayVisible"
|
||||||
|
@ -1370,7 +1400,49 @@ export default defineComponent({
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<slot v-if="!displaysMultipleNodes" name="before-data" />
|
<slot v-if="!displaysMultipleNodes" name="before-data" />
|
||||||
|
<n8n-callout
|
||||||
|
v-if="
|
||||||
|
canPinData && pinnedData.hasData.value && !editMode.enabled && !isProductionExecutionPreview
|
||||||
|
"
|
||||||
|
:class="$style.hintCallout"
|
||||||
|
theme="info"
|
||||||
|
icon="thumbtack"
|
||||||
|
>
|
||||||
|
{{ $locale.baseText('runData.pindata.thisDataIsPinned') }}
|
||||||
|
<span v-if="!isReadOnlyRoute && !readOnlyEnv" class="ml-4xs">
|
||||||
|
<n8n-link
|
||||||
|
theme="secondary"
|
||||||
|
size="small"
|
||||||
|
underline
|
||||||
|
bold
|
||||||
|
data-test-id="ndv-unpin-data"
|
||||||
|
@click.stop="onTogglePinData({ source: 'banner-link' })"
|
||||||
|
>
|
||||||
|
{{ $locale.baseText('runData.pindata.unpin') }}
|
||||||
|
</n8n-link>
|
||||||
|
</span>
|
||||||
|
<template #trailingContent>
|
||||||
|
<n8n-link
|
||||||
|
:to="dataPinningDocsUrl"
|
||||||
|
size="small"
|
||||||
|
theme="secondary"
|
||||||
|
bold
|
||||||
|
underline
|
||||||
|
@click="onClickDataPinningDocsLink"
|
||||||
|
>
|
||||||
|
{{ $locale.baseText('runData.pindata.learnMore') }}
|
||||||
|
</n8n-link>
|
||||||
|
</template>
|
||||||
|
</n8n-callout>
|
||||||
|
<n8n-callout
|
||||||
|
v-for="hint in getNodeSettingsHints()"
|
||||||
|
:key="hint.message"
|
||||||
|
:class="$style.hintCallout"
|
||||||
|
:theme="hint.type || 'info'"
|
||||||
|
:icon="hint.icon"
|
||||||
|
>
|
||||||
|
<n8n-text size="small" v-n8n-html="hint.message"></n8n-text>
|
||||||
|
</n8n-callout>
|
||||||
<n8n-callout
|
<n8n-callout
|
||||||
v-for="hint in getNodeHints()"
|
v-for="hint in getNodeHints()"
|
||||||
:key="hint.message"
|
:key="hint.message"
|
||||||
|
|
|
@ -4,10 +4,12 @@ import { useNodeConnections } from '@/composables/useNodeConnections';
|
||||||
import { useI18n } from '@/composables/useI18n';
|
import { useI18n } from '@/composables/useI18n';
|
||||||
import CanvasNodeDisabledStrikeThrough from './parts/CanvasNodeDisabledStrikeThrough.vue';
|
import CanvasNodeDisabledStrikeThrough from './parts/CanvasNodeDisabledStrikeThrough.vue';
|
||||||
import CanvasNodeStatusIcons from '@/components/canvas/elements/nodes/render-types/parts/CanvasNodeStatusIcons.vue';
|
import CanvasNodeStatusIcons from '@/components/canvas/elements/nodes/render-types/parts/CanvasNodeStatusIcons.vue';
|
||||||
|
import CanvasNodeSettingsIcons from '@/components/canvas/elements/nodes/render-types/parts/CanvasNodeSettingsIcons.vue';
|
||||||
import { useCanvasNode } from '@/composables/useCanvasNode';
|
import { useCanvasNode } from '@/composables/useCanvasNode';
|
||||||
import { NODE_INSERT_SPACER_BETWEEN_INPUT_GROUPS } from '@/constants';
|
import { NODE_INSERT_SPACER_BETWEEN_INPUT_GROUPS } from '@/constants';
|
||||||
import { N8nTooltip } from 'n8n-design-system';
|
import { N8nTooltip } from 'n8n-design-system';
|
||||||
import type { CanvasNodeDefaultRender } from '@/types';
|
import type { CanvasNodeDefaultRender } from '@/types';
|
||||||
|
import { useNodeHelpers } from '@/composables/useNodeHelpers';
|
||||||
|
|
||||||
const $style = useCssModule();
|
const $style = useCssModule();
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
|
@ -45,6 +47,8 @@ const {
|
||||||
connections,
|
connections,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const nodeHelpers = useNodeHelpers();
|
||||||
|
|
||||||
const renderOptions = computed(() => render.value.options as CanvasNodeDefaultRender['options']);
|
const renderOptions = computed(() => render.value.options as CanvasNodeDefaultRender['options']);
|
||||||
|
|
||||||
const classes = computed(() => {
|
const classes = computed(() => {
|
||||||
|
@ -122,6 +126,10 @@ function openContextMenu(event: MouseEvent) {
|
||||||
</div>
|
</div>
|
||||||
</N8nTooltip>
|
</N8nTooltip>
|
||||||
<CanvasNodeStatusIcons v-if="!isDisabled" :class="$style.statusIcons" />
|
<CanvasNodeStatusIcons v-if="!isDisabled" :class="$style.statusIcons" />
|
||||||
|
<CanvasNodeSettingsIcons
|
||||||
|
v-if="!isDisabled && !(hasPinnedData && !nodeHelpers.isProductionExecutionPreview.value)"
|
||||||
|
:class="$style.settingsIcons"
|
||||||
|
/>
|
||||||
<CanvasNodeDisabledStrikeThrough v-if="isStrikethroughVisible" />
|
<CanvasNodeDisabledStrikeThrough v-if="isStrikethroughVisible" />
|
||||||
<div :class="$style.description">
|
<div :class="$style.description">
|
||||||
<div v-if="label" :class="$style.label">
|
<div v-if="label" :class="$style.label">
|
||||||
|
@ -217,6 +225,10 @@ function openContextMenu(event: MouseEvent) {
|
||||||
right: calc(-1 * var(--spacing-2xs));
|
right: calc(-1 * var(--spacing-2xs));
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
}
|
}
|
||||||
|
.settingsIcons {
|
||||||
|
right: calc(-1 * var(--spacing-2xs));
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -299,6 +311,14 @@ function openContextMenu(event: MouseEvent) {
|
||||||
right: var(--canvas-node--status-icons-offset);
|
right: var(--canvas-node--status-icons-offset);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.settingsIcons {
|
||||||
|
display: flex;
|
||||||
|
gap: 3px;
|
||||||
|
position: absolute;
|
||||||
|
top: var(--canvas-node--status-icons-offset);
|
||||||
|
right: var(--canvas-node--status-icons-offset);
|
||||||
|
}
|
||||||
|
|
||||||
.triggerIcon {
|
.triggerIcon {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 100%;
|
right: 100%;
|
||||||
|
|
|
@ -0,0 +1,99 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import { useCanvasNode } from '@/composables/useCanvasNode';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers';
|
||||||
|
|
||||||
|
const { name } = useCanvasNode();
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const workflowHelpers = useWorkflowHelpers({ router });
|
||||||
|
const workflow = computed(() => workflowHelpers.getCurrentWorkflow());
|
||||||
|
const node = computed(() => workflow.value.getNode(name.value));
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
v-if="node?.onError === 'continueRegularOutput' || node?.onError === 'continueErrorOutput'"
|
||||||
|
data-test-id="canvas-node-status-execute-once"
|
||||||
|
:class="[$style.status, $style.pinnedData]"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon="arrow-right" />
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="node?.retryOnFail"
|
||||||
|
data-test-id="canvas-node-status-execute-once"
|
||||||
|
:class="[$style.status, $style.pinnedData]"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon="retweet" />
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="node?.executeOnce"
|
||||||
|
data-test-id="canvas-node-status-execute-once"
|
||||||
|
:class="[$style.status, $style.pinnedData]"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon="dice-one" />
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="node?.alwaysOutputData"
|
||||||
|
data-test-id="canvas-node-status-always-output-data"
|
||||||
|
:class="[$style.status, $style.pinnedData]"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon="circle" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" module>
|
||||||
|
.status {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-5xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.runData {
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--color-success);
|
||||||
|
}
|
||||||
|
|
||||||
|
.waiting {
|
||||||
|
color: var(--color-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pinnedData {
|
||||||
|
color: var(--color-text-light);
|
||||||
|
font-size: var(--font-size-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.running {
|
||||||
|
width: calc(100% - 2 * var(--canvas-node--status-icons-offset));
|
||||||
|
height: calc(100% - 2 * var(--canvas-node--status-icons-offset));
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 3.75em;
|
||||||
|
color: hsla(var(--color-primary-h), var(--color-primary-s), var(--color-primary-l), 0.7);
|
||||||
|
}
|
||||||
|
.node-waiting-spinner {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 3.75em;
|
||||||
|
color: hsla(var(--color-primary-h), var(--color-primary-s), var(--color-primary-l), 0.7);
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: absolute;
|
||||||
|
left: -34px;
|
||||||
|
top: -34px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.issues {
|
||||||
|
color: var(--color-danger);
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.count {
|
||||||
|
font-size: var(--font-size-s);
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -3,6 +3,8 @@ import { computed } from 'vue';
|
||||||
import TitledList from '@/components/TitledList.vue';
|
import TitledList from '@/components/TitledList.vue';
|
||||||
import { useNodeHelpers } from '@/composables/useNodeHelpers';
|
import { useNodeHelpers } from '@/composables/useNodeHelpers';
|
||||||
import { useCanvasNode } from '@/composables/useCanvasNode';
|
import { useCanvasNode } from '@/composables/useCanvasNode';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers';
|
||||||
|
|
||||||
const nodeHelpers = useNodeHelpers();
|
const nodeHelpers = useNodeHelpers();
|
||||||
|
|
||||||
|
@ -16,9 +18,13 @@ const {
|
||||||
hasRunData,
|
hasRunData,
|
||||||
runDataIterations,
|
runDataIterations,
|
||||||
isDisabled,
|
isDisabled,
|
||||||
|
name,
|
||||||
} = useCanvasNode();
|
} = useCanvasNode();
|
||||||
|
|
||||||
const hideNodeIssues = computed(() => false); // @TODO Implement this
|
const hideNodeIssues = computed(() => false); // @TODO Implement this
|
||||||
|
const router = useRouter();
|
||||||
|
const workflowHelpers = useWorkflowHelpers({ router });
|
||||||
|
const workflow = workflowHelpers.getCurrentWorkflow();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
|
@ -1602,7 +1602,7 @@
|
||||||
"runData.startTime": "Start Time",
|
"runData.startTime": "Start Time",
|
||||||
"runData.table": "Table",
|
"runData.table": "Table",
|
||||||
"runData.pindata.learnMore": "Learn more",
|
"runData.pindata.learnMore": "Learn more",
|
||||||
"runData.pindata.thisDataIsPinned": "This data is pinned.",
|
"runData.pindata.thisDataIsPinned": "This node is pinned, and will always output the data below.",
|
||||||
"runData.pindata.unpin": "Unpin",
|
"runData.pindata.unpin": "Unpin",
|
||||||
"runData.editor.save": "Save",
|
"runData.editor.save": "Save",
|
||||||
"runData.editor.cancel": "Cancel",
|
"runData.editor.cancel": "Cancel",
|
||||||
|
|
|
@ -161,6 +161,9 @@ import {
|
||||||
faStream,
|
faStream,
|
||||||
faPowerOff,
|
faPowerOff,
|
||||||
faPaperPlane,
|
faPaperPlane,
|
||||||
|
faCircle,
|
||||||
|
faDiceOne,
|
||||||
|
faRetweet,
|
||||||
} from '@fortawesome/free-solid-svg-icons';
|
} from '@fortawesome/free-solid-svg-icons';
|
||||||
import { faVariable, faXmark, faVault, faRefresh } from './custom';
|
import { faVariable, faXmark, faVault, faRefresh } from './custom';
|
||||||
import { faStickyNote } from '@fortawesome/free-regular-svg-icons';
|
import { faStickyNote } from '@fortawesome/free-regular-svg-icons';
|
||||||
|
@ -336,6 +339,9 @@ export const FontAwesomePlugin: Plugin = {
|
||||||
addIcon(faPowerOff);
|
addIcon(faPowerOff);
|
||||||
addIcon(faPaperPlane);
|
addIcon(faPaperPlane);
|
||||||
addIcon(faRefresh);
|
addIcon(faRefresh);
|
||||||
|
addIcon(faCircle);
|
||||||
|
addIcon(faDiceOne);
|
||||||
|
addIcon(faRetweet);
|
||||||
|
|
||||||
app.component('FontAwesomeIcon', FontAwesomeIcon);
|
app.component('FontAwesomeIcon', FontAwesomeIcon);
|
||||||
},
|
},
|
||||||
|
|
Loading…
Reference in a new issue