mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 01:56:46 +00:00
fix(editor): Add user role tooltip to personal settings page (#15941)
Co-authored-by: Raúl Gómez Morales <raul00gm@gmail.com>
This commit is contained in:
@@ -147,6 +147,7 @@
|
|||||||
"auth.newPassword": "New password",
|
"auth.newPassword": "New password",
|
||||||
"auth.password": "Password",
|
"auth.password": "Password",
|
||||||
"auth.role": "Role",
|
"auth.role": "Role",
|
||||||
|
"auth.roles.default": "Default",
|
||||||
"auth.roles.member": "Member",
|
"auth.roles.member": "Member",
|
||||||
"auth.roles.admin": "Admin",
|
"auth.roles.admin": "Admin",
|
||||||
"auth.roles.owner": "Owner",
|
"auth.roles.owner": "Owner",
|
||||||
@@ -1892,6 +1893,11 @@
|
|||||||
"settings.personal.personalSettings": "Personal Settings",
|
"settings.personal.personalSettings": "Personal Settings",
|
||||||
"settings.personal.personalSettingsUpdated": "Personal details updated",
|
"settings.personal.personalSettingsUpdated": "Personal details updated",
|
||||||
"settings.personal.personalSettingsUpdatedError": "Problem updating your details",
|
"settings.personal.personalSettingsUpdatedError": "Problem updating your details",
|
||||||
|
"settings.personal.role.tooltip.default": "Default role for new users",
|
||||||
|
"settings.personal.role.tooltip.member": "Create and manage own workflows and credentials",
|
||||||
|
"settings.personal.role.tooltip.admin": "Full access to manage workflows,tags, credentials, projects, users and more",
|
||||||
|
"settings.personal.role.tooltip.owner": "Manage everything{cloudAccess}",
|
||||||
|
"settings.personal.role.tooltip.cloud": " and access Cloud dashboard",
|
||||||
"settings.personal.save": "Save",
|
"settings.personal.save": "Save",
|
||||||
"settings.personal.security": "Security",
|
"settings.personal.security": "Security",
|
||||||
"settings.signup.signUpInviterInfo": "{firstName} {lastName} has invited you to n8n",
|
"settings.signup.signUpInviterInfo": "{firstName} {lastName} has invited you to n8n",
|
||||||
|
|||||||
@@ -47,9 +47,9 @@ export const useCloudPlanStore = defineStore(STORES.CLOUD_PLAN, () => {
|
|||||||
return state.usage?.executions >= state.data?.monthlyExecutionsLimit;
|
return state.usage?.executions >= state.data?.monthlyExecutionsLimit;
|
||||||
});
|
});
|
||||||
|
|
||||||
const hasCloudPlan = computed(() => {
|
const hasCloudPlan = computed<boolean>(() => {
|
||||||
const cloudUserId = settingsStore.settings.n8nMetadata?.userId;
|
const cloudUserId = settingsStore.settings.n8nMetadata?.userId;
|
||||||
return hasPermission(['instanceOwner']) && settingsStore.isCloudDeployment && cloudUserId;
|
return hasPermission(['instanceOwner']) && settingsStore.isCloudDeployment && !!cloudUserId;
|
||||||
});
|
});
|
||||||
|
|
||||||
const getUserCloudAccount = async () => {
|
const getUserCloudAccount = async () => {
|
||||||
@@ -189,6 +189,7 @@ export const useCloudPlanStore = defineStore(STORES.CLOUD_PLAN, () => {
|
|||||||
currentUsageData,
|
currentUsageData,
|
||||||
trialExpired,
|
trialExpired,
|
||||||
allExecutionsUsed,
|
allExecutionsUsed,
|
||||||
|
hasCloudPlan,
|
||||||
generateCloudDashboardAutoLoginLink,
|
generateCloudDashboardAutoLoginLink,
|
||||||
initialize,
|
initialize,
|
||||||
getOwnerCurrentPlan,
|
getOwnerCurrentPlan,
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import userEvent from '@testing-library/user-event';
|
||||||
import { createPinia } from 'pinia';
|
import { createPinia } from 'pinia';
|
||||||
import { waitAllPromises } from '@/__tests__/utils';
|
import { waitAllPromises } from '@/__tests__/utils';
|
||||||
import SettingsPersonalView from '@/views/SettingsPersonalView.vue';
|
import SettingsPersonalView from '@/views/SettingsPersonalView.vue';
|
||||||
@@ -7,11 +8,13 @@ import { createComponentRenderer } from '@/__tests__/render';
|
|||||||
import { setupServer } from '@/__tests__/server';
|
import { setupServer } from '@/__tests__/server';
|
||||||
import { ROLE } from '@/constants';
|
import { ROLE } from '@/constants';
|
||||||
import { useUIStore } from '@/stores/ui.store';
|
import { useUIStore } from '@/stores/ui.store';
|
||||||
|
import { useCloudPlanStore } from '@/stores/cloudPlan.store';
|
||||||
|
|
||||||
let pinia: ReturnType<typeof createPinia>;
|
let pinia: ReturnType<typeof createPinia>;
|
||||||
let settingsStore: ReturnType<typeof useSettingsStore>;
|
let settingsStore: ReturnType<typeof useSettingsStore>;
|
||||||
let usersStore: ReturnType<typeof useUsersStore>;
|
let usersStore: ReturnType<typeof useUsersStore>;
|
||||||
let uiStore: ReturnType<typeof useUIStore>;
|
let uiStore: ReturnType<typeof useUIStore>;
|
||||||
|
let cloudPlanStore: ReturnType<typeof useCloudPlanStore>;
|
||||||
let server: ReturnType<typeof setupServer>;
|
let server: ReturnType<typeof setupServer>;
|
||||||
|
|
||||||
const renderComponent = createComponentRenderer(SettingsPersonalView);
|
const renderComponent = createComponentRenderer(SettingsPersonalView);
|
||||||
@@ -40,6 +43,7 @@ describe('SettingsPersonalView', () => {
|
|||||||
settingsStore = useSettingsStore(pinia);
|
settingsStore = useSettingsStore(pinia);
|
||||||
usersStore = useUsersStore(pinia);
|
usersStore = useUsersStore(pinia);
|
||||||
uiStore = useUIStore(pinia);
|
uiStore = useUIStore(pinia);
|
||||||
|
cloudPlanStore = useCloudPlanStore(pinia);
|
||||||
|
|
||||||
usersStore.usersById[currentUser.id] = currentUser;
|
usersStore.usersById[currentUser.id] = currentUser;
|
||||||
usersStore.currentUserId = currentUser.id;
|
usersStore.currentUserId = currentUser.id;
|
||||||
@@ -143,4 +147,31 @@ describe('SettingsPersonalView', () => {
|
|||||||
expect(queryByTestId('mfa-section')).not.toBeInTheDocument();
|
expect(queryByTestId('mfa-section')).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test.each([
|
||||||
|
['Default', ROLE.Default, false, 'Default role for new users'],
|
||||||
|
['Member', ROLE.Member, false, 'Create and manage own workflows and credentials'],
|
||||||
|
[
|
||||||
|
'Admin',
|
||||||
|
ROLE.Admin,
|
||||||
|
false,
|
||||||
|
'Full access to manage workflows,tags, credentials, projects, users and more',
|
||||||
|
],
|
||||||
|
['Owner', ROLE.Owner, false, 'Manage everything'],
|
||||||
|
['Owner', ROLE.Owner, true, 'Manage everything and access Cloud dashboard'],
|
||||||
|
])('should show %s user role information', async (label, role, hasCloudPlan, tooltipText) => {
|
||||||
|
vi.spyOn(cloudPlanStore, 'hasCloudPlan', 'get').mockReturnValue(hasCloudPlan);
|
||||||
|
vi.spyOn(usersStore, 'globalRoleName', 'get').mockReturnValue(role);
|
||||||
|
|
||||||
|
const { queryByTestId, getByText } = renderComponent({ pinia });
|
||||||
|
await waitAllPromises();
|
||||||
|
|
||||||
|
expect(queryByTestId('current-user-role')).toBeVisible();
|
||||||
|
expect(queryByTestId('current-user-role')).toHaveTextContent(label);
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
|
await userEvent.hover(queryByTestId('current-user-role')!);
|
||||||
|
|
||||||
|
expect(getByText(tooltipText)).toBeVisible();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,16 +3,18 @@ import { ref, computed, onMounted, onBeforeUnmount } from 'vue';
|
|||||||
import { useI18n } from '@n8n/i18n';
|
import { useI18n } from '@n8n/i18n';
|
||||||
import { useToast } from '@/composables/useToast';
|
import { useToast } from '@/composables/useToast';
|
||||||
import { useDocumentTitle } from '@/composables/useDocumentTitle';
|
import { useDocumentTitle } from '@/composables/useDocumentTitle';
|
||||||
import type { IFormInputs, IUser, ThemeOption } from '@/Interface';
|
import type { IFormInputs, IRole, IUser, ThemeOption } from '@/Interface';
|
||||||
import {
|
import {
|
||||||
CHANGE_PASSWORD_MODAL_KEY,
|
CHANGE_PASSWORD_MODAL_KEY,
|
||||||
MFA_DOCS_URL,
|
MFA_DOCS_URL,
|
||||||
MFA_SETUP_MODAL_KEY,
|
MFA_SETUP_MODAL_KEY,
|
||||||
PROMPT_MFA_CODE_MODAL_KEY,
|
PROMPT_MFA_CODE_MODAL_KEY,
|
||||||
|
ROLE,
|
||||||
} from '@/constants';
|
} from '@/constants';
|
||||||
import { useUIStore } from '@/stores/ui.store';
|
import { useUIStore } from '@/stores/ui.store';
|
||||||
import { useUsersStore } from '@/stores/users.store';
|
import { useUsersStore } from '@/stores/users.store';
|
||||||
import { useSettingsStore } from '@/stores/settings.store';
|
import { useSettingsStore } from '@/stores/settings.store';
|
||||||
|
import { useCloudPlanStore } from '@/stores/cloudPlan.store';
|
||||||
import { createFormEventBus } from '@n8n/design-system/utils';
|
import { createFormEventBus } from '@n8n/design-system/utils';
|
||||||
import type { MfaModalEvents } from '@/event-bus/mfa';
|
import type { MfaModalEvents } from '@/event-bus/mfa';
|
||||||
import { promptMfaCodeBus } from '@/event-bus/mfa';
|
import { promptMfaCodeBus } from '@/event-bus/mfa';
|
||||||
@@ -28,6 +30,11 @@ type UserBasicDetailsWithMfa = UserBasicDetailsForm & {
|
|||||||
mfaCode?: string;
|
mfaCode?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type RoleContent = {
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
};
|
||||||
|
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
const { showToast, showError } = useToast();
|
const { showToast, showError } = useToast();
|
||||||
const documentTitle = useDocumentTitle();
|
const documentTitle = useDocumentTitle();
|
||||||
@@ -55,6 +62,7 @@ const themeOptions = ref<Array<{ name: ThemeOption; label: BaseTextKey }>>([
|
|||||||
const uiStore = useUIStore();
|
const uiStore = useUIStore();
|
||||||
const usersStore = useUsersStore();
|
const usersStore = useUsersStore();
|
||||||
const settingsStore = useSettingsStore();
|
const settingsStore = useSettingsStore();
|
||||||
|
const cloudPlanStore = useCloudPlanStore();
|
||||||
|
|
||||||
const currentUser = computed((): IUser | null => {
|
const currentUser = computed((): IUser | null => {
|
||||||
return usersStore.currentUser;
|
return usersStore.currentUser;
|
||||||
@@ -82,6 +90,33 @@ const hasAnyChanges = computed(() => {
|
|||||||
return hasAnyBasicInfoChanges.value || hasAnyPersonalisationChanges.value;
|
return hasAnyBasicInfoChanges.value || hasAnyPersonalisationChanges.value;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const roles = computed<Record<IRole, RoleContent>>(() => ({
|
||||||
|
[ROLE.Default]: {
|
||||||
|
name: i18n.baseText('auth.roles.default'),
|
||||||
|
description: i18n.baseText('settings.personal.role.tooltip.default'),
|
||||||
|
},
|
||||||
|
[ROLE.Member]: {
|
||||||
|
name: i18n.baseText('auth.roles.member'),
|
||||||
|
description: i18n.baseText('settings.personal.role.tooltip.member'),
|
||||||
|
},
|
||||||
|
[ROLE.Admin]: {
|
||||||
|
name: i18n.baseText('auth.roles.admin'),
|
||||||
|
description: i18n.baseText('settings.personal.role.tooltip.admin'),
|
||||||
|
},
|
||||||
|
[ROLE.Owner]: {
|
||||||
|
name: i18n.baseText('auth.roles.owner'),
|
||||||
|
description: i18n.baseText('settings.personal.role.tooltip.owner', {
|
||||||
|
interpolate: {
|
||||||
|
cloudAccess: cloudPlanStore.hasCloudPlan
|
||||||
|
? i18n.baseText('settings.personal.role.tooltip.cloud')
|
||||||
|
: '',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const currentUserRole = computed<RoleContent>(() => roles.value[usersStore.globalRoleName]);
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
documentTitle.set(i18n.baseText('settings.personal.personalSettings'));
|
documentTitle.set(i18n.baseText('settings.personal.personalSettings'));
|
||||||
formInputs.value = [
|
formInputs.value = [
|
||||||
@@ -260,7 +295,13 @@ onBeforeUnmount(() => {
|
|||||||
}}</n8n-heading>
|
}}</n8n-heading>
|
||||||
<div v-if="currentUser" :class="$style.user">
|
<div v-if="currentUser" :class="$style.user">
|
||||||
<span :class="$style.username" data-test-id="current-user-name">
|
<span :class="$style.username" data-test-id="current-user-name">
|
||||||
<n8n-text color="text-light">{{ currentUser.fullName }}</n8n-text>
|
<n8n-text color="text-base" bold>{{ currentUser.fullName }}</n8n-text>
|
||||||
|
<N8nTooltip placement="bottom">
|
||||||
|
<template #content>{{ currentUserRole.description }}</template>
|
||||||
|
<n8n-text :class="$style.tooltip" color="text-light" data-test-id="current-user-role">{{
|
||||||
|
currentUserRole.name
|
||||||
|
}}</n8n-text>
|
||||||
|
</N8nTooltip>
|
||||||
</span>
|
</span>
|
||||||
<n8n-avatar
|
<n8n-avatar
|
||||||
:first-name="currentUser.firstName"
|
:first-name="currentUser.firstName"
|
||||||
@@ -395,8 +436,9 @@ onBeforeUnmount(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.username {
|
.username {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr;
|
||||||
margin-right: var(--spacing-s);
|
margin-right: var(--spacing-s);
|
||||||
text-align: right;
|
|
||||||
|
|
||||||
@media (max-width: $breakpoint-sm) {
|
@media (max-width: $breakpoint-sm) {
|
||||||
max-width: 100px;
|
max-width: 100px;
|
||||||
@@ -405,6 +447,10 @@ onBeforeUnmount(() => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tooltip {
|
||||||
|
justify-self: start;
|
||||||
|
}
|
||||||
|
|
||||||
.disableMfaButton {
|
.disableMfaButton {
|
||||||
--button-color: var(--color-danger);
|
--button-color: var(--color-danger);
|
||||||
> span {
|
> span {
|
||||||
|
|||||||
Reference in New Issue
Block a user