n8n/packages/editor-ui/src/views/SettingsLogStreamingView.vue
Michael Auerswald b67f803cbe
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>
2023-01-04 09:47:48 +01:00

265 lines
8.3 KiB
Vue

<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>&nbsp;&nbsp;&nbsp;&nbsp;Disable License ({{ environment }})&nbsp;</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>