From 5e3e70b83bf905c39f23a213f953bc42d7eed357 Mon Sep 17 00:00:00 2001 From: Omar Ajoue Date: Wed, 8 Feb 2023 10:42:22 +0100 Subject: [PATCH] feat: Change desktop UM experience (#5312) * refactor: Hide prompt for desktop * feat: add email field to personalization modal * fix: update survey interfaces * chore: enable personalization survey email key display condition * feat: add users page upsell for desktop client * feat: disable UM on desktop where possible * refactor: Have a single function to decide whether UM is enabled * feat: update community nodes upsell link --------- Co-authored-by: Alex Grozav Co-authored-by: krynble Co-authored-by: freyamade --- packages/cli/src/Interfaces.ts | 1 + packages/cli/src/Server.ts | 3 +- .../UserManagement/UserManagementHelper.ts | 25 +++++---- .../cli/src/controllers/users.controller.ts | 4 +- packages/cli/src/middlewares/auth.ts | 4 +- .../src/components/N8nActionBox/ActionBox.vue | 1 + packages/editor-ui/src/Interface.ts | 1 + .../CredentialEdit/CredentialSharing.ee.vue | 10 +++- .../src/components/FeatureComingSoon.vue | 2 +- .../components/MainHeader/WorkflowDetails.vue | 10 +++- .../src/components/PersonalizationModal.vue | 11 ++++ .../src/components/WorkflowShareModal.ee.vue | 28 ++++------ packages/editor-ui/src/constants.ts | 1 + .../src/plugins/i18n/locales/en.json | 23 ++++++-- packages/editor-ui/src/router.ts | 7 ++- packages/editor-ui/src/stores/ui.ts | 9 +++ packages/editor-ui/src/stores/users.ts | 11 +++- .../src/views/SettingsCommunityNodesView.vue | 55 +++++++++++++------ .../editor-ui/src/views/SettingsUsersView.vue | 35 +++++++++++- 19 files changed, 177 insertions(+), 64 deletions(-) diff --git a/packages/cli/src/Interfaces.ts b/packages/cli/src/Interfaces.ts index c0ad0d33ee..3044528ec5 100644 --- a/packages/cli/src/Interfaces.ts +++ b/packages/cli/src/Interfaces.ts @@ -571,6 +571,7 @@ export interface IN8nUISettings { } export interface IPersonalizationSurveyAnswers { + email: string | null; codingSkill: string | null; companyIndustry: string[]; companySize: string | null; diff --git a/packages/cli/src/Server.ts b/packages/cli/src/Server.ts index f9382fba1a..55938909a2 100644 --- a/packages/cli/src/Server.ts +++ b/packages/cli/src/Server.ts @@ -303,7 +303,8 @@ class Server extends AbstractServer { showSetupOnFirstLoad: config.getEnv('userManagement.disabled') === false && config.getEnv('userManagement.isInstanceOwnerSetUp') === false && - config.getEnv('userManagement.skipInstanceOwnerSetup') === false, + config.getEnv('userManagement.skipInstanceOwnerSetup') === false && + config.getEnv('deployment.type').startsWith('desktop_') === false, }); // refresh enterprise status diff --git a/packages/cli/src/UserManagement/UserManagementHelper.ts b/packages/cli/src/UserManagement/UserManagementHelper.ts index 3660ed1374..a38a6ed310 100644 --- a/packages/cli/src/UserManagement/UserManagementHelper.ts +++ b/packages/cli/src/UserManagement/UserManagementHelper.ts @@ -37,10 +37,20 @@ export function isEmailSetUp(): boolean { } export function isUserManagementEnabled(): boolean { - return ( - !config.getEnv('userManagement.disabled') || - config.getEnv('userManagement.isInstanceOwnerSetUp') - ); + // This can be simplified but readability is more important here + + if (config.getEnv('userManagement.isInstanceOwnerSetUp')) { + // Short circuit - if owner is set up, UM cannot be disabled. + // Users must reset their instance in order to do so. + return true; + } + + // UM is disabled for desktop by default + if (config.getEnv('deployment.type').startsWith('desktop_')) { + return false; + } + + return config.getEnv('userManagement.disabled') ? false : true; } export function isSharingEnabled(): boolean { @@ -51,13 +61,6 @@ export function isSharingEnabled(): boolean { ); } -export function isUserManagementDisabled(): boolean { - return ( - config.getEnv('userManagement.disabled') && - !config.getEnv('userManagement.isInstanceOwnerSetUp') - ); -} - export async function getRoleId(scope: Role['scope'], name: Role['name']): Promise { return Db.collections.Role.findOneOrFail({ select: ['id'], diff --git a/packages/cli/src/controllers/users.controller.ts b/packages/cli/src/controllers/users.controller.ts index 19893dd59e..a71682ffb4 100644 --- a/packages/cli/src/controllers/users.controller.ts +++ b/packages/cli/src/controllers/users.controller.ts @@ -13,7 +13,7 @@ import { getInstanceBaseUrl, hashPassword, isEmailSetUp, - isUserManagementDisabled, + isUserManagementEnabled, sanitizeUser, validatePassword, } from '@/UserManagement/UserManagementHelper'; @@ -94,7 +94,7 @@ export class UsersController { @Post('/') async sendEmailInvites(req: UserRequest.Invite) { // TODO: this should be checked in the middleware rather than here - if (isUserManagementDisabled()) { + if (!isUserManagementEnabled()) { this.logger.debug( 'Request to send email invite(s) to user(s) failed because user management is disabled', ); diff --git a/packages/cli/src/middlewares/auth.ts b/packages/cli/src/middlewares/auth.ts index 4cff1e84b2..a0c13d4f21 100644 --- a/packages/cli/src/middlewares/auth.ts +++ b/packages/cli/src/middlewares/auth.ts @@ -13,7 +13,7 @@ import { isAuthenticatedRequest, isAuthExcluded, isPostUsersId, - isUserManagementDisabled, + isUserManagementEnabled, } from '@/UserManagement/UserManagementHelper'; import type { Repository } from 'typeorm'; import type { User } from '@db/entities/User'; @@ -101,7 +101,7 @@ export const setupAuthMiddlewares = ( } // skip authentication if user management is disabled - if (isUserManagementDisabled()) { + if (!isUserManagementEnabled()) { req.user = await userRepository.findOneOrFail({ relations: ['globalRole'], where: {}, diff --git a/packages/design-system/src/components/N8nActionBox/ActionBox.vue b/packages/design-system/src/components/N8nActionBox/ActionBox.vue index aa5004e132..57b626ba20 100644 --- a/packages/design-system/src/components/N8nActionBox/ActionBox.vue +++ b/packages/design-system/src/components/N8nActionBox/ActionBox.vue @@ -106,6 +106,7 @@ export default Vue.extend({ .heading { margin-bottom: var(--spacing-l); + text-align: center; } .description { diff --git a/packages/editor-ui/src/Interface.ts b/packages/editor-ui/src/Interface.ts index fdded1ad3a..d3dad5022e 100644 --- a/packages/editor-ui/src/Interface.ts +++ b/packages/editor-ui/src/Interface.ts @@ -509,6 +509,7 @@ export type IPersonalizationSurveyAnswersV3 = { automationGoalSm?: string[] | null; automationGoalSmOther?: string | null; usageModes?: string[] | null; + email?: string | null; }; export type IPersonalizationLatestVersion = IPersonalizationSurveyAnswersV3; diff --git a/packages/editor-ui/src/components/CredentialEdit/CredentialSharing.ee.vue b/packages/editor-ui/src/components/CredentialEdit/CredentialSharing.ee.vue index 181ecec202..2012311bdc 100644 --- a/packages/editor-ui/src/components/CredentialEdit/CredentialSharing.ee.vue +++ b/packages/editor-ui/src/components/CredentialEdit/CredentialSharing.ee.vue @@ -89,6 +89,7 @@ import { useUIStore } from '@/stores/ui'; import { useCredentialsStore } from '@/stores/credentials'; import { useUsageStore } from '@/stores/usage'; import { EnterpriseEditionFeature, VIEWS } from '@/constants'; +import { BaseTextKey } from '@/plugins/i18n'; export default mixins(showMessage).extend({ name: 'CredentialSharing', @@ -179,9 +180,14 @@ export default mixins(showMessage).extend({ this.modalBus.$emit('close'); }, goToUpgrade() { - let linkUrl = this.$locale.baseText(this.uiStore.contextBasedTranslationKeys.upgradeLinkUrl); - if (linkUrl.includes('subscription')) { + const linkUrlTranslationKey = this.uiStore.contextBasedTranslationKeys + .upgradeLinkUrl as BaseTextKey; + let linkUrl = this.$locale.baseText(linkUrlTranslationKey); + + if (linkUrlTranslationKey.endsWith('.upgradeLinkUrl')) { linkUrl = `${this.usageStore.viewPlansUrl}&source=credential_sharing`; + } else if (linkUrlTranslationKey.endsWith('.desktop')) { + linkUrl = `${linkUrl}&utm_campaign=upgrade-credentials-sharing`; } window.open(linkUrl, '_blank'); diff --git a/packages/editor-ui/src/components/FeatureComingSoon.vue b/packages/editor-ui/src/components/FeatureComingSoon.vue index 9c7624d953..b14c895535 100644 --- a/packages/editor-ui/src/components/FeatureComingSoon.vue +++ b/packages/editor-ui/src/components/FeatureComingSoon.vue @@ -1,5 +1,5 @@ @@ -198,6 +186,11 @@ export default mixins(showMessage).extend({ isSharingEnabled(): boolean { return this.settingsStore.isEnterpriseFeatureEnabled(EnterpriseEditionFeature.Sharing); }, + fallbackLinkUrl(): string { + return `${this.$locale.baseText( + this.uiStore.contextBasedTranslationKeys.upgradeLinkUrl as BaseTextKey, + )}${true ? '&utm_campaign=upgrade-workflow-sharing' : ''}`; + }, modalTitle(): string { return this.$locale.baseText( this.isSharingEnabled @@ -444,11 +437,14 @@ export default mixins(showMessage).extend({ }); }, goToUpgrade() { - let linkUrl = this.$locale.baseText( - this.uiStore.contextBasedTranslationKeys.upgradeLinkUrl as BaseTextKey, - ); - if (linkUrl.includes('subscription')) { + const linkUrlTranslationKey = this.uiStore.contextBasedTranslationKeys + .upgradeLinkUrl as BaseTextKey; + let linkUrl = this.$locale.baseText(linkUrlTranslationKey); + + if (linkUrlTranslationKey.endsWith('.upgradeLinkUrl')) { linkUrl = `${this.usageStore.viewPlansUrl}&source=workflow_sharing`; + } else if (linkUrlTranslationKey.endsWith('.desktop')) { + linkUrl = `${linkUrl}&utm_campaign=upgrade-workflow-sharing`; } window.open(linkUrl, '_blank'); diff --git a/packages/editor-ui/src/constants.ts b/packages/editor-ui/src/constants.ts index 83f51cf414..57ef72952e 100644 --- a/packages/editor-ui/src/constants.ts +++ b/packages/editor-ui/src/constants.ts @@ -176,6 +176,7 @@ export const INSTANCE_ID_HEADER = 'n8n-instance-id'; export const WAIT_TIME_UNLIMITED = '3000-01-01T00:00:00.000Z'; /** PERSONALIZATION SURVEY */ +export const EMAIL_KEY = 'email'; export const WORK_AREA_KEY = 'workArea'; export const FINANCE_WORK_AREA = 'finance'; export const IT_ENGINEERING_WORK_AREA = 'IT-Engineering'; diff --git a/packages/editor-ui/src/plugins/i18n/locales/en.json b/packages/editor-ui/src/plugins/i18n/locales/en.json index c3154dfa56..a9645dc898 100644 --- a/packages/editor-ui/src/plugins/i18n/locales/en.json +++ b/packages/editor-ui/src/plugins/i18n/locales/en.json @@ -513,10 +513,6 @@ "fakeDoor.settings.sso.actionBox.title": "We’re working on this (as a paid feature)", "fakeDoor.settings.sso.actionBox.title.cloud": "We’re working on this", "fakeDoor.settings.sso.actionBox.description": "SSO will offer a secured and convenient way to access n8n using your existing credentials (Google, Github, Keycloak…)", - "fakeDoor.settings.users.name": "Users", - "fakeDoor.settings.users.actionBox.title": "Upgrade to add users", - "fakeDoor.settings.users.actionBox.description": "Create multiple users on our higher plans and share workflows and credentials to collaborate", - "fakeDoor.settings.users.actionBox.button": "Upgrade now", "fakeDoor.actionBox.button.label": "Join the list", "fixedCollectionParameter.choose": "Choose...", "fixedCollectionParameter.currentlyNoItemsExist": "Currently no items exist", @@ -949,6 +945,8 @@ "personalizationModal.leadGeneration": "Lead generation, enrichment, routing", "personalizationModal.customerCommunication": "Customer communication", "personalizationModal.customerActions": "Actions when lead changes status", + "personalizationModal.yourEmailAddress": "Your email address", + "personalizationModal.email": "Enter your email..", "personalizationModal.adCampaign": "Ad campaign management", "personalizationModal.reporting": "Reporting", "personalizationModal.dataSynching": "Data syncing", @@ -1552,9 +1550,24 @@ "contextual.workflows.sharing.unavailable.button": "View plans", "contextual.workflows.sharing.unavailable.button.cloud": "Upgrade now", "contextual.workflows.sharing.unavailable.button.desktop": "View plans", + + "contextual.users.settings.unavailable.title": "Upgrade to add users", + "contextual.users.settings.unavailable.title.cloud": "Upgrade to add users", + "contextual.users.settings.unavailable.title.desktop": "Upgrade to add users", + "contextual.users.settings.unavailable.description": "Create multiple users on our higher plans and share workflows and credentials to collaborate", + "contextual.users.settings.unavailable.description.cloud": "Create multiple users on our higher plans and share workflows and credentials to collaborate", + "contextual.users.settings.unavailable.description.desktop": "Create multiple users on our higher plans and share workflows and credentials to collaborate", + "contextual.users.settings.unavailable.button": "View plans", + "contextual.users.settings.unavailable.button.cloud": "Upgrade now", + "contextual.users.settings.unavailable.button.desktop": "View plans", + + "contextual.communityNodes.unavailable.description.desktop": "Community nodes feature is unavailable on desktop. Please choose one of our available self-hosting plans.", + "contextual.communityNodes.unavailable.button.desktop": "View plans", + "contextual.upgradeLinkUrl": "https://subscription.n8n.io/", "contextual.upgradeLinkUrl.cloud": "https://app.n8n.cloud/manage?edition=cloud", - "contextual.upgradeLinkUrl.desktop": "https://n8n.io/pricing", + "contextual.upgradeLinkUrl.desktop": "https://n8n.io/pricing/?utm_source=n8n-internal&utm_medium=desktop", + "settings.ldap": "LDAP", "settings.ldap.infoTip": "Learn more about LDAP in the Docs", "settings.ldap.save": "Save connection", diff --git a/packages/editor-ui/src/router.ts b/packages/editor-ui/src/router.ts index e5b067fb0f..5e08c994e4 100644 --- a/packages/editor-ui/src/router.ts +++ b/packages/editor-ui/src/router.ts @@ -35,6 +35,7 @@ import { EnterpriseEditionFeature, VIEWS } from './constants'; import { useSettingsStore } from './stores/settings'; import { useTemplatesStore } from './stores/templates'; import SettingsUsageAndPlanVue from './views/SettingsUsageAndPlan.vue'; +import { useUsersStore } from '@/stores/users'; Vue.use(Router); @@ -507,7 +508,11 @@ const router = new Router({ deny: { shouldDeny: () => { const settingsStore = useSettingsStore(); - return settingsStore.isUserManagementEnabled === false; + + return ( + settingsStore.isUserManagementEnabled === false && + !(settingsStore.isCloudDeployment || settingsStore.isDesktopDeployment) + ); }, }, }, diff --git a/packages/editor-ui/src/stores/ui.ts b/packages/editor-ui/src/stores/ui.ts index d209736176..5915acdd43 100644 --- a/packages/editor-ui/src/stores/ui.ts +++ b/packages/editor-ui/src/stores/ui.ts @@ -207,6 +207,15 @@ export const useUIStore = defineStore(STORES.UI, { }, }, }, + users: { + settings: { + unavailable: { + title: `contextual.users.settings.unavailable.title${contextKey}`, + description: `contextual.users.settings.unavailable.description${contextKey}`, + button: `contextual.users.settings.unavailable.button${contextKey}`, + }, + }, + }, }; }, getLastSelectedNode(): INodeUi | null { diff --git a/packages/editor-ui/src/stores/users.ts b/packages/editor-ui/src/stores/users.ts index bead449155..93e8304e0f 100644 --- a/packages/editor-ui/src/stores/users.ts +++ b/packages/editor-ui/src/stores/users.ts @@ -19,11 +19,12 @@ import { validatePasswordToken, validateSignupToken, } from '@/api/users'; -import { EnterpriseEditionFeature, PERSONALIZATION_MODAL_KEY, STORES } from '@/constants'; -import { +import { PERSONALIZATION_MODAL_KEY, STORES } from '@/constants'; +import type { ICredentialsResponse, IInviteResponse, IPersonalizationLatestVersion, + IRole, IUser, IUserResponse, IUsersState, @@ -41,6 +42,9 @@ const isDefaultUser = (user: IUserResponse | null) => const isPendingUser = (user: IUserResponse | null) => Boolean(user && user.isPending); +const isInstanceOwner = (user: IUserResponse | null) => + Boolean(user?.globalRole?.name === ROLE.Owner); + export const useUsersStore = defineStore(STORES.USERS, { state: (): IUsersState => ({ currentUserId: null, @@ -56,6 +60,9 @@ export const useUsersStore = defineStore(STORES.USERS, { isDefaultUser(): boolean { return isDefaultUser(this.currentUser); }, + isInstanceOwner(): boolean { + return isInstanceOwner(this.currentUser); + }, getUserById(state) { return (userId: string): IUser | null => state.users[userId]; }, diff --git a/packages/editor-ui/src/views/SettingsCommunityNodesView.vue b/packages/editor-ui/src/views/SettingsCommunityNodesView.vue index cfa3591b67..d67426dd9b 100644 --- a/packages/editor-ui/src/views/SettingsCommunityNodesView.vue +++ b/packages/editor-ui/src/views/SettingsCommunityNodesView.vue @@ -35,14 +35,10 @@
@@ -70,6 +66,7 @@ import { useCommunityNodesStore } from '@/stores/communityNodes'; import { useUIStore } from '@/stores/ui'; import { mapStores } from 'pinia'; import { useSettingsStore } from '@/stores/settings'; +import { BaseTextKey } from '@/plugins/i18n'; const PACKAGE_COUNT_THRESHOLD = 31; @@ -129,8 +126,13 @@ export default mixins(showMessage).extend({ }, computed: { ...mapStores(useCommunityNodesStore, useSettingsStore, useUIStore), - getEmptyStateDescription() { + getEmptyStateDescription(): string { const packageCount = this.communityNodesStore.availablePackageCount; + + if (this.settingsStore.isDesktopDeployment) { + return this.$locale.baseText('contextual.communityNodes.unavailable.description.desktop'); + } + return packageCount < PACKAGE_COUNT_THRESHOLD ? this.$locale.baseText('settings.communityNodes.empty.description.no-packages', { interpolate: { @@ -144,18 +146,23 @@ export default mixins(showMessage).extend({ }, }); }, - shouldShowInstallButton() { - return !this.settingsStore.isDesktopDeployment && this.settingsStore.isNpmAvailable; - }, - actionBoxConfig() { + getEmptyStateButtonText(): string { if (this.settingsStore.isDesktopDeployment) { - return { - calloutText: this.$locale.baseText('settings.communityNodes.notAvailableOnDesktop'), - calloutTheme: 'warning', - hideButton: true, - }; + return this.$locale.baseText('contextual.communityNodes.unavailable.button.desktop'); } + return this.shouldShowInstallButton + ? this.$locale.baseText('settings.communityNodes.empty.installPackageLabel') + : ''; + }, + shouldShowInstallButton(): boolean { + return this.settingsStore.isDesktopDeployment || this.settingsStore.isNpmAvailable; + }, + actionBoxConfig(): { + calloutText: string; + calloutTheme: 'warning' | string; + hideButton: boolean; + } { if (!this.settingsStore.isNpmAvailable) { return { calloutText: this.$locale.baseText('settings.communityNodes.npmUnavailable.warning', { @@ -184,7 +191,21 @@ export default mixins(showMessage).extend({ }, }, methods: { - openInstallModal(event: MouseEvent) { + onClickEmptyStateButton(): void { + if (this.settingsStore.isDesktopDeployment) { + return this.goToUpgrade(); + } + + this.openInstallModal(); + }, + goToUpgrade(): void { + const linkUrl = `${this.$locale.baseText( + 'contextual.upgradeLinkUrl.desktop', + )}&utm_campaign=upgrade-community-nodes&selfHosted=true`; + + window.open(linkUrl, '_blank'); + }, + openInstallModal(): void { const telemetryPayload = { is_empty_state: this.communityNodesStore.getInstalledPackages.length === 0, }; diff --git a/packages/editor-ui/src/views/SettingsUsersView.vue b/packages/editor-ui/src/views/SettingsUsersView.vue index 354c503d0b..fd91306964 100644 --- a/packages/editor-ui/src/views/SettingsUsersView.vue +++ b/packages/editor-ui/src/views/SettingsUsersView.vue @@ -10,7 +10,23 @@ />
-
+
+ +
+