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 RunDataPinButton from '@/components/RunDataPinButton.vue';
|
||||
import { getGenericHints } from '@/utils/nodeViewUtils';
|
||||
import { retry } from '../__tests__/utils';
|
||||
import { continueOnFail } from '../../../core/src/NodeExecuteFunctions';
|
||||
|
||||
const LazyRunDataTable = defineAsyncComponent(
|
||||
async () => await import('@/components/RunDataTable.vue'),
|
||||
|
@ -727,6 +729,69 @@ export default defineComponent({
|
|||
|
||||
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) {
|
||||
if (itemIndex === null) {
|
||||
this.$emit('itemHover', null);
|
||||
|
@ -1208,41 +1273,6 @@ export default defineComponent({
|
|||
|
||||
<template>
|
||||
<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
|
||||
v-if="binaryDataDisplayData"
|
||||
:window-visible="binaryDataDisplayVisible"
|
||||
|
@ -1370,7 +1400,49 @@ export default defineComponent({
|
|||
</div>
|
||||
|
||||
<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
|
||||
v-for="hint in getNodeHints()"
|
||||
:key="hint.message"
|
||||
|
|
|
@ -4,10 +4,12 @@ import { useNodeConnections } from '@/composables/useNodeConnections';
|
|||
import { useI18n } from '@/composables/useI18n';
|
||||
import CanvasNodeDisabledStrikeThrough from './parts/CanvasNodeDisabledStrikeThrough.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 { NODE_INSERT_SPACER_BETWEEN_INPUT_GROUPS } from '@/constants';
|
||||
import { N8nTooltip } from 'n8n-design-system';
|
||||
import type { CanvasNodeDefaultRender } from '@/types';
|
||||
import { useNodeHelpers } from '@/composables/useNodeHelpers';
|
||||
|
||||
const $style = useCssModule();
|
||||
const i18n = useI18n();
|
||||
|
@ -45,6 +47,8 @@ const {
|
|||
connections,
|
||||
});
|
||||
|
||||
const nodeHelpers = useNodeHelpers();
|
||||
|
||||
const renderOptions = computed(() => render.value.options as CanvasNodeDefaultRender['options']);
|
||||
|
||||
const classes = computed(() => {
|
||||
|
@ -122,6 +126,10 @@ function openContextMenu(event: MouseEvent) {
|
|||
</div>
|
||||
</N8nTooltip>
|
||||
<CanvasNodeStatusIcons v-if="!isDisabled" :class="$style.statusIcons" />
|
||||
<CanvasNodeSettingsIcons
|
||||
v-if="!isDisabled && !(hasPinnedData && !nodeHelpers.isProductionExecutionPreview.value)"
|
||||
:class="$style.settingsIcons"
|
||||
/>
|
||||
<CanvasNodeDisabledStrikeThrough v-if="isStrikethroughVisible" />
|
||||
<div :class="$style.description">
|
||||
<div v-if="label" :class="$style.label">
|
||||
|
@ -217,6 +225,10 @@ function openContextMenu(event: MouseEvent) {
|
|||
right: calc(-1 * var(--spacing-2xs));
|
||||
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);
|
||||
}
|
||||
|
||||
.settingsIcons {
|
||||
display: flex;
|
||||
gap: 3px;
|
||||
position: absolute;
|
||||
top: var(--canvas-node--status-icons-offset);
|
||||
right: var(--canvas-node--status-icons-offset);
|
||||
}
|
||||
|
||||
.triggerIcon {
|
||||
position: absolute;
|
||||
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 { useNodeHelpers } from '@/composables/useNodeHelpers';
|
||||
import { useCanvasNode } from '@/composables/useCanvasNode';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers';
|
||||
|
||||
const nodeHelpers = useNodeHelpers();
|
||||
|
||||
|
@ -16,9 +18,13 @@ const {
|
|||
hasRunData,
|
||||
runDataIterations,
|
||||
isDisabled,
|
||||
name,
|
||||
} = useCanvasNode();
|
||||
|
||||
const hideNodeIssues = computed(() => false); // @TODO Implement this
|
||||
const router = useRouter();
|
||||
const workflowHelpers = useWorkflowHelpers({ router });
|
||||
const workflow = workflowHelpers.getCurrentWorkflow();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
|
@ -1602,7 +1602,7 @@
|
|||
"runData.startTime": "Start Time",
|
||||
"runData.table": "Table",
|
||||
"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.editor.save": "Save",
|
||||
"runData.editor.cancel": "Cancel",
|
||||
|
|
|
@ -161,6 +161,9 @@ import {
|
|||
faStream,
|
||||
faPowerOff,
|
||||
faPaperPlane,
|
||||
faCircle,
|
||||
faDiceOne,
|
||||
faRetweet,
|
||||
} from '@fortawesome/free-solid-svg-icons';
|
||||
import { faVariable, faXmark, faVault, faRefresh } from './custom';
|
||||
import { faStickyNote } from '@fortawesome/free-regular-svg-icons';
|
||||
|
@ -336,6 +339,9 @@ export const FontAwesomePlugin: Plugin = {
|
|||
addIcon(faPowerOff);
|
||||
addIcon(faPaperPlane);
|
||||
addIcon(faRefresh);
|
||||
addIcon(faCircle);
|
||||
addIcon(faDiceOne);
|
||||
addIcon(faRetweet);
|
||||
|
||||
app.component('FontAwesomeIcon', FontAwesomeIcon);
|
||||
},
|
||||
|
|
Loading…
Reference in a new issue