feat(editor): block UI in NDV when workflow is listening to events (#4390)

* feature: block UI in NDV when workflow is listening to events

* feature: hide stop listening button in parameters pane and show stop listening button in input pane for webhook

* feature: create block UI design system component

* fix: add back accidentally removed prop

* fix(editor): extend node settings event listener button functionality

* refactor(editor): using composition API in BlockUi component
This commit is contained in:
Csaba Tuncsik 2022-10-31 18:59:53 +01:00 committed by GitHub
parent 7563d450f9
commit 6c2c621f1d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 151 additions and 19 deletions

View file

@ -0,0 +1,20 @@
import N8nBlockUi from './BlockUi.vue';
export default {
title: 'Atoms/BlockUI',
component: N8nBlockUi,
};
const Template = (args, { argTypes }) => ({
props: Object.keys(argTypes),
components: {
N8nBlockUi,
},
template:
'<div style="position: relative; width: 100%; height: 300px;"><n8n-block-ui v-bind="$props" /></div>',
});
export const BlockUi = Template.bind({});
BlockUi.args = {
show: false
};

View file

@ -0,0 +1,40 @@
<template>
<transition name="fade" mode="out-in">
<div v-show="show" :class="['n8n-block-ui', $style.uiBlocker]" role="dialog" :aria-hidden="true" />
</transition>
</template>
<script lang="ts" setup>
type BlockUiProps = {
show: boolean;
}
const props = withDefaults(defineProps<BlockUiProps>(), {
show: false,
});
</script>
<style lang="scss" module>
.uiBlocker {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: var(--color-background-dark);
z-index: 10;
opacity: 0.6;
border-radius: var(--border-radius-large);
}
</style>
<style lang="scss" scoped>
.fade-enter-active,
.fade-leave-active {
transition: opacity 200ms;
}
.fade-enter,
.fade-leave-to {
opacity: 0;
}
</style>

View file

@ -0,0 +1,16 @@
import { render, screen } from '@testing-library/vue';
import N8nBlockUi from '../BlockUi.vue';
describe('components', () => {
describe('N8nBlockUi', () => {
it('should render but not visible', () => {
render(N8nBlockUi);
expect(screen.queryByRole('dialog', { hidden: true })).not.toBeVisible();
});
it('should render and is visible', () => {
render(N8nBlockUi, { props: { show: true } });
expect(screen.getByRole('dialog', { hidden: true })).toBeVisible();
});
});
});

View file

@ -0,0 +1,3 @@
import N8nBlockUi from './BlockUi.vue';
export default N8nBlockUi;

View file

@ -4,6 +4,7 @@ import N8nActionDropdown from '../components/N8nActionDropdown';
import N8nActionToggle from '../components/N8nActionToggle'; import N8nActionToggle from '../components/N8nActionToggle';
import N8nAvatar from '../components/N8nAvatar'; import N8nAvatar from '../components/N8nAvatar';
import N8nBadge from "../components/N8nBadge"; import N8nBadge from "../components/N8nBadge";
import N8nBlockUi from "../components/N8nBlockUi";
import N8nButton from '../components/N8nButton'; import N8nButton from '../components/N8nButton';
import { N8nElButton } from '../components/N8nButton/overrides'; import { N8nElButton } from '../components/N8nButton/overrides';
import N8nCallout from '../components/N8nCallout'; import N8nCallout from '../components/N8nCallout';
@ -51,6 +52,7 @@ export default {
app.component('n8n-action-toggle', N8nActionToggle); app.component('n8n-action-toggle', N8nActionToggle);
app.component('n8n-avatar', N8nAvatar); app.component('n8n-avatar', N8nAvatar);
app.component('n8n-badge', N8nBadge); app.component('n8n-badge', N8nBadge);
app.component('n8n-block-ui', N8nBlockUi);
app.component('n8n-button', N8nButton); app.component('n8n-button', N8nButton);
app.component('el-button', N8nElButton); app.component('el-button', N8nElButton);
app.component('n8n-callout', N8nCallout); app.component('n8n-callout', N8nCallout);

View file

@ -72,6 +72,7 @@
:linkedRuns="linked" :linkedRuns="linked"
:sessionId="sessionId" :sessionId="sessionId"
:isReadOnly="readOnly || hasForeignCredential" :isReadOnly="readOnly || hasForeignCredential"
:blockUI="blockUi && isTriggerNode"
@linkRun="onLinkRunToOutput" @linkRun="onLinkRunToOutput"
@unlinkRun="() => onUnlinkRun('output')" @unlinkRun="() => onUnlinkRun('output')"
@runChange="onRunOutputIndexChange" @runChange="onRunOutputIndexChange"
@ -87,8 +88,10 @@
:sessionId="sessionId" :sessionId="sessionId"
:nodeType="activeNodeType" :nodeType="activeNodeType"
:isReadOnly="readOnly || hasForeignCredential" :isReadOnly="readOnly || hasForeignCredential"
:blockUI="blockUi && showTriggerPanel"
@valueChanged="valueChanged" @valueChanged="valueChanged"
@execute="onNodeExecute" @execute="onNodeExecute"
@stopExecution="onStopExecution"
@activate="onWorkflowActivate" @activate="onWorkflowActivate"
/> />
<a <a
@ -225,14 +228,10 @@ export default mixins(
return null; return null;
}, },
showTriggerPanel(): boolean { showTriggerPanel(): boolean {
const isWebhookBasedNode = this.activeNodeType && this.activeNodeType.webhooks && this.activeNodeType.webhooks.length; const isWebhookBasedNode = !!this.activeNodeType?.webhooks?.length;
const isPollingNode = this.activeNodeType && this.activeNodeType.polling; const isPollingNode = this.activeNodeType?.polling;
const override = this.activeNodeType && this.activeNodeType.triggerPanel; const override = !!this.activeNodeType?.triggerPanel;
return Boolean( return !this.readOnly && this.isTriggerNode && (isWebhookBasedNode || isPollingNode || override);
!this.readOnly &&
this.isTriggerNode &&
(isWebhookBasedNode || isPollingNode || override),
);
}, },
workflow(): Workflow { workflow(): Workflow {
return this.getCurrentWorkflow(); return this.getCurrentWorkflow();
@ -343,6 +342,15 @@ export default mixins(
outputPanelEditMode(): { enabled: boolean; value: string; } { outputPanelEditMode(): { enabled: boolean; value: string; } {
return this.$store.getters['ndv/outputPanelEditMode']; return this.$store.getters['ndv/outputPanelEditMode'];
}, },
isWorkflowRunning(): boolean {
return this.$store.getters.isActionActive('workflowRunning');
},
isExecutionWaitingForWebhook(): boolean {
return this.$store.getters.executionWaitingForWebhook;
},
blockUi(): boolean {
return this.isWorkflowRunning || this.isExecutionWaitingForWebhook;
},
}, },
watch: { watch: {
activeNode(node: INodeUi | null) { activeNode(node: INodeUi | null) {
@ -608,8 +616,13 @@ export default mixins(
}); });
}, },
checkForeignCredentials() { checkForeignCredentials() {
if(this.activeNode){
const issues = this.getNodeCredentialIssues(this.activeNode); const issues = this.getNodeCredentialIssues(this.activeNode);
this.hasForeignCredential = !!issues?.credentials?.foreign; this.hasForeignCredential = !!issues?.credentials?.foreign;
}
},
onStopExecution(){
this.$emit('stopExecution');
}, },
}, },
}); });

