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

View file

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

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

View file

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

View file

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