mirror of
https://github.com/n8n-io/n8n.git
synced 2024-12-26 05:04:05 -08:00
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:
parent
7563d450f9
commit
6c2c621f1d
|
@ -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
|
||||||
|
};
|
40
packages/design-system/src/components/N8nBlockUi/BlockUi.vue
Normal file
40
packages/design-system/src/components/N8nBlockUi/BlockUi.vue
Normal 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>
|
|
@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,3 @@
|
||||||
|
import N8nBlockUi from './BlockUi.vue';
|
||||||
|
|
||||||
|
export default N8nBlockUi;
|
|
@ -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);
|
||||||
|
|
|
@ -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() {
|
||||||
const issues = this.getNodeCredentialIssues(this.activeNode);
|
if(this.activeNode){
|
||||||
this.hasForeignCredential = !!issues?.credentials?.foreign;
|
const issues = this.getNodeCredentialIssues(this.activeNode);
|
||||||
|
this.hasForeignCredential = !!issues?.credentials?.foreign;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onStopExecution(){
|
||||||
|
this.$emit('stopExecution');
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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>
|
||||||
|
|
Loading…
Reference in a new issue