View file

@ -3,7 +3,7 @@
<div slot="content">{{ disabledHint }}</div> <div slot="content">{{ disabledHint }}</div>
<div> <div>
<n8n-button <n8n-button
:loading="nodeRunning && !isListeningForEvents" :loading="nodeRunning && !isListeningForEvents && !isListeningForWorkflowEvents"
:disabled="disabled || !!disabledHint" :disabled="disabled || !!disabledHint"
:label="buttonLabel" :label="buttonLabel"
:type="type" :type="type"
@ -98,6 +98,9 @@ export default mixins(
(!executedNode || executedNode === this.nodeName) (!executedNode || executedNode === this.nodeName)
); );
}, },
isListeningForWorkflowEvents(): boolean {
return this.nodeRunning && this.isTriggerNode && !this.isScheduleTrigger && !this.isManualTriggerNode;
},
hasIssues (): boolean { hasIssues (): boolean {
return Boolean(this.node && this.node.issues && (this.node.issues.parameters || this.node.issues.credentials)); return Boolean(this.node && this.node.issues && (this.node.issues.parameters || this.node.issues.credentials));
}, },
@ -125,7 +128,7 @@ export default mixins(
return ''; return '';
}, },
buttonLabel(): string { buttonLabel(): string {
if (this.isListeningForEvents) { if (this.isListeningForEvents || this.isListeningForWorkflowEvents) {
return this.$locale.baseText('ndv.execute.stopListening'); return this.$locale.baseText('ndv.execute.stopListening');
} }
@ -164,6 +167,8 @@ export default mixins(
async onClick() { async onClick() {
if (this.isListeningForEvents) { if (this.isListeningForEvents) {
this.stopWaitingForWebhook(); this.stopWaitingForWebhook();
} else if (this.isListeningForWorkflowEvents) {
this.$emit('stopExecution');
} else { } else {
let shouldUnpinAndExecute = false; let shouldUnpinAndExecute = false;
if (this.hasPinData) { if (this.hasPinData) {

View file

@ -12,11 +12,13 @@
></NodeTitle> ></NodeTitle>
<div v-if="!isReadOnly"> <div v-if="!isReadOnly">
<NodeExecuteButton <NodeExecuteButton
v-if="!blockUI"
:nodeName="node.name" :nodeName="node.name"
:disabled="outputPanelEditMode.enabled" :disabled="outputPanelEditMode.enabled && !isTriggerNode"
size="small" size="small"
telemetrySource="parameters" telemetrySource="parameters"
@execute="onNodeExecute" @execute="onNodeExecute"
@stopExecution="onStopExecution"
/> />
</div> </div>
</div> </div>
@ -113,6 +115,7 @@
/> />
</div> </div>
</div> </div>
<n8n-block-ui :show="blockUI" />
</div> </div>
</template> </template>
@ -224,6 +227,9 @@ export default mixins(externalHooks, nodeHelpers).extend({
isCommunityNode(): boolean { isCommunityNode(): boolean {
return isCommunityPackageName(this.node.type); return isCommunityPackageName(this.node.type);
}, },
isTriggerNode(): boolean {
return this.$store.getters['nodeTypes/isTriggerNode'](this.node.type);
},
}, },
props: { props: {
eventBus: {}, eventBus: {},
@ -239,6 +245,10 @@ export default mixins(externalHooks, nodeHelpers).extend({
isReadOnly: { isReadOnly: {
type: Boolean, type: Boolean,
}, },
blockUI: {
type: Boolean,
default: false,
},
}, },
data() { data() {
return { return {
@ -758,6 +768,9 @@ export default mixins(externalHooks, nodeHelpers).extend({
node_type: this.node.type, node_type: this.node.type,
}); });
}, },
onStopExecution(){
this.$emit('stopExecution');
},
}, },
mounted() { mounted() {
this.setNodeValues(); this.setNodeValues();

View file

@ -10,6 +10,7 @@
:executingMessage="$locale.baseText('ndv.output.executing')" :executingMessage="$locale.baseText('ndv.output.executing')"
:sessionId="sessionId" :sessionId="sessionId"
:isReadOnly="isReadOnly" :isReadOnly="isReadOnly"
:blockUI="blockUI"
paneType="output" paneType="output"
@runChange="onRunIndexChange" @runChange="onRunIndexChange"
@linkRun="onLinkRun" @linkRun="onLinkRun"
@ -109,6 +110,10 @@ export default mixins(
sessionId: { sessionId: {
type: String, type: String,
}, },
blockUI: {
type: Boolean,
default: false,
},
}, },
computed: { computed: {
node(): INodeUi { node(): INodeUi {

View file

@ -321,7 +321,7 @@
</n8n-select> </n8n-select>
</div> </div>
</div> </div>
<n8n-block-ui :show="blockUI" :class="$style.uiBlocker" />
</div> </div>
</template> </template>
@ -367,7 +367,7 @@ import { genericHelpers } from '@/components/mixins/genericHelpers';
import { nodeHelpers } from '@/components/mixins/nodeHelpers'; import { nodeHelpers } from '@/components/mixins/nodeHelpers';
import { pinData } from '@/components/mixins/pinData'; import { pinData } from '@/components/mixins/pinData';
import { CodeEditor } from "@/components/forms"; import { CodeEditor } from "@/components/forms";
import { dataPinningEventBus } from '../event-bus/data-pinning-event-bus'; import { dataPinningEventBus } from '@/event-bus/data-pinning-event-bus';
import { clearJsonKey, executionDataToJson, stringSizeInBytes } from './helpers'; import { clearJsonKey, executionDataToJson, stringSizeInBytes } from './helpers';
import RunDataTable from './RunDataTable.vue'; import RunDataTable from './RunDataTable.vue';
import RunDataJson from '@/components/RunDataJson.vue'; import RunDataJson from '@/components/RunDataJson.vue';
@ -437,6 +437,10 @@ export default mixins(
showMappingHint: { showMappingHint: {
type: Boolean, type: Boolean,
}, },
blockUI: {
type: Boolean,
default: false,
},
}, },
data () { data () {
return { return {
@ -1383,4 +1387,9 @@ export default mixins(
height: 100%; height: 100%;
} }
.uiBlocker {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
</style> </style>

View file

@ -28,6 +28,12 @@
:copy-button-text="$locale.baseText('generic.clickToCopy')" :copy-button-text="$locale.baseText('generic.clickToCopy')"
@copy="onTestLinkCopied" @copy="onTestLinkCopied"
></CopyInput> ></CopyInput>
<NodeExecuteButton
:nodeName="nodeName"
@execute="onNodeExecute"
size="medium"
telemetrySource="inputs"
/>
</div> </div>
<div v-else> <div v-else>
<n8n-text tag="div" size="large" color="text-dark" class="mb-2xs" bold>{{ <n8n-text tag="div" size="large" color="text-dark" class="mb-2xs" bold>{{
@ -60,7 +66,6 @@
</div> </div>
<NodeExecuteButton <NodeExecuteButton
v-if="!isActivelyPolling"
:nodeName="nodeName" :nodeName="nodeName"
@execute="onNodeExecute" @execute="onNodeExecute"
size="medium" size="medium"

View file

@ -77,6 +77,7 @@
:readOnly="isReadOnly" :readOnly="isReadOnly"
:renaming="renamingActive" :renaming="renamingActive"
@valueChanged="valueChanged" @valueChanged="valueChanged"
@stopExecution="stopExecution"
/> />
<node-creation <node-creation
v-if="!isReadOnly" v-if="!isReadOnly"
@ -120,15 +121,15 @@
class="stop-execution" type="secondary" :title="stopExecutionInProgress class="stop-execution" type="secondary" :title="stopExecutionInProgress
? $locale.baseText('nodeView.stoppingCurrentExecution') ? $locale.baseText('nodeView.stoppingCurrentExecution')
: $locale.baseText('nodeView.stopCurrentExecution') : $locale.baseText('nodeView.stopCurrentExecution')
" :loading="stopExecutionInProgress" @click.stop="stopExecution()" /> " :loading="stopExecutionInProgress" @click.stop="stopExecution" />
<n8n-icon-button v-if="workflowRunning === true && executionWaitingForWebhook === true" class="stop-execution" <n8n-icon-button v-if="workflowRunning === true && executionWaitingForWebhook === true" class="stop-execution"
icon="stop" size="large" :title="$locale.baseText('nodeView.stopWaitingForWebhookCall')" type="secondary" icon="stop" size="large" :title="$locale.baseText('nodeView.stopWaitingForWebhookCall')" type="secondary"
@click.stop="stopWaitingForWebhook()" /> @click.stop="stopWaitingForWebhook" />
<n8n-icon-button v-if="!isReadOnly && workflowExecution && !workflowRunning && !allTriggersDisabled" <n8n-icon-button v-if="!isReadOnly && workflowExecution && !workflowRunning && !allTriggersDisabled"
:title="$locale.baseText('nodeView.deletesTheCurrentExecutionData')" icon="trash" size="large" :title="$locale.baseText('nodeView.deletesTheCurrentExecutionData')" icon="trash" size="large"
@click.stop="clearExecutionData()" /> @click.stop="clearExecutionData" />
</div> </div>
</div> </div>
</div> </div>