harmonies node statuses

Co-authored-by: Federico Meini <fedme@users.noreply.github.com>
This commit is contained in:
Shireen Missi 2024-10-30 11:47:39 +00:00
parent d7ba206b30
commit 5a715a2fc4
No known key found for this signature in database
GPG key ID: D213F10998FACC51
6 changed files with 240 additions and 37 deletions

View file

@ -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"

View file

@ -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%;

View file

@ -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>

View file

@ -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>

View file

@ -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",

View file

@ -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);
}, },