mirror of
https://github.com/n8n-io/n8n.git
synced 2024-11-15 17:14:05 -08:00
885dba6f12
## Summary Provide details about your pull request and what it adds, fixes, or changes. Photos and videos are recommended. As part of NodeView refactor, this PR migrates all externalHooks calls to `useExternalHooks` composable. #### How to test the change: 1. Run using env `export N8N_DEPLOYMENT_TYPE=cloud` 2. Hooks should still run as expected ## Issues fixed Include links to Github issue or Community forum post or **Linear ticket**: > Important in order to close automatically and provide context to reviewers https://linear.app/n8n/issue/N8N-6349/externalhooks ## Review / Merge checklist - [x] PR title and summary are descriptive. **Remember, the title automatically goes into the changelog. Use `(no-changelog)` otherwise.** ([conventions](https://github.com/n8n-io/n8n/blob/master/.github/pull_request_title_conventions.md)) - [x] [Docs updated](https://github.com/n8n-io/n8n-docs) or follow-up ticket created. - [x] Tests included. > A bug is not considered fixed, unless a test is added to prevent it from happening again. A feature is not complete without tests. > > *(internal)* You can use Slack commands to trigger [e2e tests](https://www.notion.so/n8n/How-to-use-Test-Instances-d65f49dfc51f441ea44367fb6f67eb0a?pvs=4#a39f9e5ba64a48b58a71d81c837e8227) or [deploy test instance](https://www.notion.so/n8n/How-to-use-Test-Instances-d65f49dfc51f441ea44367fb6f67eb0a?pvs=4#f6a177d32bde4b57ae2da0b8e454bfce) or [deploy early access version on Cloud](https://www.notion.so/n8n/Cloudbot-3dbe779836004972b7057bc989526998?pvs=4#fef2d36ab02247e1a0f65a74f6fb534e).
631 lines
16 KiB
Vue
631 lines
16 KiB
Vue
<template>
|
|
<div
|
|
id="side-menu"
|
|
:class="{
|
|
['side-menu']: true,
|
|
[$style.sideMenu]: true,
|
|
[$style.sideMenuCollapsed]: isCollapsed,
|
|
}"
|
|
>
|
|
<div
|
|
id="collapse-change-button"
|
|
:class="['clickable', $style.sideMenuCollapseButton]"
|
|
@click="toggleCollapse"
|
|
>
|
|
<n8n-icon v-if="isCollapsed" icon="chevron-right" size="xsmall" class="ml-5xs" />
|
|
<n8n-icon v-else icon="chevron-left" size="xsmall" class="mr-5xs" />
|
|
</div>
|
|
<n8n-menu :items="mainMenuItems" :collapsed="isCollapsed" @select="handleSelect">
|
|
<template #header>
|
|
<div :class="$style.logo">
|
|
<img :src="logoPath" data-test-id="n8n-logo" :class="$style.icon" alt="n8n" />
|
|
</div>
|
|
</template>
|
|
|
|
<template #beforeLowerMenu>
|
|
<ExecutionsUsage
|
|
:cloud-plan-data="currentPlanAndUsageData"
|
|
v-if="fullyExpanded && userIsTrialing"
|
|
/></template>
|
|
<template #menuSuffix>
|
|
<div>
|
|
<div v-if="hasVersionUpdates" :class="$style.updates" @click="openUpdatesPanel">
|
|
<div :class="$style.giftContainer">
|
|
<GiftNotificationIcon />
|
|
</div>
|
|
<n8n-text
|
|
:class="{ ['ml-xs']: true, [$style.expanded]: fullyExpanded }"
|
|
color="text-base"
|
|
>
|
|
{{ nextVersions.length > 99 ? '99+' : nextVersions.length }} update{{
|
|
nextVersions.length > 1 ? 's' : ''
|
|
}}
|
|
</n8n-text>
|
|
</div>
|
|
<MainSidebarSourceControl :is-collapsed="isCollapsed" />
|
|
</div>
|
|
</template>
|
|
<template #footer v-if="showUserArea">
|
|
<div :class="$style.userArea">
|
|
<div class="ml-3xs" data-test-id="main-sidebar-user-menu">
|
|
<!-- This dropdown is only enabled when sidebar is collapsed -->
|
|
<el-dropdown
|
|
:disabled="!isCollapsed"
|
|
placement="right-end"
|
|
trigger="click"
|
|
@command="onUserActionToggle"
|
|
>
|
|
<div :class="{ [$style.avatar]: true, ['clickable']: isCollapsed }">
|
|
<n8n-avatar
|
|
:firstName="usersStore.currentUser.firstName"
|
|
:lastName="usersStore.currentUser.lastName"
|
|
size="small"
|
|
/>
|
|
</div>
|
|
<template #dropdown>
|
|
<el-dropdown-menu>
|
|
<el-dropdown-item command="settings">
|
|
{{ $locale.baseText('settings') }}
|
|
</el-dropdown-item>
|
|
<el-dropdown-item command="logout">
|
|
{{ $locale.baseText('auth.signout') }}
|
|
</el-dropdown-item>
|
|
</el-dropdown-menu>
|
|
</template>
|
|
</el-dropdown>
|
|
</div>
|
|
<div
|
|
:class="{ ['ml-2xs']: true, [$style.userName]: true, [$style.expanded]: fullyExpanded }"
|
|
>
|
|
<n8n-text size="small" :bold="true" color="text-dark">{{
|
|
usersStore.currentUser.fullName
|
|
}}</n8n-text>
|
|
</div>
|
|
<div :class="{ [$style.userActions]: true, [$style.expanded]: fullyExpanded }">
|
|
<n8n-action-dropdown
|
|
:items="userMenuItems"
|
|
placement="top-start"
|
|
data-test-id="user-menu"
|
|
@select="onUserActionToggle"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</n8n-menu>
|
|
</div>
|
|
</template>
|
|
|
|
<script lang="ts">
|
|
import type { CloudPlanAndUsageData, IExecutionResponse, IMenuItem, IVersion } from '@/Interface';
|
|
import GiftNotificationIcon from './GiftNotificationIcon.vue';
|
|
|
|
import { genericHelpers } from '@/mixins/genericHelpers';
|
|
import { useMessage } from '@/composables/useMessage';
|
|
import { workflowHelpers } from '@/mixins/workflowHelpers';
|
|
import { workflowRun } from '@/mixins/workflowRun';
|
|
|
|
import { ABOUT_MODAL_KEY, VERSIONS_MODAL_KEY, VIEWS } from '@/constants';
|
|
import { userHelpers } from '@/mixins/userHelpers';
|
|
import { debounceHelper } from '@/mixins/debounce';
|
|
import { defineComponent } from 'vue';
|
|
import { mapStores } from 'pinia';
|
|
import { useCloudPlanStore } from '@/stores/cloudPlan.store';
|
|
import { useRootStore } from '@/stores/n8nRoot.store';
|
|
import { useSettingsStore } from '@/stores/settings.store';
|
|
import { useSourceControlStore } from '@/stores/sourceControl.store';
|
|
import { useUIStore } from '@/stores/ui.store';
|
|
import { useUsersStore } from '@/stores/users.store';
|
|
import { useVersionsStore } from '@/stores/versions.store';
|
|
import { useWorkflowsStore } from '@/stores/workflows.store';
|
|
import { isNavigationFailure } from 'vue-router';
|
|
import ExecutionsUsage from '@/components/ExecutionsUsage.vue';
|
|
import MainSidebarSourceControl from '@/components/MainSidebarSourceControl.vue';
|
|
import { hasPermission } from '@/rbac/permissions';
|
|
import { useExternalHooks } from '@/composables/useExternalHooks';
|
|
|
|
export default defineComponent({
|
|
name: 'MainSidebar',
|
|
components: {
|
|
GiftNotificationIcon,
|
|
ExecutionsUsage,
|
|
MainSidebarSourceControl,
|
|
},
|
|
mixins: [genericHelpers, workflowHelpers, workflowRun, userHelpers, debounceHelper],
|
|
setup(props, ctx) {
|
|
const externalHooks = useExternalHooks();
|
|
|
|
return {
|
|
externalHooks,
|
|
...useMessage(),
|
|
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
|
...workflowRun.setup?.(props, ctx),
|
|
};
|
|
},
|
|
data() {
|
|
return {
|
|
basePath: '',
|
|
fullyExpanded: false,
|
|
};
|
|
},
|
|
computed: {
|
|
...mapStores(
|
|
useRootStore,
|
|
useSettingsStore,
|
|
useUIStore,
|
|
useUsersStore,
|
|
useVersionsStore,
|
|
useWorkflowsStore,
|
|
useCloudPlanStore,
|
|
useSourceControlStore,
|
|
),
|
|
logoPath(): string {
|
|
if (this.isCollapsed) return this.basePath + 'n8n-logo-collapsed.svg';
|
|
|
|
return this.basePath + this.uiStore.logo;
|
|
},
|
|
hasVersionUpdates(): boolean {
|
|
return (
|
|
this.settingsStore.settings.releaseChannel === 'stable' &&
|
|
this.versionsStore.hasVersionUpdates
|
|
);
|
|
},
|
|
nextVersions(): IVersion[] {
|
|
return this.versionsStore.nextVersions;
|
|
},
|
|
isCollapsed(): boolean {
|
|
return this.uiStore.sidebarMenuCollapsed;
|
|
},
|
|
canUserAccessSettings(): boolean {
|
|
const accessibleRoute = this.findFirstAccessibleSettingsRoute();
|
|
return accessibleRoute !== null;
|
|
},
|
|
showUserArea(): boolean {
|
|
return hasPermission(['authenticated']);
|
|
},
|
|
workflowExecution(): IExecutionResponse | null {
|
|
return this.workflowsStore.getWorkflowExecution;
|
|
},
|
|
userMenuItems(): object[] {
|
|
return [
|
|
{
|
|
id: 'settings',
|
|
label: this.$locale.baseText('settings'),
|
|
},
|
|
{
|
|
id: 'logout',
|
|
label: this.$locale.baseText('auth.signout'),
|
|
},
|
|
];
|
|
},
|
|
mainMenuItems(): IMenuItem[] {
|
|
const items: IMenuItem[] = [];
|
|
const injectedItems = this.uiStore.sidebarMenuItems;
|
|
|
|
const workflows: IMenuItem = {
|
|
id: 'workflows',
|
|
icon: 'network-wired',
|
|
label: this.$locale.baseText('mainSidebar.workflows'),
|
|
position: 'top',
|
|
activateOnRouteNames: [VIEWS.WORKFLOWS],
|
|
};
|
|
|
|
if (this.sourceControlStore.preferences.branchReadOnly) {
|
|
workflows.secondaryIcon = {
|
|
name: 'lock',
|
|
tooltip: {
|
|
content: this.$locale.baseText('mainSidebar.workflows.readOnlyEnv.tooltip'),
|
|
},
|
|
};
|
|
}
|
|
|
|
if (injectedItems && injectedItems.length > 0) {
|
|
for (const item of injectedItems) {
|
|
items.push({
|
|
id: item.id,
|
|
icon: item.icon || '',
|
|
label: item.label || '',
|
|
position: item.position,
|
|
type: item.properties?.href ? 'link' : 'regular',
|
|
properties: item.properties,
|
|
} as IMenuItem);
|
|
}
|
|
}
|
|
|
|
const regularItems: IMenuItem[] = [
|
|
workflows,
|
|
{
|
|
id: 'templates',
|
|
icon: 'box-open',
|
|
label: this.$locale.baseText('mainSidebar.templates'),
|
|
position: 'top',
|
|
available: this.settingsStore.isTemplatesEnabled,
|
|
activateOnRouteNames: [VIEWS.TEMPLATES],
|
|
},
|
|
{
|
|
id: 'credentials',
|
|
icon: 'key',
|
|
label: this.$locale.baseText('mainSidebar.credentials'),
|
|
customIconSize: 'medium',
|
|
position: 'top',
|
|
activateOnRouteNames: [VIEWS.CREDENTIALS],
|
|
},
|
|
{
|
|
id: 'variables',
|
|
icon: 'variable',
|
|
label: this.$locale.baseText('mainSidebar.variables'),
|
|
customIconSize: 'medium',
|
|
position: 'top',
|
|
activateOnRouteNames: [VIEWS.VARIABLES],
|
|
},
|
|
{
|
|
id: 'executions',
|
|
icon: 'tasks',
|
|
label: this.$locale.baseText('mainSidebar.executions'),
|
|
position: 'top',
|
|
activateOnRouteNames: [VIEWS.EXECUTIONS],
|
|
},
|
|
{
|
|
id: 'cloud-admin',
|
|
type: 'link',
|
|
position: 'bottom',
|
|
label: 'Admin Panel',
|
|
icon: 'home',
|
|
available: this.settingsStore.isCloudDeployment && hasPermission(['instanceOwner']),
|
|
},
|
|
{
|
|
id: 'settings',
|
|
icon: 'cog',
|
|
label: this.$locale.baseText('settings'),
|
|
position: 'bottom',
|
|
available: this.canUserAccessSettings && this.usersStore.currentUser !== null,
|
|
activateOnRouteNames: [VIEWS.USERS_SETTINGS, VIEWS.API_SETTINGS, VIEWS.PERSONAL_SETTINGS],
|
|
},
|
|
{
|
|
id: 'help',
|
|
icon: 'question',
|
|
label: 'Help',
|
|
position: 'bottom',
|
|
children: [
|
|
{
|
|
id: 'quickstart',
|
|
icon: 'video',
|
|
label: this.$locale.baseText('mainSidebar.helpMenuItems.quickstart'),
|
|
type: 'link',
|
|
properties: {
|
|
href: 'https://www.youtube.com/watch?v=1MwSoB0gnM4',
|
|
newWindow: true,
|
|
},
|
|
},
|
|
{
|
|
id: 'docs',
|
|
icon: 'book',
|
|
label: this.$locale.baseText('mainSidebar.helpMenuItems.documentation'),
|
|
type: 'link',
|
|
properties: {
|
|
href: 'https://docs.n8n.io?utm_source=n8n_app&utm_medium=app_sidebar',
|
|
newWindow: true,
|
|
},
|
|
},
|
|
{
|
|
id: 'forum',
|
|
icon: 'users',
|
|
label: this.$locale.baseText('mainSidebar.helpMenuItems.forum'),
|
|
type: 'link',
|
|
properties: {
|
|
href: 'https://community.n8n.io?utm_source=n8n_app&utm_medium=app_sidebar',
|
|
newWindow: true,
|
|
},
|
|
},
|
|
{
|
|
id: 'examples',
|
|
icon: 'graduation-cap',
|
|
label: this.$locale.baseText('mainSidebar.helpMenuItems.course'),
|
|
type: 'link',
|
|
properties: {
|
|
href: 'https://www.youtube.com/watch?v=1MwSoB0gnM4',
|
|
newWindow: true,
|
|
},
|
|
},
|
|
{
|
|
id: 'about',
|
|
icon: 'info',
|
|
label: this.$locale.baseText('mainSidebar.aboutN8n'),
|
|
position: 'bottom',
|
|
},
|
|
],
|
|
},
|
|
];
|
|
return [...items, ...regularItems];
|
|
},
|
|
userIsTrialing(): boolean {
|
|
return this.cloudPlanStore.userIsTrialing;
|
|
},
|
|
currentPlanAndUsageData(): CloudPlanAndUsageData | null {
|
|
const planData = this.cloudPlanStore.currentPlanData;
|
|
const usage = this.cloudPlanStore.currentUsageData;
|
|
if (!planData || !usage) return null;
|
|
return {
|
|
...planData,
|
|
usage,
|
|
};
|
|
},
|
|
},
|
|
async mounted() {
|
|
this.basePath = this.rootStore.baseUrl;
|
|
if (this.$refs.user) {
|
|
void this.externalHooks.run('mainSidebar.mounted', {
|
|
userRef: this.$refs.user as Element,
|
|
});
|
|
}
|
|
|
|
void this.$nextTick(() => {
|
|
if (window.innerWidth < 900 || this.uiStore.isNodeView) {
|
|
this.uiStore.sidebarMenuCollapsed = true;
|
|
} else {
|
|
this.uiStore.sidebarMenuCollapsed = false;
|
|
}
|
|
|
|
this.fullyExpanded = !this.isCollapsed;
|
|
});
|
|
},
|
|
created() {
|
|
window.addEventListener('resize', this.onResize);
|
|
},
|
|
beforeUnmount() {
|
|
window.removeEventListener('resize', this.onResize);
|
|
},
|
|
methods: {
|
|
trackHelpItemClick(itemType: string) {
|
|
this.$telemetry.track('User clicked help resource', {
|
|
type: itemType,
|
|
workflow_id: this.workflowsStore.workflowId,
|
|
});
|
|
},
|
|
async onUserActionToggle(action: string) {
|
|
switch (action) {
|
|
case 'logout':
|
|
this.onLogout();
|
|
break;
|
|
case 'settings':
|
|
void this.$router.push({ name: VIEWS.PERSONAL_SETTINGS });
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
},
|
|
onLogout() {
|
|
void this.$router.push({ name: VIEWS.SIGNOUT });
|
|
},
|
|
toggleCollapse() {
|
|
this.uiStore.toggleSidebarMenuCollapse();
|
|
// When expanding, delay showing some element to ensure smooth animation
|
|
if (!this.isCollapsed) {
|
|
setTimeout(() => {
|
|
this.fullyExpanded = !this.isCollapsed;
|
|
}, 300);
|
|
} else {
|
|
this.fullyExpanded = !this.isCollapsed;
|
|
}
|
|
},
|
|
openUpdatesPanel() {
|
|
this.uiStore.openModal(VERSIONS_MODAL_KEY);
|
|
},
|
|
async handleSelect(key: string) {
|
|
switch (key) {
|
|
case 'workflows': {
|
|
if (this.$router.currentRoute.name !== VIEWS.WORKFLOWS) {
|
|
this.goToRoute({ name: VIEWS.WORKFLOWS });
|
|
}
|
|
break;
|
|
}
|
|
case 'templates': {
|
|
if (this.$router.currentRoute.name !== VIEWS.TEMPLATES) {
|
|
this.goToRoute({ name: VIEWS.TEMPLATES });
|
|
}
|
|
break;
|
|
}
|
|
case 'credentials': {
|
|
if (this.$router.currentRoute.name !== VIEWS.CREDENTIALS) {
|
|
this.goToRoute({ name: VIEWS.CREDENTIALS });
|
|
}
|
|
break;
|
|
}
|
|
case 'variables': {
|
|
if (this.$router.currentRoute.name !== VIEWS.VARIABLES) {
|
|
this.goToRoute({ name: VIEWS.VARIABLES });
|
|
}
|
|
break;
|
|
}
|
|
case 'executions': {
|
|
if (this.$router.currentRoute.name !== VIEWS.EXECUTIONS) {
|
|
this.goToRoute({ name: VIEWS.EXECUTIONS });
|
|
}
|
|
break;
|
|
}
|
|
case 'settings': {
|
|
const defaultRoute = this.findFirstAccessibleSettingsRoute();
|
|
if (defaultRoute) {
|
|
const route = this.$router.resolve({ name: defaultRoute });
|
|
if (this.$router.currentRoute.name !== defaultRoute) {
|
|
this.goToRoute(route.path);
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case 'about': {
|
|
this.trackHelpItemClick('about');
|
|
this.uiStore.openModal(ABOUT_MODAL_KEY);
|
|
break;
|
|
}
|
|
case 'cloud-admin': {
|
|
this.cloudPlanStore.redirectToDashboard();
|
|
break;
|
|
}
|
|
case 'quickstart':
|
|
case 'docs':
|
|
case 'forum':
|
|
case 'examples': {
|
|
this.trackHelpItemClick(key);
|
|
break;
|
|
}
|
|
default:
|
|
break;
|
|
}
|
|
},
|
|
goToRoute(route: string | { name: string }) {
|
|
this.$router.push(route).catch((failure) => {
|
|
console.log(failure);
|
|
// Catch navigation failures caused by route guards
|
|
if (!isNavigationFailure(failure)) {
|
|
console.error(failure);
|
|
}
|
|
});
|
|
},
|
|
findFirstAccessibleSettingsRoute() {
|
|
const settingsRoutes = this.$router
|
|
.getRoutes()
|
|
.find((route) => route.path === '/settings')!
|
|
.children.map((route) => route.name || '');
|
|
|
|
let defaultSettingsRoute = null;
|
|
for (const route of settingsRoutes) {
|
|
if (this.canUserAccessRouteByName(route)) {
|
|
defaultSettingsRoute = route;
|
|
break;
|
|
}
|
|
}
|
|
|
|
return defaultSettingsRoute;
|
|
},
|
|
onResize(event: UIEvent) {
|
|
void this.callDebounced('onResizeEnd', { debounceTime: 100 }, event);
|
|
},
|
|
async onResizeEnd(event: UIEvent) {
|
|
const browserWidth = (event.target as Window).outerWidth;
|
|
await this.checkWidthAndAdjustSidebar(browserWidth);
|
|
},
|
|
async checkWidthAndAdjustSidebar(width: number) {
|
|
if (width < 900) {
|
|
this.uiStore.sidebarMenuCollapsed = true;
|
|
await this.$nextTick();
|
|
this.fullyExpanded = !this.isCollapsed;
|
|
}
|
|
},
|
|
},
|
|
});
|
|
</script>
|
|
|
|
<style lang="scss" module>
|
|
.sideMenu {
|
|
position: relative;
|
|
height: 100%;
|
|
border-right: var(--border-width-base) var(--border-style-base) var(--color-foreground-base);
|
|
transition: width 150ms ease-in-out;
|
|
width: $sidebar-expanded-width;
|
|
.logo {
|
|
height: $header-height;
|
|
display: flex;
|
|
align-items: center;
|
|
padding: var(--spacing-xs);
|
|
|
|
img {
|
|
position: relative;
|
|
left: 1px;
|
|
height: 20px;
|
|
}
|
|
}
|
|
|
|
&.sideMenuCollapsed {
|
|
width: $sidebar-width;
|
|
|
|
.logo img {
|
|
left: 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
.sideMenuCollapseButton {
|
|
position: absolute;
|
|
right: -10px;
|
|
top: 50%;
|
|
z-index: 999;
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
color: var(--color-text-base);
|
|
background-color: var(--color-foreground-xlight);
|
|
width: 20px;
|
|
height: 20px;
|
|
border: var(--border-width-base) var(--border-style-base) var(--color-foreground-base);
|
|
border-radius: 50%;
|
|
|
|
&:hover {
|
|
color: var(--color-primary-shade-1);
|
|
}
|
|
}
|
|
|
|
.updates {
|
|
display: flex;
|
|
align-items: center;
|
|
cursor: pointer;
|
|
padding: var(--spacing-2xs) var(--spacing-l);
|
|
margin: var(--spacing-2xs) 0 0;
|
|
|
|
svg {
|
|
color: var(--color-text-base) !important;
|
|
}
|
|
span {
|
|
display: none;
|
|
&.expanded {
|
|
display: initial;
|
|
}
|
|
}
|
|
|
|
&:hover {
|
|
&,
|
|
& svg {
|
|
color: var(--color-text-dark) !important;
|
|
}
|
|
}
|
|
}
|
|
|
|
.userArea {
|
|
display: flex;
|
|
padding: var(--spacing-xs);
|
|
align-items: center;
|
|
height: 60px;
|
|
border-top: var(--border-width-base) var(--border-style-base) var(--color-foreground-base);
|
|
|
|
.userName {
|
|
display: none;
|
|
overflow: hidden;
|
|
width: 100px;
|
|
white-space: nowrap;
|
|
text-overflow: ellipsis;
|
|
|
|
&.expanded {
|
|
display: initial;
|
|
}
|
|
|
|
span {
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
}
|
|
|
|
.userActions {
|
|
display: none;
|
|
|
|
&.expanded {
|
|
display: initial;
|
|
}
|
|
}
|
|
}
|
|
|
|
@media screen and (max-height: 470px) {
|
|
:global(#help) {
|
|
display: none;
|
|
}
|
|
}
|
|
</style>
|