mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -08:00
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:
parent
004d38d82b
commit
1fe6459569
|
@ -313,6 +313,7 @@ export class Server extends AbstractServer {
|
||||||
advancedExecutionFilters: false,
|
advancedExecutionFilters: false,
|
||||||
variables: false,
|
variables: false,
|
||||||
versionControl: false,
|
versionControl: false,
|
||||||
|
auditLogs: false,
|
||||||
},
|
},
|
||||||
hideUsagePage: config.getEnv('hideUsagePage'),
|
hideUsagePage: config.getEnv('hideUsagePage'),
|
||||||
license: {
|
license: {
|
||||||
|
|
|
@ -17,6 +17,7 @@ const defaultSettings: IN8nUISettings = {
|
||||||
advancedExecutionFilters: false,
|
advancedExecutionFilters: false,
|
||||||
variables: true,
|
variables: true,
|
||||||
versionControl: false,
|
versionControl: false,
|
||||||
|
auditLogs: false,
|
||||||
},
|
},
|
||||||
executionMode: 'regular',
|
executionMode: 'regular',
|
||||||
executionTimeout: 0,
|
executionTimeout: 0,
|
||||||
|
|
|
@ -44,6 +44,7 @@ export const SETTINGS_STORE_DEFAULT_STATE: ISettingsState = {
|
||||||
logStreaming: false,
|
logStreaming: false,
|
||||||
variables: false,
|
variables: false,
|
||||||
versionControl: false,
|
versionControl: false,
|
||||||
|
auditLogs: false,
|
||||||
},
|
},
|
||||||
executionMode: 'regular',
|
executionMode: 'regular',
|
||||||
executionTimeout: 0,
|
executionTimeout: 0,
|
||||||
|
|
|
@ -74,6 +74,14 @@ export default defineComponent({
|
||||||
available: this.canAccessApiSettings(),
|
available: this.canAccessApiSettings(),
|
||||||
activateOnRouteNames: [VIEWS.API_SETTINGS],
|
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',
|
id: 'settings-version-control',
|
||||||
icon: 'code-branch',
|
icon: 'code-branch',
|
||||||
|
@ -159,6 +167,9 @@ export default defineComponent({
|
||||||
canAccessVersionControl(): boolean {
|
canAccessVersionControl(): boolean {
|
||||||
return this.canUserAccessRouteByName(VIEWS.VERSION_CONTROL);
|
return this.canUserAccessRouteByName(VIEWS.VERSION_CONTROL);
|
||||||
},
|
},
|
||||||
|
canAccessAuditLogs(): boolean {
|
||||||
|
return this.canUserAccessRouteByName(VIEWS.AUDIT_LOGS);
|
||||||
|
},
|
||||||
canAccessSso(): boolean {
|
canAccessSso(): boolean {
|
||||||
return this.canUserAccessRouteByName(VIEWS.SSO_SETTINGS);
|
return this.canUserAccessRouteByName(VIEWS.SSO_SETTINGS);
|
||||||
},
|
},
|
||||||
|
@ -220,6 +231,11 @@ export default defineComponent({
|
||||||
void this.$router.push({ name: VIEWS.VERSION_CONTROL });
|
void this.$router.push({ name: VIEWS.VERSION_CONTROL });
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case 'settings-audit-logs':
|
||||||
|
if (this.$router.currentRoute.name !== VIEWS.AUDIT_LOGS) {
|
||||||
|
void this.$router.push({ name: VIEWS.AUDIT_LOGS });
|
||||||
|
}
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
|
@ -376,6 +376,7 @@ export const enum VIEWS {
|
||||||
SSO_SETTINGS = 'SSoSettings',
|
SSO_SETTINGS = 'SSoSettings',
|
||||||
SAML_ONBOARDING = 'SamlOnboarding',
|
SAML_ONBOARDING = 'SamlOnboarding',
|
||||||
VERSION_CONTROL = 'VersionControl',
|
VERSION_CONTROL = 'VersionControl',
|
||||||
|
AUDIT_LOGS = 'AuditLogs',
|
||||||
}
|
}
|
||||||
|
|
||||||
export const enum FAKE_DOOR_FEATURES {
|
export const enum FAKE_DOOR_FEATURES {
|
||||||
|
@ -443,6 +444,7 @@ export const enum EnterpriseEditionFeature {
|
||||||
Variables = 'variables',
|
Variables = 'variables',
|
||||||
Saml = 'saml',
|
Saml = 'saml',
|
||||||
VersionControl = 'versionControl',
|
VersionControl = 'versionControl',
|
||||||
|
AuditLogs = 'auditLogs',
|
||||||
}
|
}
|
||||||
export const MAIN_NODE_PANEL_WIDTH = 360;
|
export const MAIN_NODE_PANEL_WIDTH = 360;
|
||||||
|
|
||||||
|
|
|
@ -1407,6 +1407,10 @@
|
||||||
"settings.versionControl.refreshBranches.success": "Branches successfully refreshed",
|
"settings.versionControl.refreshBranches.success": "Branches successfully refreshed",
|
||||||
"settings.versionControl.refreshBranches.error": "Error refreshing branches",
|
"settings.versionControl.refreshBranches.error": "Error refreshing branches",
|
||||||
"showMessage.cancel": "@:_reusableBaseText.cancel",
|
"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.ok": "OK",
|
||||||
"showMessage.showDetails": "Show Details",
|
"showMessage.showDetails": "Show Details",
|
||||||
"startupError": "Error connecting to n8n",
|
"startupError": "Error connecting to n8n",
|
||||||
|
|
|
@ -32,6 +32,7 @@ import {
|
||||||
faCodeBranch,
|
faCodeBranch,
|
||||||
faCog,
|
faCog,
|
||||||
faCogs,
|
faCogs,
|
||||||
|
faClipboardList,
|
||||||
faClock,
|
faClock,
|
||||||
faClone,
|
faClone,
|
||||||
faCloud,
|
faCloud,
|
||||||
|
@ -171,6 +172,7 @@ addIcon(faCode);
|
||||||
addIcon(faCodeBranch);
|
addIcon(faCodeBranch);
|
||||||
addIcon(faCog);
|
addIcon(faCog);
|
||||||
addIcon(faCogs);
|
addIcon(faCogs);
|
||||||
|
addIcon(faClipboardList);
|
||||||
addIcon(faClock);
|
addIcon(faClock);
|
||||||
addIcon(faClone);
|
addIcon(faClone);
|
||||||
addIcon(faCloud);
|
addIcon(faCloud);
|
||||||
|
|
|
@ -42,6 +42,7 @@ import SettingsSso from './views/SettingsSso.vue';
|
||||||
import SignoutView from '@/views/SignoutView.vue';
|
import SignoutView from '@/views/SignoutView.vue';
|
||||||
import SamlOnboarding from '@/views/SamlOnboarding.vue';
|
import SamlOnboarding from '@/views/SamlOnboarding.vue';
|
||||||
import SettingsVersionControl from './views/SettingsVersionControl.vue';
|
import SettingsVersionControl from './views/SettingsVersionControl.vue';
|
||||||
|
import SettingsAuditLogs from './views/SettingsAuditLogs.vue';
|
||||||
import { usePostHog } from './stores/posthog.store';
|
import { usePostHog } from './stores/posthog.store';
|
||||||
|
|
||||||
Vue.use(Router);
|
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'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
16
packages/editor-ui/src/stores/auditLogs.store.ts
Normal file
16
packages/editor-ui/src/stores/auditLogs.store.ts
Normal 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,
|
||||||
|
};
|
||||||
|
});
|
|
@ -24,3 +24,4 @@ export * from './workflows.store';
|
||||||
export * from './cloudPlan.store';
|
export * from './cloudPlan.store';
|
||||||
export * from './versionControl.store';
|
export * from './versionControl.store';
|
||||||
export * from './sso.store';
|
export * from './sso.store';
|
||||||
|
export * from './auditLogs.store';
|
||||||
|
|
42
packages/editor-ui/src/views/SettingsAuditLogs.vue
Normal file
42
packages/editor-ui/src/views/SettingsAuditLogs.vue
Normal 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>
|
|
@ -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();
|
||||||
|
});
|
||||||
|
});
|
|
@ -2112,6 +2112,7 @@ export interface IN8nUISettings {
|
||||||
advancedExecutionFilters: boolean;
|
advancedExecutionFilters: boolean;
|
||||||
variables: boolean;
|
variables: boolean;
|
||||||
versionControl: boolean;
|
versionControl: boolean;
|
||||||
|
auditLogs: boolean;
|
||||||
};
|
};
|
||||||
hideUsagePage: boolean;
|
hideUsagePage: boolean;
|
||||||
license: {
|
license: {
|
||||||
|
|
Loading…
Reference in a new issue