feat: Add global event bus (#4860)
* fix branch * fix deserialize, add filewriter * add catchAll eventGroup/Name * adding simple Redis sender and receiver to eventbus * remove native node threads * improve eventbus * refactor and simplify * more refactoring and syslog client * more refactor, improved endpoints and eventbus * remove local broker and receivers from mvp * destination de/serialization * create MessageEventBusDestinationEntity * db migrations, load destinations at startup * add delete destination endpoint * pnpm merge and circular import fix * delete destination fix * trigger log file shuffle after size reached * add environment variables for eventbus * reworking event messages * serialize to thread fix * some refactor and lint fixing * add emit to eventbus * cleanup and fix sending unsent * quicksave frontend trial * initial EventTree vue component * basic log streaming settings in vue * http request code merge * create destination settings modals * fix eventmessage options types * credentials are loaded * fix and clean up frontend code * move request code to axios * update lock file * merge fix * fix redis build * move destination interfaces into workflow pkg * revive sentry as destination * migration fixes and frontend cleanup * N8N-5777 / N8N-5789 N8N-5788 * N8N-5784 * N8N-5782 removed event levels * N8N-5790 sentry destination cleanup * N8N-5786 and refactoring * N8N-5809 and refactor/cleanup * UI fixes and anonymize renaming * N8N-5837 * N8N-5834 * fix no-items UI issues * remove card / settings label in modal * N8N-5842 fix * disable webhook auth for now and update ui * change sidebar to tabs * remove payload option * extend audit events with more user data * N8N-5853 and UI revert to sidebar * remove redis destination * N8N-5864 / N8N-5868 / N8N-5867 / N8N-5865 * ui and licensing fixes * add node events and info bubbles to frontend * ui wording changes * frontend tests * N8N-5896 and ee rename * improves backend tests * merge fix * fix backend test * make linter happy * remove unnecessary cfg / limit actions to owners * fix multiple sentry DSN and anon bug * eslint fix * more tests and fixes * merge fix * fix workflow audit events * remove 'n8n.workflow.execution.error' event * merge fix * lint fix * lint fix * review fixes * fix merge * prettier fixes * merge * review changes * use loggerproxy * remove catch from internal hook promises * fix tests * lint fix * include review PR changes * review changes * delete duplicate lines from a bad merge * decouple log-streaming UI options from public API * logstreaming -> log-streaming for consistency * do not make unnecessary api calls when log streaming is disabled * prevent sentryClient.close() from being called if init failed * fix the e2e test for log-streaming * review changes * cleanup * use `private` for one last private property * do not use node prefix package names.. just yet * remove unused import * fix the tests because there is a folder called `events`, tsc-alias is messing up all imports for native events module. https://github.com/justkey007/tsc-alias/issues/152 Co-authored-by: कारतोफ्फेलस्क्रिप्ट™ <aditya@netroy.in>
This commit is contained in:
committed by
GitHub
parent
0795cdb74c
commit
b67f803cbe
264
packages/editor-ui/src/views/SettingsLogStreamingView.vue
Normal file
264
packages/editor-ui/src/views/SettingsLogStreamingView.vue
Normal file
@@ -0,0 +1,264 @@
|
||||
<template>
|
||||
<div>
|
||||
<div :class="$style.header">
|
||||
<div class="mb-2xl">
|
||||
<n8n-heading size="2xlarge">
|
||||
{{ $locale.baseText(`settings.log-streaming.heading`) }}
|
||||
</n8n-heading>
|
||||
<template v-if="environment !== 'production'">
|
||||
<strong> Disable License ({{ environment }}) </strong>
|
||||
<el-switch v-model="disableLicense" size="large" data-test-id="disable-license-toggle" />
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<template v-if="isLicensed">
|
||||
<div class="mb-l">
|
||||
<n8n-info-tip theme="info" type="note">
|
||||
<template>
|
||||
<span v-html="$locale.baseText('settings.log-streaming.infoText')"></span>
|
||||
</template>
|
||||
</n8n-info-tip>
|
||||
</div>
|
||||
<template v-if="storeHasItems()">
|
||||
<el-row
|
||||
:gutter="10"
|
||||
v-for="item in sortedItemKeysByLabel"
|
||||
:key="item.key"
|
||||
:class="$style.destinationItem"
|
||||
>
|
||||
<el-col v-if="logStreamingStore.items[item.key]?.destination">
|
||||
<event-destination-card
|
||||
:destination="logStreamingStore.items[item.key]?.destination"
|
||||
:eventBus="eventBus"
|
||||
:isInstanceOwner="isInstanceOwner"
|
||||
@remove="onRemove(logStreamingStore.items[item.key]?.destination?.id)"
|
||||
@edit="onEdit(logStreamingStore.items[item.key]?.destination?.id)"
|
||||
/>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<div class="mt-m text-right">
|
||||
<n8n-button v-if="isInstanceOwner" size="large" @click="addDestination">
|
||||
{{ $locale.baseText(`settings.log-streaming.add`) }}
|
||||
</n8n-button>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div :class="$style.actionBoxContainer" data-test-id="action-box-licensed">
|
||||
<n8n-action-box
|
||||
:buttonText="$locale.baseText(`settings.log-streaming.add`)"
|
||||
@click="addDestination"
|
||||
>
|
||||
<template #heading>
|
||||
<span v-html="$locale.baseText(`settings.log-streaming.addFirstTitle`)" />
|
||||
</template>
|
||||
</n8n-action-box>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div v-if="$locale.baseText('settings.log-streaming.infoText')" class="mb-l">
|
||||
<n8n-info-tip theme="info" type="note">
|
||||
<template>
|
||||
<span v-html="$locale.baseText('settings.log-streaming.infoText')"></span>
|
||||
</template>
|
||||
</n8n-info-tip>
|
||||
</div>
|
||||
<div :class="$style.actionBoxContainer" data-test-id="action-box-unlicensed">
|
||||
<n8n-action-box
|
||||
:description="$locale.baseText('settings.log-streaming.actionBox.description')"
|
||||
:buttonText="$locale.baseText('settings.log-streaming.actionBox.button')"
|
||||
@click="onContactUsClicked"
|
||||
>
|
||||
<template #heading>
|
||||
<span v-html="$locale.baseText('settings.log-streaming.actionBox.title')" />
|
||||
</template>
|
||||
</n8n-action-box>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { mapStores } from 'pinia';
|
||||
import mixins from 'vue-typed-mixins';
|
||||
import { useWorkflowsStore } from '../stores/workflows';
|
||||
import { useUsersStore } from '../stores/users';
|
||||
import { useCredentialsStore } from '../stores/credentials';
|
||||
import { useLogStreamingStore } from '../stores/logStreamingStore';
|
||||
import { useSettingsStore } from '../stores/settings';
|
||||
import { useUIStore } from '../stores/ui';
|
||||
import { LOG_STREAM_MODAL_KEY, EnterpriseEditionFeature } from '../constants';
|
||||
import Vue from 'vue';
|
||||
import { restApi } from '../mixins/restApi';
|
||||
import {
|
||||
deepCopy,
|
||||
defaultMessageEventBusDestinationOptions,
|
||||
MessageEventBusDestinationOptions,
|
||||
} from 'n8n-workflow';
|
||||
import PageViewLayout from '@/components/layouts/PageViewLayout.vue';
|
||||
import EventDestinationCard from '@/components/SettingsLogStreaming/EventDestinationCard.ee.vue';
|
||||
|
||||
export default mixins(restApi).extend({
|
||||
name: 'SettingsLogStreamingView',
|
||||
props: {},
|
||||
components: {
|
||||
PageViewLayout,
|
||||
EventDestinationCard,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
eventBus: new Vue(),
|
||||
destinations: Array<MessageEventBusDestinationOptions>,
|
||||
disableLicense: false,
|
||||
allDestinations: [] as MessageEventBusDestinationOptions[],
|
||||
isInstanceOwner: false,
|
||||
};
|
||||
},
|
||||
async mounted() {
|
||||
if (!this.isLicensed) return;
|
||||
|
||||
this.isInstanceOwner = this.usersStore.currentUser?.globalRole?.name === 'owner';
|
||||
// Prepare credentialsStore so modals can pick up credentials
|
||||
await this.credentialsStore.fetchCredentialTypes(false);
|
||||
await this.credentialsStore.fetchAllCredentials();
|
||||
this.uiStore.nodeViewInitialized = false;
|
||||
|
||||
// fetch Destination data from the backend
|
||||
await this.getDestinationDataFromREST();
|
||||
|
||||
// since we are not really integrated into the hooks, we listen to the store and refresh the destinations
|
||||
this.logStreamingStore.$onAction(({ name, after }) => {
|
||||
if (name === 'removeDestination' || name === 'updateDestination') {
|
||||
after(async () => {
|
||||
this.$forceUpdate();
|
||||
});
|
||||
}
|
||||
});
|
||||
// refresh when a modal closes
|
||||
this.eventBus.$on('destinationWasSaved', async () => {
|
||||
this.$forceUpdate();
|
||||
});
|
||||
// listen to remove emission
|
||||
this.eventBus.$on('remove', async (destinationId: string) => {
|
||||
await this.onRemove(destinationId);
|
||||
});
|
||||
// listen to modal closing and remove nodes from store
|
||||
this.eventBus.$on('closing', async (destinationId: string) => {
|
||||
this.workflowsStore.removeAllNodes({ setStateDirty: false, removePinData: true });
|
||||
this.uiStore.stateIsDirty = false;
|
||||
});
|
||||
},
|
||||
computed: {
|
||||
...mapStores(
|
||||
useSettingsStore,
|
||||
useLogStreamingStore,
|
||||
useWorkflowsStore,
|
||||
useUIStore,
|
||||
useUsersStore,
|
||||
useCredentialsStore,
|
||||
),
|
||||
sortedItemKeysByLabel() {
|
||||
const sortedKeys: Array<{ label: string; key: string }> = [];
|
||||
for (const [key, value] of Object.entries(this.logStreamingStore.items)) {
|
||||
sortedKeys.push({ key, label: value.destination?.label ?? 'Destination' });
|
||||
}
|
||||
return sortedKeys.sort((a, b) => a.label.localeCompare(b.label));
|
||||
},
|
||||
environment() {
|
||||
return process.env.NODE_ENV;
|
||||
},
|
||||
isLicensed(): boolean {
|
||||
if (this.disableLicense === true) return false;
|
||||
return this.settingsStore.isEnterpriseFeatureEnabled(EnterpriseEditionFeature.LogStreaming);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async getDestinationDataFromREST(): Promise<any> {
|
||||
this.logStreamingStore.clearEventNames();
|
||||
this.logStreamingStore.clearDestinationItemTrees();
|
||||
this.allDestinations = [];
|
||||
const eventNamesData = await this.restApi().makeRestApiRequest('get', '/eventbus/eventnames');
|
||||
if (eventNamesData) {
|
||||
for (const eventName of eventNamesData) {
|
||||
this.logStreamingStore.addEventName(eventName);
|
||||
}
|
||||
}
|
||||
const destinationData: MessageEventBusDestinationOptions[] =
|
||||
await this.restApi().makeRestApiRequest('get', '/eventbus/destination');
|
||||
if (destinationData) {
|
||||
for (const destination of destinationData) {
|
||||
this.logStreamingStore.addDestination(destination);
|
||||
this.allDestinations.push(destination);
|
||||
}
|
||||
}
|
||||
this.$forceUpdate();
|
||||
},
|
||||
onContactUsClicked() {
|
||||
window.open('mailto:sales@n8n.io', '_blank');
|
||||
this.$telemetry.track('user clicked contact us button', {
|
||||
feature: EnterpriseEditionFeature.LogStreaming,
|
||||
});
|
||||
},
|
||||
storeHasItems(): boolean {
|
||||
return this.logStreamingStore.items && Object.keys(this.logStreamingStore.items).length > 0;
|
||||
},
|
||||
async addDestination() {
|
||||
const newDestination = deepCopy(defaultMessageEventBusDestinationOptions);
|
||||
newDestination.id = uuid();
|
||||
this.logStreamingStore.addDestination(newDestination);
|
||||
this.uiStore.openModalWithData({
|
||||
name: LOG_STREAM_MODAL_KEY,
|
||||
data: {
|
||||
destination: newDestination,
|
||||
isNew: true,
|
||||
eventBus: this.eventBus,
|
||||
},
|
||||
});
|
||||
},
|
||||
async onRemove(destinationId?: string) {
|
||||
if (!destinationId) return;
|
||||
await this.restApi().makeRestApiRequest(
|
||||
'DELETE',
|
||||
`/eventbus/destination?id=${destinationId}`,
|
||||
);
|
||||
this.logStreamingStore.removeDestination(destinationId);
|
||||
const foundNode = this.workflowsStore.getNodeByName(destinationId);
|
||||
if (foundNode) {
|
||||
this.workflowsStore.removeNode(foundNode);
|
||||
}
|
||||
},
|
||||
async onEdit(destinationId?: string) {
|
||||
if (!destinationId) return;
|
||||
const editDestination = this.logStreamingStore.getDestination(destinationId);
|
||||
if (editDestination) {
|
||||
this.uiStore.openModalWithData({
|
||||
name: LOG_STREAM_MODAL_KEY,
|
||||
data: {
|
||||
destination: editDestination,
|
||||
isNew: false,
|
||||
eventBus: this.eventBus,
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
white-space: nowrap;
|
||||
|
||||
*:first-child {
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.destinationItem {
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user