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:
Michael Auerswald
2023-01-04 09:47:48 +01:00
committed by GitHub
parent 0795cdb74c
commit b67f803cbe
104 changed files with 5867 additions and 219 deletions

View File

@@ -0,0 +1,240 @@
import { deepCopy, MessageEventBusDestinationOptions } from 'n8n-workflow';
import { defineStore } from 'pinia';
export interface EventSelectionItem {
selected: boolean;
indeterminate: boolean;
name: string;
label: string;
}
export interface EventSelectionGroup extends EventSelectionItem {
children: EventSelectionItem[];
}
export interface TreeAndSelectionStoreItem {
destination: MessageEventBusDestinationOptions;
selectedEvents: Set<string>;
eventGroups: EventSelectionGroup[];
}
export interface DestinationSettingsStore {
[key: string]: TreeAndSelectionStoreItem;
}
export const useLogStreamingStore = defineStore('logStreaming', {
state: () => ({
items: {} as DestinationSettingsStore,
eventNames: new Set<string>(),
}),
getters: {},
actions: {
addDestination(destination: MessageEventBusDestinationOptions) {
if (destination.id && destination.id in this.items) {
this.items[destination.id].destination = destination;
} else {
this.setSelectionAndBuildItems(destination);
}
},
getDestination(destinationId: string): MessageEventBusDestinationOptions | undefined {
if (destinationId in this.items) {
return this.items[destinationId].destination;
} else {
return;
}
},
getAllDestinations(): MessageEventBusDestinationOptions[] {
const destinations: MessageEventBusDestinationOptions[] = [];
for (const key of Object.keys(this.items)) {
destinations.push(this.items[key].destination);
}
return destinations;
},
updateDestination(destination: MessageEventBusDestinationOptions) {
this.$patch((state) => {
if (destination.id && destination.id in this.items) {
state.items[destination.id].destination = destination;
}
// to trigger refresh
state.items = deepCopy(state.items);
});
},
removeDestination(destinationId: string) {
if (!destinationId) return;
delete this.items[destinationId];
if (destinationId in this.items) {
this.$patch({
items: {
...this.items,
},
});
}
},
clearDestinations() {
this.items = {};
},
addEventName(name: string) {
this.eventNames.add(name);
},
removeEventName(name: string) {
this.eventNames.delete(name);
},
clearEventNames() {
this.eventNames.clear();
},
addSelectedEvent(id: string, name: string) {
this.items[id]?.selectedEvents?.add(name);
this.setSelectedInGroup(id, name, true);
},
removeSelectedEvent(id: string, name: string) {
this.items[id]?.selectedEvents?.delete(name);
this.setSelectedInGroup(id, name, false);
},
getSelectedEvents(destinationId: string): string[] {
const selectedEvents: string[] = [];
if (destinationId in this.items) {
for (const group of this.items[destinationId].eventGroups) {
if (group.selected) {
selectedEvents.push(group.name);
}
for (const event of group.children) {
if (event.selected) {
selectedEvents.push(event.name);
}
}
}
}
return selectedEvents;
},
setSelectedInGroup(destinationId: string, name: string, isSelected: boolean) {
if (destinationId in this.items) {
const groupName = eventGroupFromEventName(name);
const groupIndex = this.items[destinationId].eventGroups.findIndex(
(e) => e.name === groupName,
);
if (groupIndex > -1) {
if (groupName === name) {
this.$patch((state) => {
state.items[destinationId].eventGroups[groupIndex].selected = isSelected;
});
} else {
const eventIndex = this.items[destinationId].eventGroups[groupIndex].children.findIndex(
(e) => e.name === name,
);
if (eventIndex > -1) {
this.$patch((state) => {
state.items[destinationId].eventGroups[groupIndex].children[eventIndex].selected =
isSelected;
if (isSelected) {
state.items[destinationId].eventGroups[groupIndex].indeterminate = isSelected;
} else {
let anySelected = false;
for (
let i = 0;
i < state.items[destinationId].eventGroups[groupIndex].children.length;
i++
) {
anySelected =
anySelected ||
state.items[destinationId].eventGroups[groupIndex].children[i].selected;
}
state.items[destinationId].eventGroups[groupIndex].indeterminate = anySelected;
}
});
}
}
}
}
},
removeDestinationItemTree(id: string) {
delete this.items[id];
},
clearDestinationItemTrees() {
this.items = {} as DestinationSettingsStore;
},
setSelectionAndBuildItems(destination: MessageEventBusDestinationOptions) {
if (destination.id) {
if (!(destination.id in this.items)) {
this.items[destination.id] = {
destination,
selectedEvents: new Set<string>(),
eventGroups: [],
} as TreeAndSelectionStoreItem;
}
this.items[destination.id]?.selectedEvents?.clear();
if (destination.subscribedEvents) {
for (const eventName of destination.subscribedEvents) {
this.items[destination.id]?.selectedEvents?.add(eventName);
}
}
this.items[destination.id].eventGroups = eventGroupsFromStringList(
this.eventNames,
this.items[destination.id]?.selectedEvents,
);
}
},
},
});
export function eventGroupFromEventName(eventName: string): string | undefined {
const matches = eventName.match(/^[\w\s]+\.[\w\s]+/);
if (matches && matches?.length > 0) {
return matches[0];
}
return undefined;
}
function prettifyEventName(label: string, group = ''): string {
label = label.replace(group + '.', '');
if (label.length > 0) {
label = label[0].toUpperCase() + label.substring(1);
label = label.replaceAll('.', ' ');
}
return label;
}
export function eventGroupsFromStringList(
dottedList: Set<string>,
selectionList: Set<string> = new Set(),
) {
const result = [] as EventSelectionGroup[];
const eventNameArray = Array.from(dottedList.values());
const groups: Set<string> = new Set<string>();
// since a Set returns iteration items on the order they were added, we can make sure workflow and nodes come first
groups.add('n8n.workflow');
groups.add('n8n.node');
for (const eventName of eventNameArray) {
const matches = eventName.match(/^[\w\s]+\.[\w\s]+/);
if (matches && matches?.length > 0) {
groups.add(matches[0]);
}
}
for (const group of groups) {
const collection: EventSelectionGroup = {
children: [],
label: group,
name: group,
selected: selectionList.has(group),
indeterminate: false,
};
const eventsOfGroup = eventNameArray.filter((e) => e.startsWith(group));
for (const event of eventsOfGroup) {
if (!collection.selected && selectionList.has(event)) {
collection.indeterminate = true;
}
const subCollection: EventSelectionItem = {
label: prettifyEventName(event, group),
name: event,
selected: selectionList.has(event),
indeterminate: false,
};
collection.children.push(subCollection);
}
result.push(collection);
}
return result;
}

