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: {
limit: 0,
},
banners: {
v1: {
dismissed: false,
},
},
};
}
@ -411,6 +416,16 @@ export class Server extends AbstractServer {
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
Object.assign(this.frontendSettings.enterprise, {
sharing: isSharingEnabled(),

View file

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

View file

@ -148,4 +148,11 @@ export class OwnerController {
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 { DataSource, Repository } from 'typeorm';
import { Settings } from '../entities/Settings';
import config from '@/config';
@Service()
export class SettingsRepository extends Repository<Settings> {
constructor(dataSource: DataSource) {
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: {
type: Boolean,
},
overrideIcon: {
type: Boolean,
default: false,
},
},
computed: {
classes(): string[] {
@ -61,6 +65,8 @@ export default defineComponent({
];
},
getIcon(): string {
if (this.overrideIcon) return this.icon;
if (Object.keys(CALLOUT_DEFAULT_ICONS).includes(this.theme)) {
return CALLOUT_DEFAULT_ICONS[this.theme];
}

View file

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

View file

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

View file

@ -56,7 +56,6 @@ const defaultSettings: IN8nUISettings = {
urlBaseEditor: '',
urlBaseWebhook: '',
userManagement: {
enabled: true,
showSetupOnFirstLoad: true,
smtpSetup: true,
authenticationMethod: 'email',
@ -75,6 +74,11 @@ const defaultSettings: IN8nUISettings = {
deployment: {
type: 'default',
},
banners: {
v1: {
dismissed: false,
},
},
};
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 { createPinia, PiniaVuePlugin } from 'pinia';
import { useWebhooksStore } from '@/stores';
import { useWebhooksStore, useUIStore } from '@/stores';
Vue.config.productionTip = false;
@ -46,6 +46,8 @@ new Vue({
}).$mount('#app');
router.afterEach((to, from) => {
useUIStore().restoreBanner('v1');
void runExternalHook('main.routeChange', useWebhooksStore(), { from, to });
});

View file

@ -109,6 +109,9 @@
"auth.signup.setupYourAccount": "Set up your account",
"auth.signup.setupYourAccountError": "Problem setting up your account",
"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.backToOverviewPage": "Back to overview page",
"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',
],
};
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,
faGem,
} from '@fortawesome/free-solid-svg-icons';
import { faVariable } from './custom';
import { faVariable, faXmark } from './custom';
import { faStickyNote } from '@fortawesome/free-regular-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
@ -277,6 +277,7 @@ export const FontAwesomePlugin: PluginObject<{}> = {
addIcon(faTree);
addIcon(faUserLock);
addIcon(faGem);
addIcon(faXmark);
app.component('font-awesome-icon', FontAwesomeIcon);
},

View file

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

View file

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

View file

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