feat(editor): Add v1 banner (#6443)

This commit is contained in:
Iván Ovejero 2023-06-19 16:23:57 +02:00 committed by कारतोफ्फेलस्क्रिप्ट™
parent 85372aabdf
commit 0fe415add2
17 changed files with 198 additions and 5 deletions

View file

@ -316,6 +316,11 @@ export class Server extends AbstractServer {
variables: { variables: {
limit: 0, limit: 0,
}, },
banners: {
v1: {
dismissed: false,
},
},
}; };
} }
@ -411,6 +416,16 @@ export class Server extends AbstractServer {
config.getEnv('deployment.type').startsWith('desktop_') === false, config.getEnv('deployment.type').startsWith('desktop_') === false,
}); });
let v1Dismissed = false;
try {
v1Dismissed = config.getEnv('ui.banners.v1.dismissed');
} catch {
// not yet in DB
}
this.frontendSettings.banners.v1.dismissed = v1Dismissed;
// refresh enterprise status // refresh enterprise status
Object.assign(this.frontendSettings.enterprise, { Object.assign(this.frontendSettings.enterprise, {
sharing: isSharingEnabled(), sharing: isSharingEnabled(),

View file

@ -80,6 +80,7 @@ type ExceptionPaths = {
'nodes.exclude': string[] | undefined; 'nodes.exclude': string[] | undefined;
'nodes.include': string[] | undefined; 'nodes.include': string[] | undefined;
'userManagement.isInstanceOwnerSetUp': boolean; 'userManagement.isInstanceOwnerSetUp': boolean;
'ui.banners.v1.dismissed': boolean;
}; };
// ----------------------------------- // -----------------------------------

View file

@ -148,4 +148,11 @@ export class OwnerController {
return sanitizeUser(owner); return sanitizeUser(owner);
} }
@Post('/dismiss-v1')
async dismissBanner() {
await this.settingsRepository.saveSetting('ui.banners.v1.dismissed', JSON.stringify(true));
return { success: true };
}
} }

View file

@ -1,10 +1,23 @@
import { Service } from 'typedi'; import { Service } from 'typedi';
import { DataSource, Repository } from 'typeorm'; import { DataSource, Repository } from 'typeorm';
import { Settings } from '../entities/Settings'; import { Settings } from '../entities/Settings';
import config from '@/config';
@Service() @Service()
export class SettingsRepository extends Repository<Settings> { export class SettingsRepository extends Repository<Settings> {
constructor(dataSource: DataSource) { constructor(dataSource: DataSource) {
super(Settings, dataSource.manager); super(Settings, dataSource.manager);
} }
async saveSetting(key: string, value: string, loadOnStartup = true) {
const setting = await this.findOneBy({ key });
if (setting) {
await this.update({ key }, { value, loadOnStartup });
} else {
await this.save({ key, value, loadOnStartup });
}
if (loadOnStartup) config.set('ui.banners.v1.dismissed', true);
}
} }

View file

