feat: Audit Logs - add new page to frontend [WIP] (no-changelog) (#6418)

* feat: Audit Logs (WIP)

* feat: Audit Logs license depending contents

* fix(editor): simplify import

* fix(editor): add audit logs to server
This commit is contained in:
Csaba Tuncsik 2023-06-15 08:33:28 +02:00 committed by GitHub
parent 004d38d82b
commit 1fe6459569
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 170 additions and 0 deletions

View file

@ -313,6 +313,7 @@ export class Server extends AbstractServer {
advancedExecutionFilters: false,
variables: false,
versionControl: false,
auditLogs: false,
},
hideUsagePage: config.getEnv('hideUsagePage'),
license: {

View file

@ -17,6 +17,7 @@ const defaultSettings: IN8nUISettings = {
advancedExecutionFilters: false,
variables: true,
versionControl: false,
auditLogs: false,
},
executionMode: 'regular',
executionTimeout: 0,

View file

@ -44,6 +44,7 @@ export const SETTINGS_STORE_DEFAULT_STATE: ISettingsState = {
logStreaming: false,
variables: false,
versionControl: false,
auditLogs: false,
},
executionMode: 'regular',
executionTimeout: 0,

View file

@ -74,6 +74,14 @@ export default defineComponent({
available: this.canAccessApiSettings(),
activateOnRouteNames: [VIEWS.API_SETTINGS],
},
{
id: 'settings-audit-logs',
icon: 'clipboard-list',
label: this.$locale.baseText('settings.auditLogs.title'),
position: 'top',
available: this.canAccessAuditLogs(),
activateOnRouteNames: [VIEWS.AUDIT_LOGS],
},
{
id: 'settings-version-control',
icon: 'code-branch',
@ -159,6 +167,9 @@ export default defineComponent({
canAccessVersionControl(): boolean {
return this.canUserAccessRouteByName(VIEWS.VERSION_CONTROL);
},
canAccessAuditLogs(): boolean {
return this.canUserAccessRouteByName(VIEWS.AUDIT_LOGS);
},
canAccessSso(): boolean {
return this.canUserAccessRouteByName(VIEWS.SSO_SETTINGS);
},
@ -220,6 +231,11 @@ export default defineComponent({
void this.$router.push({ name: VIEWS.VERSION_CONTROL });
}
break;
case 'settings-audit-logs':
if (this.$router.currentRoute.name !== VIEWS.AUDIT_LOGS) {
void this.$router.push({ name: VIEWS.AUDIT_LOGS });
}
break;
default:
break;
}

View file

@ -376,6 +376,7 @@ export const enum VIEWS {
SSO_SETTINGS = 'SSoSettings',
SAML_ONBOARDING = 'SamlOnboarding',
VERSION_CONTROL = 'VersionControl',
AUDIT_LOGS = 'AuditLogs',
}
export const enum FAKE_DOOR_FEATURES {
@ -443,6 +444,7 @@ export const enum EnterpriseEditionFeature {
Variables = 'variables',
Saml = 'saml',
VersionControl = 'versionControl',
AuditLogs = 'auditLogs',
}
export const MAIN_NODE_PANEL_WIDTH = 360;

View file

@ -1407,6 +1407,10 @@
"settings.versionControl.refreshBranches.success": "Branches successfully refreshed",
"settings.versionControl.refreshBranches.error": "Error refreshing branches",
"showMessage.cancel": "@:_reusableBaseText.cancel",
"settings.auditLogs.title": "Audit Logs",
"settings.auditLogs.actionBox.title": "Available on Enterprise plan",
"settings.auditLogs.actionBox.description": "Upgrade to see the audit logs of your n8n instance.",
"settings.auditLogs.actionBox.buttonText": "See plans",
"showMessage.ok": "OK",
"showMessage.showDetails": "Show Details",
"startupError": "Error connecting to n8n",

View file

@ -32,6 +32,7 @@ import {
faCodeBranch,
faCog,
faCogs,
faClipboardList,
faClock,
faClone,
faCloud,
@ -171,6 +172,7 @@ addIcon(faCode);
addIcon(faCodeBranch);
addIcon(faCog);
addIcon(faCogs);
addIcon(faClipboardList);
addIcon(faClock);
addIcon(faClone);
addIcon(faCloud);

View file

@ -42,6 +42,7 @@ import SettingsSso from './views/SettingsSso.vue';
import SignoutView from '@/views/SignoutView.vue';
import SamlOnboarding from '@/views/SamlOnboarding.vue';
import SettingsVersionControl from './views/SettingsVersionControl.vue';
import SettingsAuditLogs from './views/SettingsAuditLogs.vue';
import { usePostHog } from './stores/posthog.store';
Vue.use(Router);
@ -714,6 +715,31 @@ export const routes = [
},
},
},
{
path: 'audit-logs',
name: VIEWS.AUDIT_LOGS,
components: {
settingsView: SettingsAuditLogs,
},
meta: {
telemetry: {
pageCategory: 'settings',
getProperties(route: Route) {
return {
feature: 'audit-logs',
};
},
},
permissions: {
allow: {
role: [ROLE.Owner],
},
deny: {
shouldDeny: () => !window.localStorage.getItem('audit-logs'),
},
},
},
},
],
},
{

View file

@ -0,0 +1,16 @@
import { computed } from 'vue';
import { defineStore } from 'pinia';
import { EnterpriseEditionFeature } from '@/constants';
import { useSettingsStore } from '@/stores';
export const useAuditLogsStore = defineStore('auditLogs', () => {
const settingsStore = useSettingsStore();
const isEnterpriseAuditLogsFeatureEnabled = computed(() =>
settingsStore.isEnterpriseFeatureEnabled(EnterpriseEditionFeature.AuditLogs),
);
return {
isEnterpriseAuditLogsFeatureEnabled,
};
});

View file

@ -24,3 +24,4 @@ export * from './workflows.store';
export * from './cloudPlan.store';
export * from './versionControl.store';
export * from './sso.store';
export * from './auditLogs.store';

View file

@ -0,0 +1,42 @@
<script lang="ts" setup>
import { useI18n } from '@/composables';
import { useUIStore, useAuditLogsStore } from '@/stores';
const { i18n: locale } = useI18n();
const uiStore = useUIStore();
const auditLogsStore = useAuditLogsStore();
const goToUpgrade = () => {
uiStore.goToUpgrade('audit-logs', 'upgrade-audit-logs');
};
</script>
<template>
<div>
<n8n-heading size="2xlarge" tag="h1">{{
locale.baseText('settings.auditLogs.title')
}}</n8n-heading>
<div
v-if="auditLogsStore.isEnterpriseAuditLogsFeatureEnabled"
data-test-id="audit-logs-content-licensed"
></div>
<n8n-action-box
v-else
data-test-id="audit-logs-content-unlicensed"
:class="$style.actionBox"
:description="locale.baseText('settings.auditLogs.actionBox.description')"
:buttonText="locale.baseText('settings.auditLogs.actionBox.buttonText')"
@click="goToUpgrade"
>
<template #heading>
<span>{{ locale.baseText('settings.auditLogs.actionBox.title') }}</span>
</template>
</n8n-action-box>
</div>
</template>
<style lang="scss" module>
.actionBox {
margin: var(--spacing-2xl) 0 0;
}
</style>

View file

@ -0,0 +1,57 @@
import { vi } from 'vitest';
import { render } from '@testing-library/vue';
import { createPinia, setActivePinia, PiniaVuePlugin } from 'pinia';
import { merge } from 'lodash-es';
import { i18nInstance } from '@/plugins/i18n';
import { useAuditLogsStore, useSettingsStore } from '@/stores';
import SettingsAuditLogs from '@/views/SettingsAuditLogs.vue';
let pinia: ReturnType<typeof createPinia>;
let settingsStore: ReturnType<typeof useSettingsStore>;
let auditLogsStore: ReturnType<typeof useAuditLogsStore>;
const renderComponent = (renderOptions: Parameters<typeof render>[1] = {}) =>
render(
SettingsAuditLogs,
merge(
{
pinia,
i18n: i18nInstance,
},
renderOptions,
),
(vue) => {
vue.use(PiniaVuePlugin);
},
);
describe('SettingsAuditLogs', () => {
beforeEach(() => {
pinia = createPinia();
setActivePinia(pinia);
settingsStore = useSettingsStore();
auditLogsStore = useAuditLogsStore();
});
afterEach(() => {
vi.clearAllMocks();
});
it('should render paywall state when there is no license', () => {
vi.spyOn(settingsStore, 'isEnterpriseFeatureEnabled').mockReturnValue(false);
const { getByTestId, queryByTestId } = renderComponent();
expect(queryByTestId('audit-logs-content-licensed')).not.toBeInTheDocument();
expect(getByTestId('audit-logs-content-unlicensed')).toBeInTheDocument();
});
it('should render licensed content', () => {
vi.spyOn(settingsStore, 'isEnterpriseFeatureEnabled').mockReturnValue(true);
const { getByTestId, queryByTestId } = renderComponent();
expect(getByTestId('audit-logs-content-licensed')).toBeInTheDocument();
expect(queryByTestId('audit-logs-content-unlicensed')).not.toBeInTheDocument();
});
});

View file

@ -2112,6 +2112,7 @@ export interface IN8nUISettings {
advancedExecutionFilters: boolean;
variables: boolean;
versionControl: boolean;
auditLogs: boolean;
};
hideUsagePage: boolean;
license: {