View File

@@ -18,6 +18,7 @@ import {
FAKE_DOOR_FEATURES,
IMPORT_CURL_MODAL_KEY,
INVITE_USER_MODAL_KEY,
LOG_STREAM_MODAL_KEY,
ONBOARDING_CALL_SIGNUP_MODAL_KEY,
PERSONALIZATION_MODAL_KEY,
STORES,
@@ -118,6 +119,10 @@ export const useUIStore = defineStore(STORES.UI, {
curlCommand: '',
httpNodeParameters: '',
},
[LOG_STREAM_MODAL_KEY]: {
open: false,
data: undefined,
},
},
modalStack: [],
sidebarMenuCollapsed: true,
@@ -135,16 +140,6 @@ export const useUIStore = defineStore(STORES.UI, {
linkURL: 'https://n8n-community.typeform.com/to/l7QOrERN#f=environments',
uiLocations: ['settings'],
},
{
id: FAKE_DOOR_FEATURES.LOGGING,
featureName: 'fakeDoor.settings.logging.name',
icon: 'sign-in-alt',
infoText: 'fakeDoor.settings.logging.infoText',
actionBoxTitle: 'fakeDoor.settings.logging.actionBox.title',
actionBoxDescription: 'fakeDoor.settings.logging.actionBox.description',
linkURL: 'https://n8n-community.typeform.com/to/l7QOrERN#f=logging',
uiLocations: ['settings'],
},
{
id: FAKE_DOOR_FEATURES.SSO,
featureName: 'fakeDoor.settings.sso.name',