@ -50,6 +50,10 @@ export default defineComponent({
slim: { slim: {
type: Boolean, type: Boolean,
}, },
overrideIcon: {
type: Boolean,
default: false,
},
}, },
computed: { computed: {
classes(): string[] { classes(): string[] {
@ -61,6 +65,8 @@ export default defineComponent({
]; ];
}, },
getIcon(): string { getIcon(): string {
if (this.overrideIcon) return this.icon;
if (Object.keys(CALLOUT_DEFAULT_ICONS).includes(this.theme)) { if (Object.keys(CALLOUT_DEFAULT_ICONS).includes(this.theme)) {
return CALLOUT_DEFAULT_ICONS[this.theme]; return CALLOUT_DEFAULT_ICONS[this.theme];
} }

View file

@ -9,6 +9,7 @@
[$style.sidebarCollapsed]: uiStore.sidebarMenuCollapsed, [$style.sidebarCollapsed]: uiStore.sidebarMenuCollapsed,
}" }"
> >
<V1Banner />
<div id="header" :class="$style.header"> <div id="header" :class="$style.header">
<router-view name="header"></router-view> <router-view name="header"></router-view>
</div> </div>
@ -30,6 +31,7 @@
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import { mapStores } from 'pinia'; import { mapStores } from 'pinia';
import V1Banner from '@/components/V1Banner.vue';
import Modals from '@/components/Modals.vue'; import Modals from '@/components/Modals.vue';
import LoadingView from '@/views/LoadingView.vue'; import LoadingView from '@/views/LoadingView.vue';
import Telemetry from '@/components/Telemetry.vue'; import Telemetry from '@/components/Telemetry.vue';
@ -60,6 +62,7 @@ export default defineComponent({
LoadingView, LoadingView,
Telemetry, Telemetry,
Modals, Modals,
V1Banner,
}, },
mixins: [newVersions, userHelpers], mixins: [newVersions, userHelpers],
setup(props) { setup(props) {
@ -278,12 +281,12 @@ export default defineComponent({
.header { .header {
grid-area: header; grid-area: header;
z-index: 999; z-index: 99;
} }
.sidebar { .sidebar {
grid-area: sidebar; grid-area: sidebar;
height: 100vh; height: 100vh;
z-index: 999; z-index: 99;
} }
</style> </style>

View file

@ -1044,6 +1044,12 @@ export interface UIState {
activeActions: string[]; activeActions: string[];
activeCredentialType: string | null; activeCredentialType: string | null;
sidebarMenuCollapsed: boolean; sidebarMenuCollapsed: boolean;
banners: {
v1: {
dismissed: boolean;
mode: 'temporary' | 'permanent';
};
};
modalStack: string[]; modalStack: string[];
modals: Modals; modals: Modals;
isPageLoading: boolean; isPageLoading: boolean;

View file

@ -56,7 +56,6 @@ const defaultSettings: IN8nUISettings = {
urlBaseEditor: '', urlBaseEditor: '',
urlBaseWebhook: '', urlBaseWebhook: '',
userManagement: { userManagement: {
enabled: true,
showSetupOnFirstLoad: true, showSetupOnFirstLoad: true,
smtpSetup: true, smtpSetup: true,
authenticationMethod: 'email', authenticationMethod: 'email',
@ -75,6 +74,11 @@ const defaultSettings: IN8nUISettings = {
deployment: { deployment: {
type: 'default', type: 'default',
}, },
banners: {
v1: {
dismissed: false,
},
},
}; };
export function routesForSettings(server: Server) { export function routesForSettings(server: Server) {

View file

@ -0,0 +1,6 @@
import type { IRestApiContext } from '@/Interface';
import { makeRestApiRequest } from '@/utils/apiUtils';
export async function dismissV1BannerPermanently(context: IRestApiContext): Promise<void> {
return makeRestApiRequest(context, 'POST', '/owner/dismiss-v1');
}

View file

@ -0,0 +1,76 @@
<template>
<n8n-callout
v-if="shouldDisplay"
theme="warning"
icon="info-circle"
override-icon
:class="$style['v1-banner']"
>
<span v-html="locale.baseText('banners.v1.message')"></span>
{{ '' }}
<a v-if="isInstanceOwner" @click="dismissBanner('v1', 'permanent')">
<span v-html="locale.baseText('banners.v1.action')"></span>
</a>
<template #trailingContent>
<n8n-icon
size="small"
icon="xmark"
:title="locale.baseText('banners.v1.iconTitle')"
:class="$style.xmark"
@click="dismissBanner('v1', 'temporary')"
/>
</template>
</n8n-callout>
</template>
<script setup lang="ts">
import { VIEWS } from '@/constants';
import { computed } from 'vue';
import { useUIStore, useUsersStore, useRootStore } from '@/stores';
import { useRoute } from 'vue-router/composables';
import { i18n as locale } from '@/plugins/i18n';
const { isInstanceOwner } = useUsersStore();
const { dismissBanner } = useUIStore();
const shouldDisplay = computed(() => {
if (!useRootStore().versionCli.startsWith('1.')) return false;
if (useUIStore().banners.v1.dismissed) return false;
const VIEWABLE_AT: string[] = [
VIEWS.HOMEPAGE,
VIEWS.COLLECTION,
VIEWS.TEMPLATE,
VIEWS.TEMPLATES,
VIEWS.CREDENTIALS,
VIEWS.VARIABLES,
VIEWS.WORKFLOWS,
VIEWS.EXECUTIONS,
];
const { name } = useRoute();
if (name && VIEWABLE_AT.includes(name)) return true;
return false;
});
</script>
<style module lang="scss">
.v1-banner {
position: absolute;
top: 0;
left: 0;
width: 100%;
z-index: 999;
a {
text-decoration: underline;
}
.xmark {
cursor: pointer;
}
}
</style>

View file

@ -24,7 +24,7 @@ import { FontAwesomePlugin } from './plugins/icons';
import { runExternalHook } from '@/utils'; import { runExternalHook } from '@/utils';
import { createPinia, PiniaVuePlugin } from 'pinia'; import { createPinia, PiniaVuePlugin } from 'pinia';
import { useWebhooksStore } from '@/stores'; import { useWebhooksStore, useUIStore } from '@/stores';
Vue.config.productionTip = false; Vue.config.productionTip = false;
@ -46,6 +46,8 @@ new Vue({
}).$mount('#app'); }).$mount('#app');
router.afterEach((to, from) => { router.afterEach((to, from) => {
useUIStore().restoreBanner('v1');
void runExternalHook('main.routeChange', useWebhooksStore(), { from, to }); void runExternalHook('main.routeChange', useWebhooksStore(), { from, to });
}); });

View file

@ -109,6 +109,9 @@
"auth.signup.setupYourAccount": "Set up your account", "auth.signup.setupYourAccount": "Set up your account",
"auth.signup.setupYourAccountError": "Problem setting up your account", "auth.signup.setupYourAccountError": "Problem setting up your account",
"auth.signup.tokenValidationError": "Issue validating invite token", "auth.signup.tokenValidationError": "Issue validating invite token",
"banners.v1.message": "n8n has been updated to version 1, introducing some breaking changes. Please consult the <a target=\"_blank\" href=\"https://docs.n8n.io/1-0-migration-checklist\">migration guide</a> for more information.",
"banners.v1.action": "Confirm",
"banners.v1.iconTitle": "Dismiss v1 banner",
"binaryDataDisplay.backToList": "Back to list", "binaryDataDisplay.backToList": "Back to list",
"binaryDataDisplay.backToOverviewPage": "Back to overview page", "binaryDataDisplay.backToOverviewPage": "Back to overview page",
"binaryDataDisplay.noDataFoundToDisplay": "No data found to display", "binaryDataDisplay.noDataFoundToDisplay": "No data found to display",

View file

@ -11,3 +11,15 @@ export const faVariable: IconDefinition = {
'M42.6,17.8c2.4,0,7.2-2,7.2-8.4c0-6.4-4.6-6.8-6.1-6.8c-2.8,0-5.6,2-8.1,6.3c-2.5,4.4-5.3,9.1-5.3,9.1 l-0.1,0c-0.6-3.1-1.1-5.6-1.3-6.7c-0.5-2.7-3.6-8.4-9.9-8.4c-6.4,0-12.2,3.7-12.2,3.7l0,0C5.8,7.3,5.1,8.5,5.1,9.9 c0,2.1,1.7,3.9,3.9,3.9c0.6,0,1.2-0.2,1.7-0.4l0,0c0,0,4.8-2.7,5.9,0c0.3,0.8,0.6,1.7,0.9,2.7c1.2,4.2,2.4,9.1,3.3,13.5l-4.2,6 c0,0-4.7-1.7-7.1-1.7s-7.2,2-7.2,8.4s4.6,6.8,6.1,6.8c2.8,0,5.6-2,8.1-6.3c2.5-4.4,5.3-9.1,5.3-9.1c0.8,4,1.5,7.1,1.9,8.5 c1.6,4.5,5.3,7.2,10.1,7.2c0,0,5,0,10.9-3.3c1.4-0.6,2.4-2,2.4-3.6c0-2.1-1.7-3.9-3.9-3.9c-0.6,0-1.2,0.2-1.7,0.4l0,0 c0,0-4.2,2.4-5.6,0.5c-1-2-1.9-4.6-2.6-7.8c-0.6-2.8-1.3-6.2-2-9.5l4.3-6.2C35.5,16.1,40.2,17.8,42.6,17.8z', 'M42.6,17.8c2.4,0,7.2-2,7.2-8.4c0-6.4-4.6-6.8-6.1-6.8c-2.8,0-5.6,2-8.1,6.3c-2.5,4.4-5.3,9.1-5.3,9.1 l-0.1,0c-0.6-3.1-1.1-5.6-1.3-6.7c-0.5-2.7-3.6-8.4-9.9-8.4c-6.4,0-12.2,3.7-12.2,3.7l0,0C5.8,7.3,5.1,8.5,5.1,9.9 c0,2.1,1.7,3.9,3.9,3.9c0.6,0,1.2-0.2,1.7-0.4l0,0c0,0,4.8-2.7,5.9,0c0.3,0.8,0.6,1.7,0.9,2.7c1.2,4.2,2.4,9.1,3.3,13.5l-4.2,6 c0,0-4.7-1.7-7.1-1.7s-7.2,2-7.2,8.4s4.6,6.8,6.1,6.8c2.8,0,5.6-2,8.1-6.3c2.5-4.4,5.3-9.1,5.3-9.1c0.8,4,1.5,7.1,1.9,8.5 c1.6,4.5,5.3,7.2,10.1,7.2c0,0,5,0,10.9-3.3c1.4-0.6,2.4-2,2.4-3.6c0-2.1-1.7-3.9-3.9-3.9c-0.6,0-1.2,0.2-1.7,0.4l0,0 c0,0-4.2,2.4-5.6,0.5c-1-2-1.9-4.6-2.6-7.8c-0.6-2.8-1.3-6.2-2-9.5l4.3-6.2C35.5,16.1,40.2,17.8,42.6,17.8z',
], ],
}; };
export const faXmark: IconDefinition = {
prefix: 'fas' as IconPrefix,
iconName: 'xmark' as IconName,
icon: [
400,
400,
[],
'',
'M342.6 150.6c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0L192 210.7 86.6 105.4c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3L146.7 256 41.4 361.4c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0L192 301.3 297.4 406.6c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3L237.3 256 342.6 150.6z',
],
};

View file

@ -134,7 +134,7 @@ import {
faUserLock, faUserLock,
faGem, faGem,
} from '@fortawesome/free-solid-svg-icons'; } from '@fortawesome/free-solid-svg-icons';
import { faVariable } from './custom'; import { faVariable, faXmark } from './custom';
import { faStickyNote } from '@fortawesome/free-regular-svg-icons'; import { faStickyNote } from '@fortawesome/free-regular-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
@ -277,6 +277,7 @@ export const FontAwesomePlugin: PluginObject<{}> = {
addIcon(faTree); addIcon(faTree);
addIcon(faUserLock); addIcon(faUserLock);
addIcon(faGem); addIcon(faGem);
addIcon(faXmark);
app.component('font-awesome-icon', FontAwesomeIcon); app.component('font-awesome-icon', FontAwesomeIcon);
}, },

View file

@ -212,6 +212,10 @@ export const useSettingsStore = defineStore(STORES.SETTINGS, {
rootStore.setN8nMetadata(settings.n8nMetadata || {}); rootStore.setN8nMetadata(settings.n8nMetadata || {});
rootStore.setDefaultLocale(settings.defaultLocale); rootStore.setDefaultLocale(settings.defaultLocale);
rootStore.setIsNpmAvailable(settings.isNpmAvailable); rootStore.setIsNpmAvailable(settings.isNpmAvailable);
if (settings.banners.v1.dismissed) {
useUIStore().setBanners({ v1: { dismissed: true, mode: 'permanent' } });
}
useVersionsStore().setVersionNotificationSettings(settings.versionNotifications); useVersionsStore().setVersionNotificationSettings(settings.versionNotifications);
}, },
stopShowingSetupPage(): void { stopShowingSetupPage(): void {

View file

@ -52,6 +52,7 @@ import type { BaseTextKey } from '@/plugins/i18n';
import { i18n as locale } from '@/plugins/i18n'; import { i18n as locale } from '@/plugins/i18n';
import type { Modals, NewCredentialsModal } from '@/Interface'; import type { Modals, NewCredentialsModal } from '@/Interface';
import { useTelemetryStore } from '@/stores/telemetry.store'; import { useTelemetryStore } from '@/stores/telemetry.store';
import { dismissV1BannerPermanently } from '@/api/ui';
export const useUIStore = defineStore(STORES.UI, { export const useUIStore = defineStore(STORES.UI, {
state: (): UIState => ({ state: (): UIState => ({
@ -139,6 +140,12 @@ export const useUIStore = defineStore(STORES.UI, {
}, },
modalStack: [], modalStack: [],
sidebarMenuCollapsed: true, sidebarMenuCollapsed: true,
banners: {
v1: {
dismissed: false,
mode: 'temporary',
},
},
isPageLoading: true, isPageLoading: true,
currentView: '', currentView: '',
mainPanelPosition: 0.5, mainPanelPosition: 0.5,
@ -332,6 +339,12 @@ export const useUIStore = defineStore(STORES.UI, {
}, },
}, },
actions: { actions: {
setBanners(banners: UIState['banners']): void {
this.banners = {
...this.banners,
...banners,
};
},
setMode(name: keyof Modals, mode: string): void { setMode(name: keyof Modals, mode: string): void {
this.modals[name] = { this.modals[name] = {
...this.modals[name], ...this.modals[name],
@ -508,6 +521,22 @@ export const useUIStore = defineStore(STORES.UI, {
toggleSidebarMenuCollapse(): void { toggleSidebarMenuCollapse(): void {
this.sidebarMenuCollapsed = !this.sidebarMenuCollapsed; this.sidebarMenuCollapsed = !this.sidebarMenuCollapsed;
}, },
async dismissBanner(bannerType: 'v1', mode: 'temporary' | 'permanent'): Promise<void> {
if (mode === 'permanent') {
await dismissV1BannerPermanently(useRootStore().getRestApiContext);
this.banners[bannerType].dismissed = true;
this.banners[bannerType].mode = 'permanent';
return;
}
this.banners[bannerType].dismissed = true;
this.banners[bannerType].mode = 'temporary';
},
restoreBanner(bannerType: 'v1'): void {
if (this.banners[bannerType].dismissed && this.banners[bannerType].mode === 'temporary') {
this.banners[bannerType].dismissed = false;
}
},
async getCurlToJson(curlCommand: string): Promise<CurlToJSONResponse> { async getCurlToJson(curlCommand: string): Promise<CurlToJSONResponse> {
const rootStore = useRootStore(); const rootStore = useRootStore();
return getCurlToJson(rootStore.getRestApiContext, curlCommand); return getCurlToJson(rootStore.getRestApiContext, curlCommand);

View file

@ -2146,4 +2146,9 @@ export interface IN8nUISettings {
variables: { variables: {
limit: number; limit: number;
}; };
banners: {
v1: {
dismissed: boolean;
};
};
} }