mirror of
https://github.com/n8n-io/n8n.git
synced 2025-01-11 12:57:29 -08:00
✨ Add new version notification (#1977)
* add menu item
* implement versions modal
* fix up modal
* clean up badges
* implement key features
* fix up spacing
* add error message
* add notification icon
* fix notification
* fix bug when no updates
* address lint issues
* address multi line nodes
* add closing animation
* keep drawer open
* address design feedback
* address badge styling
* use grid for icons
* update cli to return version information
* set env variables
* add scss color variables
* address comments
* fix lint issue
* handle edge cases
* update scss variables, spacing
* update spacing
* build
* override top value for theme
* bolden version
* update config
* check endpoint exists
* handle long names
* set dates
* set title
* fix bug
* update warning
* remove unused component
* refactor components
* add fragments
* inherit styles
* fix icon size
* fix lint issues
* add cli dep
* address comments
* handle network error
* address empty case
* Revert "address comments"
480f969e07
* remove dependency
* build
* update variable names
* update variable names
* refactor verion card
* split out variables
* clean up impl
* clean up scss
* move from nodeview
* refactor out gift notification icon
* fix lint issues
* clean up variables
* update scss variables
* update info url
* Add instanceId to frontendSettings
* Use createHash from crypto module
* Add instanceId to store & send it as http header
* Fix lintings
* Fix interfaces & apply review changes
* Apply review changes
* add console message
* update text info
* update endpoint
* clean up interface
* address comments
* cleanup todo
* Update packages/cli/config/index.ts
Co-authored-by: Ben Hesseldieck <1849459+BHesseldieck@users.noreply.github.com>
* update console message
* ⚡ Display node-name on hover
* ⚡ Formatting fix
Co-authored-by: MedAliMarz <servfrdali@yahoo.fr>
Co-authored-by: Ben Hesseldieck <1849459+BHesseldieck@users.noreply.github.com>
Co-authored-by: Jan Oberhauser <jan.oberhauser@gmail.com>
This commit is contained in:
parent
073e5e24cb
commit
98ec23544b
|
@ -142,6 +142,9 @@ export class Start extends Command {
|
||||||
LoggerProxy.init(logger);
|
LoggerProxy.init(logger);
|
||||||
logger.info('Initializing n8n process');
|
logger.info('Initializing n8n process');
|
||||||
|
|
||||||
|
// todo remove a few versions after release
|
||||||
|
logger.info('\nn8n now checks for new versions and security updates. You can turn this off using the environment variable N8N_VERSION_NOTIFICATIONS_ENABLED to "false"\nFor more information, please refer to https://docs.n8n.io/getting-started/installation/advanced/configuration.html\n');
|
||||||
|
|
||||||
// Start directly with the init of the database to improve startup time
|
// Start directly with the init of the database to improve startup time
|
||||||
const startDbInitPromise = Db.init().catch((error: Error) => {
|
const startDbInitPromise = Db.init().catch((error: Error) => {
|
||||||
logger.error(`There was an error initializing DB: "${error.message}"`);
|
logger.error(`There was an error initializing DB: "${error.message}"`);
|
||||||
|
|
|
@ -618,6 +618,27 @@ const config = convict({
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
versionNotifications: {
|
||||||
|
enabled: {
|
||||||
|
doc: 'Whether feature is enabled to request notifications about new versions and security updates.',
|
||||||
|
format: Boolean,
|
||||||
|
default: true,
|
||||||
|
env: 'N8N_VERSION_NOTIFICATIONS_ENABLED',
|
||||||
|
},
|
||||||
|
endpoint: {
|
||||||
|
doc: 'Endpoint to retrieve version information from.',
|
||||||
|
format: String,
|
||||||
|
default: 'https://api.n8n.io/versions/',
|
||||||
|
env: 'N8N_VERSION_NOTIFICATIONS_ENDPOINT',
|
||||||
|
},
|
||||||
|
infoUrl: {
|
||||||
|
doc: `Url in New Versions Panel with more information on updating one's instance.`,
|
||||||
|
format: String,
|
||||||
|
default: 'https://docs.n8n.io/getting-started/installation/updating.html',
|
||||||
|
env: 'N8N_VERSION_NOTIFICATIONS_INFO_URL',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Overwrite default configuration with settings which got defined in
|
// Overwrite default configuration with settings which got defined in
|
||||||
|
|
|
@ -312,6 +312,11 @@ export interface IN8nConfigNodes {
|
||||||
exclude: string[];
|
exclude: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IVersionNotificationSettings {
|
||||||
|
enabled: boolean;
|
||||||
|
endpoint: string;
|
||||||
|
infoUrl: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface IN8nUISettings {
|
export interface IN8nUISettings {
|
||||||
endpointWebhook: string;
|
endpointWebhook: string;
|
||||||
|
@ -331,6 +336,8 @@ export interface IN8nUISettings {
|
||||||
n8nMetadata?: {
|
n8nMetadata?: {
|
||||||
[key: string]: string | number | undefined;
|
[key: string]: string | number | undefined;
|
||||||
};
|
};
|
||||||
|
versionNotifications: IVersionNotificationSettings;
|
||||||
|
instanceId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IPackageVersions {
|
export interface IPackageVersions {
|
||||||
|
|
|
@ -21,7 +21,7 @@ import * as clientOAuth1 from 'oauth-1.0a';
|
||||||
import { RequestOptions } from 'oauth-1.0a';
|
import { RequestOptions } from 'oauth-1.0a';
|
||||||
import * as csrf from 'csrf';
|
import * as csrf from 'csrf';
|
||||||
import * as requestPromise from 'request-promise-native';
|
import * as requestPromise from 'request-promise-native';
|
||||||
import { createHmac } from 'crypto';
|
import { createHash, createHmac } from 'crypto';
|
||||||
// IMPORTANT! Do not switch to anther bcrypt library unless really necessary and
|
// IMPORTANT! Do not switch to anther bcrypt library unless really necessary and
|
||||||
// tested with all possible systems like Windows, Alpine on ARM, FreeBSD, ...
|
// tested with all possible systems like Windows, Alpine on ARM, FreeBSD, ...
|
||||||
import { compare } from 'bcryptjs';
|
import { compare } from 'bcryptjs';
|
||||||
|
@ -196,6 +196,12 @@ class App {
|
||||||
'oauth1': urlBaseWebhook + `${this.restEndpoint}/oauth1-credential/callback`,
|
'oauth1': urlBaseWebhook + `${this.restEndpoint}/oauth1-credential/callback`,
|
||||||
'oauth2': urlBaseWebhook + `${this.restEndpoint}/oauth2-credential/callback`,
|
'oauth2': urlBaseWebhook + `${this.restEndpoint}/oauth2-credential/callback`,
|
||||||
},
|
},
|
||||||
|
versionNotifications: {
|
||||||
|
enabled: config.get('versionNotifications.enabled'),
|
||||||
|
endpoint: config.get('versionNotifications.endpoint'),
|
||||||
|
infoUrl: config.get('versionNotifications.infoUrl'),
|
||||||
|
},
|
||||||
|
instanceId: '',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -225,6 +231,7 @@ class App {
|
||||||
|
|
||||||
this.versions = await GenericHelpers.getVersions();
|
this.versions = await GenericHelpers.getVersions();
|
||||||
this.frontendSettings.versionCli = this.versions.cli;
|
this.frontendSettings.versionCli = this.versions.cli;
|
||||||
|
this.frontendSettings.instanceId = await generateInstanceId() as string;
|
||||||
|
|
||||||
await this.externalHooks.run('frontend.settings', [this.frontendSettings]);
|
await this.externalHooks.run('frontend.settings', [this.frontendSettings]);
|
||||||
|
|
||||||
|
@ -2210,3 +2217,10 @@ async function getExecutionsCount(countFilter: IDataObject): Promise<{ count: nu
|
||||||
const count = await Db.collections.Execution!.count(countFilter);
|
const count = await Db.collections.Execution!.count(countFilter);
|
||||||
return { count, estimate: false };
|
return { count, estimate: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function generateInstanceId() {
|
||||||
|
const encryptionKey = await UserSettings.getEncryptionKey();
|
||||||
|
const hash = encryptionKey ? createHash('sha256').update(encryptionKey.slice(Math.round(encryptionKey.length / 2))).digest('hex') : undefined;
|
||||||
|
|
||||||
|
return hash;
|
||||||
|
}
|
||||||
|
|
|
@ -25,7 +25,9 @@
|
||||||
"test:unit": "vue-cli-service test:unit"
|
"test:unit": "vue-cli-service test:unit"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"v-click-outside": "^3.1.2"
|
"timeago.js": "^4.0.2",
|
||||||
|
"v-click-outside": "^3.1.2",
|
||||||
|
"vue-fragment": "^1.5.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@beyonk/google-fonts-webpack-plugin": "^1.5.0",
|
"@beyonk/google-fonts-webpack-plugin": "^1.5.0",
|
||||||
|
|
|
@ -445,6 +445,12 @@ export interface IPushDataConsoleMessage {
|
||||||
message: string;
|
message: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IVersionNotificationSettings {
|
||||||
|
enabled: boolean;
|
||||||
|
endpoint: string;
|
||||||
|
infoUrl: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface IN8nUISettings {
|
export interface IN8nUISettings {
|
||||||
endpointWebhook: string;
|
endpointWebhook: string;
|
||||||
endpointWebhookTest: string;
|
endpointWebhookTest: string;
|
||||||
|
@ -463,6 +469,8 @@ export interface IN8nUISettings {
|
||||||
n8nMetadata?: {
|
n8nMetadata?: {
|
||||||
[key: string]: string | number | undefined;
|
[key: string]: string | number | undefined;
|
||||||
};
|
};
|
||||||
|
versionNotifications: IVersionNotificationSettings;
|
||||||
|
instanceId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IWorkflowSettings extends IWorkflowSettingsWorkflow {
|
export interface IWorkflowSettings extends IWorkflowSettingsWorkflow {
|
||||||
|
@ -547,6 +555,29 @@ export interface ITagRow {
|
||||||
delete?: boolean;
|
delete?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IVersion {
|
||||||
|
name: string;
|
||||||
|
nodes: IVersionNode[];
|
||||||
|
createdAt: string;
|
||||||
|
description: string;
|
||||||
|
documentationUrl: string;
|
||||||
|
hasBreakingChange: boolean;
|
||||||
|
hasSecurityFix: boolean;
|
||||||
|
hasSecurityIssue: boolean;
|
||||||
|
securityIssueFixVersion: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IVersionNode {
|
||||||
|
name: string;
|
||||||
|
displayName: string;
|
||||||
|
icon: string;
|
||||||
|
defaults: INodeParameters;
|
||||||
|
iconData: {
|
||||||
|
type: string;
|
||||||
|
icon?: string;
|
||||||
|
fileBuffer?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
export interface IRootState {
|
export interface IRootState {
|
||||||
activeExecutions: IExecutionsCurrentSummaryExtended[];
|
activeExecutions: IExecutionsCurrentSummaryExtended[];
|
||||||
activeWorkflows: string[];
|
activeWorkflows: string[];
|
||||||
|
@ -583,6 +614,7 @@ export interface IRootState {
|
||||||
urlBaseWebhook: string;
|
urlBaseWebhook: string;
|
||||||
workflow: IWorkflowDb;
|
workflow: IWorkflowDb;
|
||||||
sidebarMenuItems: IMenuItem[];
|
sidebarMenuItems: IMenuItem[];
|
||||||
|
instanceId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ITagsState {
|
export interface ITagsState {
|
||||||
|
@ -605,6 +637,12 @@ export interface IUiState {
|
||||||
isPageLoading: boolean;
|
isPageLoading: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IVersionsState {
|
||||||
|
versionNotificationSettings: IVersionNotificationSettings;
|
||||||
|
nextVersions: IVersion[];
|
||||||
|
currentVersion: IVersion | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
export interface IWorkflowsState {
|
export interface IWorkflowsState {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,6 @@ import {
|
||||||
IRestApiContext,
|
IRestApiContext,
|
||||||
} from '../Interface';
|
} from '../Interface';
|
||||||
|
|
||||||
|
|
||||||
class ResponseError extends Error {
|
class ResponseError extends Error {
|
||||||
// The HTTP status code of response
|
// The HTTP status code of response
|
||||||
httpStatusCode?: number;
|
httpStatusCode?: number;
|
||||||
|
@ -91,6 +90,6 @@ export async function makeRestApiRequest(context: IRestApiContext, method: Metho
|
||||||
return response.data;
|
return response.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function get(baseURL: string, endpoint: string, params?: IDataObject) {
|
export async function get(baseURL: string, endpoint: string, params?: IDataObject, headers?: IDataObject) {
|
||||||
return await request({method: 'GET', baseURL, endpoint, data: params});
|
return await request({method: 'GET', baseURL, endpoint, headers, data: params});
|
||||||
}
|
}
|
||||||
|
|
9
packages/editor-ui/src/api/versions.ts
Normal file
9
packages/editor-ui/src/api/versions.ts
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import { IVersion } from '@/Interface';
|
||||||
|
import { INSTANCE_ID_HEADER } from '@/constants';
|
||||||
|
import { IDataObject } from 'n8n-workflow';
|
||||||
|
import { get } from './helpers';
|
||||||
|
|
||||||
|
export async function getNextVersions(endpoint: string, version: string, instanceId: string): Promise<IVersion[]> {
|
||||||
|
const headers = {[INSTANCE_ID_HEADER as string] : instanceId};
|
||||||
|
return await get(endpoint, version, {}, headers);
|
||||||
|
}
|
51
packages/editor-ui/src/components/Badge.vue
Normal file
51
packages/editor-ui/src/components/Badge.vue
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
<template functional>
|
||||||
|
<fragment>
|
||||||
|
<el-tag
|
||||||
|
v-if="props.type === 'danger'"
|
||||||
|
type="danger"
|
||||||
|
size="small"
|
||||||
|
:class="$style['danger']"
|
||||||
|
>
|
||||||
|
{{ props.text }}
|
||||||
|
</el-tag>
|
||||||
|
<el-tag
|
||||||
|
v-else-if="props.type === 'warning'"
|
||||||
|
size="small"
|
||||||
|
:class="$style['warning']"
|
||||||
|
>
|
||||||
|
{{ props.text }}
|
||||||
|
</el-tag>
|
||||||
|
</fragment>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
export default {
|
||||||
|
props: ["text", "type"],
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" module>
|
||||||
|
.badge {
|
||||||
|
font-size: 11px;
|
||||||
|
line-height: 18px;
|
||||||
|
max-height: 18px;
|
||||||
|
font-weight: 400;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 2px 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.danger {
|
||||||
|
composes: badge;
|
||||||
|
color: $--badge-danger-color;
|
||||||
|
background-color: $--badge-danger-background-color;
|
||||||
|
border-color: $--badge-danger-border-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warning {
|
||||||
|
composes: badge;
|
||||||
|
background-color: $--badge-warning-background-color;
|
||||||
|
color: $--badge-warning-color;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
</style>
|
36
packages/editor-ui/src/components/GiftNotificationIcon.vue
Normal file
36
packages/editor-ui/src/components/GiftNotificationIcon.vue
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
<template>
|
||||||
|
<div :class="$style['gift-icon']">
|
||||||
|
<font-awesome-icon icon="gift" />
|
||||||
|
<div :class="$style['notification']">
|
||||||
|
<div></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" module>
|
||||||
|
.gift-icon {
|
||||||
|
display: flex;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.notification {
|
||||||
|
height: .47em;
|
||||||
|
width: .47em;
|
||||||
|
border-radius: 50%;
|
||||||
|
color: $--gift-notification-active-color;
|
||||||
|
position: absolute;
|
||||||
|
background-color: $--gift-notification-outer-color;
|
||||||
|
right: -.3em;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
top: -.148em;
|
||||||
|
|
||||||
|
div {
|
||||||
|
height: .36em;
|
||||||
|
width: .36em;
|
||||||
|
background-color: $--gift-notification-inner-color;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -127,6 +127,14 @@
|
||||||
|
|
||||||
<MenuItemsIterator :items="sidebarMenuBottomItems" :root="true"/>
|
<MenuItemsIterator :items="sidebarMenuBottomItems" :root="true"/>
|
||||||
|
|
||||||
|
<div class="footer-menu-items">
|
||||||
|
<el-menu-item index="updates" class="updates" v-if="hasVersionUpdates" @click="openUpdatesPanel">
|
||||||
|
<div class="gift-container">
|
||||||
|
<GiftNotificationIcon />
|
||||||
|
</div>
|
||||||
|
<span slot="title" class="item-title-root">{{nextVersions.length > 99 ? '99+' : nextVersions.length}} update{{nextVersions.length > 1 ? 's' : ''}} available</span>
|
||||||
|
</el-menu-item>
|
||||||
|
</div>
|
||||||
</el-menu>
|
</el-menu>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
@ -149,6 +157,7 @@ import About from '@/components/About.vue';
|
||||||
import CredentialsEdit from '@/components/CredentialsEdit.vue';
|
import CredentialsEdit from '@/components/CredentialsEdit.vue';
|
||||||
import CredentialsList from '@/components/CredentialsList.vue';
|
import CredentialsList from '@/components/CredentialsList.vue';
|
||||||
import ExecutionsList from '@/components/ExecutionsList.vue';
|
import ExecutionsList from '@/components/ExecutionsList.vue';
|
||||||
|
import GiftNotificationIcon from './GiftNotificationIcon.vue';
|
||||||
import WorkflowSettings from '@/components/WorkflowSettings.vue';
|
import WorkflowSettings from '@/components/WorkflowSettings.vue';
|
||||||
|
|
||||||
import { genericHelpers } from '@/components/mixins/genericHelpers';
|
import { genericHelpers } from '@/components/mixins/genericHelpers';
|
||||||
|
@ -212,6 +221,7 @@ export default mixins(
|
||||||
CredentialsEdit,
|
CredentialsEdit,
|
||||||
CredentialsList,
|
CredentialsList,
|
||||||
ExecutionsList,
|
ExecutionsList,
|
||||||
|
GiftNotificationIcon,
|
||||||
WorkflowSettings,
|
WorkflowSettings,
|
||||||
MenuItemsIterator,
|
MenuItemsIterator,
|
||||||
},
|
},
|
||||||
|
@ -232,6 +242,10 @@ export default mixins(
|
||||||
...mapGetters('ui', {
|
...mapGetters('ui', {
|
||||||
isCollapsed: 'sidebarMenuCollapsed',
|
isCollapsed: 'sidebarMenuCollapsed',
|
||||||
}),
|
}),
|
||||||
|
...mapGetters('versions', [
|
||||||
|
'hasVersionUpdates',
|
||||||
|
'nextVersions',
|
||||||
|
]),
|
||||||
exeuctionId (): string | undefined {
|
exeuctionId (): string | undefined {
|
||||||
return this.$route.params.id;
|
return this.$route.params.id;
|
||||||
},
|
},
|
||||||
|
@ -311,6 +325,9 @@ export default mixins(
|
||||||
openTagManager() {
|
openTagManager() {
|
||||||
this.$store.dispatch('ui/openTagsManagerModal');
|
this.$store.dispatch('ui/openTagsManagerModal');
|
||||||
},
|
},
|
||||||
|
openUpdatesPanel() {
|
||||||
|
this.$store.dispatch('ui/openUpdatesPanel');
|
||||||
|
},
|
||||||
async stopExecution () {
|
async stopExecution () {
|
||||||
const executionId = this.$store.getters.activeExecutionId;
|
const executionId = this.$store.getters.activeExecutionId;
|
||||||
if (executionId === null) {
|
if (executionId === null) {
|
||||||
|
@ -574,6 +591,39 @@ a.logo {
|
||||||
&.expanded {
|
&.expanded {
|
||||||
width: $--sidebar-expanded-width;
|
width: $--sidebar-expanded-width;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-menu-items {
|
||||||
|
display: flex;
|
||||||
|
flex-grow: 1;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-end;
|
||||||
|
padding-bottom: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-menu-item.updates {
|
||||||
|
color: $--sidebar-inactive-color;
|
||||||
|
.item-title-root {
|
||||||
|
font-size: 13px;
|
||||||
|
top: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: $--sidebar-active-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gift-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: center;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,6 +1,21 @@
|
||||||
<template>
|
<template>
|
||||||
<div v-if="dialogVisible">
|
<div>
|
||||||
|
<el-drawer
|
||||||
|
v-if="drawer"
|
||||||
|
:direction="drawerDirection"
|
||||||
|
:visible="visible && visibleDrawer"
|
||||||
|
:size="drawerWidth"
|
||||||
|
:before-close="closeDrawer"
|
||||||
|
>
|
||||||
|
<template v-slot:title>
|
||||||
|
<slot name="header" />
|
||||||
|
</template>
|
||||||
|
<template>
|
||||||
|
<slot name="content"/>
|
||||||
|
</template>
|
||||||
|
</el-drawer>
|
||||||
<el-dialog
|
<el-dialog
|
||||||
|
v-else
|
||||||
:visible="dialogVisible"
|
:visible="dialogVisible"
|
||||||
:before-close="closeDialog"
|
:before-close="closeDialog"
|
||||||
:title="title"
|
:title="title"
|
||||||
|
@ -32,7 +47,12 @@ const sizeMap: {[size: string]: string} = {
|
||||||
|
|
||||||
export default Vue.extend({
|
export default Vue.extend({
|
||||||
name: "Modal",
|
name: "Modal",
|
||||||
props: ['name', 'title', 'eventBus', 'size'],
|
props: ['name', 'title', 'eventBus', 'size', 'drawer', 'drawerDirection', 'drawerWidth', 'visible'],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
visibleDrawer: this.drawer,
|
||||||
|
};
|
||||||
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
window.addEventListener('keydown', this.onWindowKeydown);
|
window.addEventListener('keydown', this.onWindowKeydown);
|
||||||
|
|
||||||
|
@ -65,8 +85,18 @@ export default Vue.extend({
|
||||||
this.$emit('enter');
|
this.$emit('enter');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
closeDialog() {
|
closeDialog(callback?: () => void) {
|
||||||
this.$store.commit('ui/closeTopModal');
|
this.$store.commit('ui/closeTopModal');
|
||||||
|
if (callback) {
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
closeDrawer() {
|
||||||
|
this.visibleDrawer = false;
|
||||||
|
setTimeout(() =>{
|
||||||
|
this.$store.commit('ui/closeTopModal');
|
||||||
|
this.visibleDrawer = true;
|
||||||
|
}, 300); // delayed for closing animation to take effect
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
@ -84,6 +114,15 @@ export default Vue.extend({
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
.el-drawer__header {
|
||||||
|
margin: 0;
|
||||||
|
padding: 30px 30px 0 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-drawer__body {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
.dialog-wrapper {
|
.dialog-wrapper {
|
||||||
* {
|
* {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
v-if="isOpen(name)"
|
v-if="isOpen(name) || keepAlive"
|
||||||
>
|
>
|
||||||
<slot :modalName="name" :active="isActive(name)"></slot>
|
<slot :modalName="name" :active="isActive(name)" :open="isOpen(name)"></slot>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -11,7 +11,7 @@ import Vue from "vue";
|
||||||
|
|
||||||
export default Vue.extend({
|
export default Vue.extend({
|
||||||
name: "ModalRoot",
|
name: "ModalRoot",
|
||||||
props: ["name"],
|
props: ["name", "keepAlive"],
|
||||||
methods: {
|
methods: {
|
||||||
isActive(name: string) {
|
isActive(name: string) {
|
||||||
return this.$store.getters['ui/isModalActive'](name);
|
return this.$store.getters['ui/isModalActive'](name);
|
||||||
|
|
|
@ -24,17 +24,26 @@
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</ModalRoot>
|
</ModalRoot>
|
||||||
|
<ModalRoot :name="VERSIONS_MODAL_KEY" :keepAlive="true">
|
||||||
|
<template v-slot="{ modalName, open }">
|
||||||
|
<UpdatesPanel
|
||||||
|
:modalName="modalName"
|
||||||
|
:visible="open"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</ModalRoot>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Vue from "vue";
|
import Vue from "vue";
|
||||||
import { DUPLICATE_MODAL_KEY, TAGS_MANAGER_MODAL_KEY, WORKLOW_OPEN_MODAL_KEY } from '@/constants';
|
import { DUPLICATE_MODAL_KEY, TAGS_MANAGER_MODAL_KEY, WORKLOW_OPEN_MODAL_KEY, VERSIONS_MODAL_KEY } from '@/constants';
|
||||||
|
|
||||||
import TagsManager from "@/components/TagsManager/TagsManager.vue";
|
import TagsManager from "@/components/TagsManager/TagsManager.vue";
|
||||||
import DuplicateWorkflowDialog from "@/components/DuplicateWorkflowDialog.vue";
|
import DuplicateWorkflowDialog from "@/components/DuplicateWorkflowDialog.vue";
|
||||||
import WorkflowOpen from "@/components/WorkflowOpen.vue";
|
import WorkflowOpen from "@/components/WorkflowOpen.vue";
|
||||||
import ModalRoot from "./ModalRoot.vue";
|
import ModalRoot from "./ModalRoot.vue";
|
||||||
|
import UpdatesPanel from "./UpdatesPanel.vue";
|
||||||
|
|
||||||
export default Vue.extend({
|
export default Vue.extend({
|
||||||
name: "Modals",
|
name: "Modals",
|
||||||
|
@ -43,11 +52,13 @@ export default Vue.extend({
|
||||||
DuplicateWorkflowDialog,
|
DuplicateWorkflowDialog,
|
||||||
WorkflowOpen,
|
WorkflowOpen,
|
||||||
ModalRoot,
|
ModalRoot,
|
||||||
|
UpdatesPanel,
|
||||||
},
|
},
|
||||||
data: () => ({
|
data: () => ({
|
||||||
DUPLICATE_MODAL_KEY,
|
DUPLICATE_MODAL_KEY,
|
||||||
TAGS_MANAGER_MODAL_KEY,
|
TAGS_MANAGER_MODAL_KEY,
|
||||||
WORKLOW_OPEN_MODAL_KEY,
|
WORKLOW_OPEN_MODAL_KEY,
|
||||||
|
VERSIONS_MODAL_KEY,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -30,7 +30,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<NodeIcon class="node-icon" :nodeType="nodeType" size="60" :style="nodeIconStyle" :shrink="true"/>
|
<NodeIcon class="node-icon" :nodeType="nodeType" size="60" :shrink="true" :disabled="this.data.disabled"/>
|
||||||
</div>
|
</div>
|
||||||
<div class="node-description">
|
<div class="node-description">
|
||||||
<div class="node-name" :title="data.name">
|
<div class="node-name" :title="data.name">
|
||||||
|
@ -77,11 +77,6 @@ export default mixins(externalHooks, nodeBase, nodeHelpers, workflowHelpers).ext
|
||||||
isExecuting (): boolean {
|
isExecuting (): boolean {
|
||||||
return this.$store.getters.executingNode === this.data.name;
|
return this.$store.getters.executingNode === this.data.name;
|
||||||
},
|
},
|
||||||
nodeIconStyle (): object {
|
|
||||||
return {
|
|
||||||
color: this.data.disabled ? '#ccc' : this.data.color,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
nodeType (): INodeTypeDescription | null {
|
nodeType (): INodeTypeDescription | null {
|
||||||
return this.$store.getters.nodeType(this.data.type);
|
return this.$store.getters.nodeType(this.data.type);
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<template functional>
|
<template functional>
|
||||||
<div :class="{[$style['node-item']]: true, [$style.bordered]: props.bordered}">
|
<div :class="{[$style['node-item']]: true, [$style.bordered]: props.bordered}">
|
||||||
<NodeIcon :class="$style['node-icon']" :nodeType="props.nodeType" :style="{color: props.nodeType.defaults.color}" />
|
<NodeIcon :class="$style['node-icon']" :nodeType="props.nodeType" />
|
||||||
<div>
|
<div>
|
||||||
<div :class="$style.details">
|
<div :class="$style.details">
|
||||||
<span :class="$style.name">{{props.nodeType.displayName}}</span>
|
<span :class="$style.name">{{props.nodeType.displayName}}</span>
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="node-icon-wrapper" :style="iconStyleData" :class="{shrink: isSvgIcon && shrink, full: !shrink}">
|
<div class="node-icon-wrapper" :style="iconStyleData" :class="{shrink: isSvgIcon && shrink, full: !shrink}">
|
||||||
<div v-if="nodeIconData !== null" class="icon">
|
<div v-if="nodeIconData !== null" class="icon">
|
||||||
<img :src="nodeIconData.path" style="max-width: 100%; max-height: 100%;" v-if="nodeIconData.type === 'file'"/>
|
<img v-if="nodeIconData.type === 'file'" :src="nodeIconData.fileBuffer || nodeIconData.path" style="max-width: 100%; max-height: 100%;" />
|
||||||
<font-awesome-icon :icon="nodeIconData.path" v-else-if="nodeIconData.type === 'fa'" />
|
<font-awesome-icon v-else :icon="nodeIconData.icon || nodeIconData.path" />
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="node-icon-placeholder">
|
<div v-else class="node-icon-placeholder">
|
||||||
{{nodeType !== null ? nodeType.displayName.charAt(0) : '?' }}
|
{{nodeType !== null ? nodeType.displayName.charAt(0) : '?' }}
|
||||||
|
@ -26,16 +26,19 @@ export default Vue.extend({
|
||||||
'nodeType',
|
'nodeType',
|
||||||
'size',
|
'size',
|
||||||
'shrink',
|
'shrink',
|
||||||
|
'disabled',
|
||||||
],
|
],
|
||||||
computed: {
|
computed: {
|
||||||
iconStyleData (): object {
|
iconStyleData (): object {
|
||||||
|
const color = this.disabled ? '#ccc' : this.nodeType.defaults && this.nodeType.defaults.color;
|
||||||
if (!this.size) {
|
if (!this.size) {
|
||||||
return {};
|
return {color};
|
||||||
}
|
}
|
||||||
|
|
||||||
const size = parseInt(this.size, 10);
|
const size = parseInt(this.size, 10);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
color,
|
||||||
width: size + 'px',
|
width: size + 'px',
|
||||||
height: size + 'px',
|
height: size + 'px',
|
||||||
'font-size': Math.floor(parseInt(this.size, 10) * 0.6) + 'px',
|
'font-size': Math.floor(parseInt(this.size, 10) * 0.6) + 'px',
|
||||||
|
@ -54,6 +57,10 @@ export default Vue.extend({
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.nodeType.iconData) {
|
||||||
|
return this.nodeType.iconData;
|
||||||
|
}
|
||||||
|
|
||||||
const restUrl = this.$store.getters.getRestUrl;
|
const restUrl = this.$store.getters.getRestUrl;
|
||||||
|
|
||||||
if (this.nodeType.icon) {
|
if (this.nodeType.icon) {
|
||||||
|
@ -94,6 +101,10 @@ export default Vue.extend({
|
||||||
&.full .icon {
|
&.full .icon {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.shrink .icon {
|
&.shrink .icon {
|
||||||
|
|
15
packages/editor-ui/src/components/TimeAgo.vue
Normal file
15
packages/editor-ui/src/components/TimeAgo.vue
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
<template functional>
|
||||||
|
<span>
|
||||||
|
{{$options.format(props.date)}}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { format } from 'timeago.js';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'UpdatesPanel',
|
||||||
|
props: ['date'],
|
||||||
|
format,
|
||||||
|
};
|
||||||
|
</script>
|
122
packages/editor-ui/src/components/UpdatesPanel.vue
Normal file
122
packages/editor-ui/src/components/UpdatesPanel.vue
Normal file
|
@ -0,0 +1,122 @@
|
||||||
|
<template>
|
||||||
|
<Modal
|
||||||
|
:name="modalName"
|
||||||
|
:drawer="true"
|
||||||
|
:visible="visible"
|
||||||
|
drawerDirection="ltr"
|
||||||
|
drawerWidth="520px"
|
||||||
|
>
|
||||||
|
<template slot="header">
|
||||||
|
<span :class="$style.title">We’ve been busy ✨</span>
|
||||||
|
</template>
|
||||||
|
<template slot="content">
|
||||||
|
<section :class="$style['description']">
|
||||||
|
|
||||||
|
<p v-if="currentVersion">
|
||||||
|
You’re on {{ currentVersion.name }}, which was released
|
||||||
|
<strong><TimeAgo :date="currentVersion.createdAt" /></strong> and is
|
||||||
|
<strong>{{ nextVersions.length }} version{{nextVersions.length > 1 ? "s" : ""}}</strong>
|
||||||
|
behind the latest and greatest n8n
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<a
|
||||||
|
:class="$style['info-url']"
|
||||||
|
:href="infoUrl"
|
||||||
|
v-if="infoUrl"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
<font-awesome-icon icon="info-circle"></font-awesome-icon>
|
||||||
|
<span>How to update your n8n version</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
</section>
|
||||||
|
<section :class="$style.versions">
|
||||||
|
<div
|
||||||
|
v-for="version in nextVersions"
|
||||||
|
:key="version.name"
|
||||||
|
:class="$style['versions-card']"
|
||||||
|
>
|
||||||
|
<VersionCard :version="version" />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import Vue from 'vue';
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
|
|
||||||
|
import Modal from './Modal.vue';
|
||||||
|
import TimeAgo from './TimeAgo.vue';
|
||||||
|
import VersionCard from './VersionCard.vue';
|
||||||
|
|
||||||
|
export default Vue.extend({
|
||||||
|
name: 'UpdatesPanel',
|
||||||
|
components: {
|
||||||
|
Modal,
|
||||||
|
VersionCard,
|
||||||
|
TimeAgo,
|
||||||
|
},
|
||||||
|
props: ['modalName', 'visible'],
|
||||||
|
computed: {
|
||||||
|
...mapGetters('versions', ['nextVersions', 'currentVersion', 'infoUrl']),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style module lang="scss">
|
||||||
|
.title {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 24px;
|
||||||
|
line-height: 24px;
|
||||||
|
color: $--updates-panel-text-color;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
padding: 0px 30px;
|
||||||
|
margin-block-start: 16px;
|
||||||
|
margin-block-end: 30px;
|
||||||
|
|
||||||
|
p {
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 22px;
|
||||||
|
color: $--updates-panel-description-text-color;
|
||||||
|
font-weight: 400;
|
||||||
|
margin: 0 0 16px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
div {
|
||||||
|
padding-top: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.versions {
|
||||||
|
background-color: $--updates-panel-dark-background-color;
|
||||||
|
border-top: $--updates-panel-border;
|
||||||
|
height: 100%;
|
||||||
|
padding: 30px;
|
||||||
|
overflow-y: scroll;
|
||||||
|
padding-bottom: 220px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.versions-card {
|
||||||
|
margin-block-end: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-url {
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 14px;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
color: $--updates-panel-info-icon-color;
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
color: $--updates-panel-info-url-color;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
132
packages/editor-ui/src/components/VersionCard.vue
Normal file
132
packages/editor-ui/src/components/VersionCard.vue
Normal file
|
@ -0,0 +1,132 @@
|
||||||
|
<template functional>
|
||||||
|
<a v-if="props.version" :set="version = props.version" :href="version.documentationUrl" target="_blank" :class="$style.card">
|
||||||
|
<div :class="$style.header">
|
||||||
|
<div>
|
||||||
|
<div :class="$style.name">
|
||||||
|
Version {{version.name}}
|
||||||
|
</div>
|
||||||
|
<WarningTooltip v-if="version.hasSecurityIssue">
|
||||||
|
<template>
|
||||||
|
This version has a security issue.<br/>It is listed here for completeness.
|
||||||
|
</template>
|
||||||
|
</WarningTooltip>
|
||||||
|
<Badge
|
||||||
|
v-if="version.hasSecurityFix"
|
||||||
|
text="Security update"
|
||||||
|
type="danger"
|
||||||
|
/>
|
||||||
|
<Badge
|
||||||
|
v-if="version.hasBreakingChange"
|
||||||
|
text="Breaking changes"
|
||||||
|
type="warning"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div :class="$style['release-date']">
|
||||||
|
Released <TimeAgo :date="version.createdAt" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div :class="$style.divider" v-if="version.description || (version.nodes && version.nodes.length)"></div>
|
||||||
|
<div>
|
||||||
|
<div v-if="version.description" v-html="version.description" :class="$style.description"></div>
|
||||||
|
<div v-if="version.nodes && version.nodes.length > 0" :class="$style.nodes">
|
||||||
|
<NodeIcon
|
||||||
|
v-for="node in version.nodes"
|
||||||
|
:key="node.name"
|
||||||
|
:nodeType="node"
|
||||||
|
:title="$options.nodeName(node)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import Vue from 'vue';
|
||||||
|
import NodeIcon from './NodeIcon.vue';
|
||||||
|
import TimeAgo from './TimeAgo.vue';
|
||||||
|
import Badge from './Badge.vue';
|
||||||
|
import WarningTooltip from './WarningTooltip.vue';
|
||||||
|
import { IVersionNode } from '@/Interface';
|
||||||
|
|
||||||
|
Vue.component('NodeIcon', NodeIcon);
|
||||||
|
Vue.component('TimeAgo', TimeAgo);
|
||||||
|
Vue.component('Badge', Badge);
|
||||||
|
Vue.component('WarningTooltip', WarningTooltip);
|
||||||
|
|
||||||
|
export default Vue.extend({
|
||||||
|
components: { NodeIcon, TimeAgo, Badge, WarningTooltip },
|
||||||
|
name: 'UpdatesPanel',
|
||||||
|
props: ['version'],
|
||||||
|
// @ts-ignore
|
||||||
|
nodeName (node: IVersionNode): string {
|
||||||
|
return node !== null ? node.displayName : 'unknown';
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style module lang="scss">
|
||||||
|
.card {
|
||||||
|
background-color: $--version-card-background-color;
|
||||||
|
border: $--version-card-border;
|
||||||
|
border-radius: 8px;
|
||||||
|
display: block;
|
||||||
|
padding: 16px;
|
||||||
|
text-decoration: none;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
box-shadow: 0px 2px 10px $--version-card-box-shadow-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
|
||||||
|
> * {
|
||||||
|
display: flex;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
> div:first-child {
|
||||||
|
flex-grow: 1;
|
||||||
|
|
||||||
|
> * {
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.name {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 18px;
|
||||||
|
color: $--version-card-name-text-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.divider {
|
||||||
|
border-bottom: $--version-card-border;
|
||||||
|
width: 100%;
|
||||||
|
margin: 10px 0 15px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 19px;
|
||||||
|
color: $--version-card-description-text-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.release-date {
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 18px;
|
||||||
|
font-weight: 400;
|
||||||
|
color: $--version-card-release-date-text-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodes {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(10, 1fr);
|
||||||
|
grid-row-gap: 12px;
|
||||||
|
margin-block-start: 24px;
|
||||||
|
}
|
||||||
|
</style>
|
15
packages/editor-ui/src/components/WarningTooltip.vue
Normal file
15
packages/editor-ui/src/components/WarningTooltip.vue
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
<template functional>
|
||||||
|
<el-tooltip effect="light" content=" " placement="top" >
|
||||||
|
<div slot="content"><slot /></div>
|
||||||
|
<font-awesome-icon :class="$style['icon']" icon="exclamation-triangle"></font-awesome-icon>
|
||||||
|
</el-tooltip>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
|
||||||
|
<style lang="scss" module>
|
||||||
|
.icon {
|
||||||
|
font-size: 14px;
|
||||||
|
height: 18px;
|
||||||
|
color: $--warning-tooltip-color;
|
||||||
|
}
|
||||||
|
</style>
|
40
packages/editor-ui/src/components/mixins/newVersions.ts
Normal file
40
packages/editor-ui/src/components/mixins/newVersions.ts
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
import mixins from 'vue-typed-mixins';
|
||||||
|
import { showMessage } from './showMessage';
|
||||||
|
import {
|
||||||
|
IVersion,
|
||||||
|
} from '../../Interface';
|
||||||
|
|
||||||
|
export const newVersions = mixins(
|
||||||
|
showMessage,
|
||||||
|
).extend({
|
||||||
|
methods: {
|
||||||
|
async checkForNewVersions() {
|
||||||
|
const enabled = this.$store.getters['versions/areNotificationsEnabled'];
|
||||||
|
if (!enabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.$store.dispatch('versions/fetchVersions');
|
||||||
|
|
||||||
|
const currentVersion: IVersion | undefined = this.$store.getters['versions/currentVersion'];
|
||||||
|
const nextVersions: IVersion[] = this.$store.getters['versions/nextVersions'];
|
||||||
|
if (currentVersion && currentVersion.hasSecurityIssue && nextVersions.length) {
|
||||||
|
const fixVersion = currentVersion.securityIssueFixVersion;
|
||||||
|
let message = `Please update to latest version.`;
|
||||||
|
if (fixVersion) {
|
||||||
|
message = `Please update to version ${fixVersion} or higher.`;
|
||||||
|
}
|
||||||
|
|
||||||
|
message = `${message} <a class="primary-color">More info</a>`;
|
||||||
|
this.$showWarning('Critical update available', message, {
|
||||||
|
onClick: () => {
|
||||||
|
this.$store.dispatch('ui/openUpdatesPanel');
|
||||||
|
},
|
||||||
|
closeOnClick: true,
|
||||||
|
customClass: 'clickable',
|
||||||
|
duration: 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
|
@ -1,5 +1,5 @@
|
||||||
import { Notification } from 'element-ui';
|
import { Notification } from 'element-ui';
|
||||||
import { ElNotificationOptions } from 'element-ui/types/notification';
|
import { ElNotificationComponent, ElNotificationOptions } from 'element-ui/types/notification';
|
||||||
import mixins from 'vue-typed-mixins';
|
import mixins from 'vue-typed-mixins';
|
||||||
|
|
||||||
import { externalHooks } from '@/components/mixins/externalHooks';
|
import { externalHooks } from '@/components/mixins/externalHooks';
|
||||||
|
@ -16,6 +16,30 @@ export const showMessage = mixins(externalHooks).extend({
|
||||||
return Notification(messageData);
|
return Notification(messageData);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
$showWarning(title: string, message: string, config?: {onClick?: () => void, duration?: number, customClass?: string, closeOnClick?: boolean}) {
|
||||||
|
let notification: ElNotificationComponent;
|
||||||
|
if (config && config.closeOnClick) {
|
||||||
|
const cb = config.onClick;
|
||||||
|
config.onClick = () => {
|
||||||
|
if (notification) {
|
||||||
|
notification.close();
|
||||||
|
}
|
||||||
|
if (cb) {
|
||||||
|
cb();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
notification = this.$showMessage({
|
||||||
|
title,
|
||||||
|
message,
|
||||||
|
type: 'warning',
|
||||||
|
...(config || {}),
|
||||||
|
});
|
||||||
|
|
||||||
|
return notification;
|
||||||
|
},
|
||||||
|
|
||||||
$getExecutionError(error?: ExecutionError) {
|
$getExecutionError(error?: ExecutionError) {
|
||||||
// There was a problem with executing the workflow
|
// There was a problem with executing the workflow
|
||||||
let errorMessage = 'There was a problem executing the workflow!';
|
let errorMessage = 'There was a problem executing the workflow!';
|
||||||
|
|
|
@ -14,6 +14,7 @@ export const MAX_TAG_NAME_LENGTH = 24;
|
||||||
export const DUPLICATE_MODAL_KEY = 'duplicate';
|
export const DUPLICATE_MODAL_KEY = 'duplicate';
|
||||||
export const TAGS_MANAGER_MODAL_KEY = 'tagsManager';
|
export const TAGS_MANAGER_MODAL_KEY = 'tagsManager';
|
||||||
export const WORKLOW_OPEN_MODAL_KEY = 'workflowOpen';
|
export const WORKLOW_OPEN_MODAL_KEY = 'workflowOpen';
|
||||||
|
export const VERSIONS_MODAL_KEY = 'versions';
|
||||||
|
|
||||||
// breakpoints
|
// breakpoints
|
||||||
export const BREAKPOINT_SM = 768;
|
export const BREAKPOINT_SM = 768;
|
||||||
|
@ -48,3 +49,6 @@ export const HIDDEN_NODES = ['n8n-nodes-base.start'];
|
||||||
export const WEBHOOK_NODE_NAME = 'n8n-nodes-base.webhook';
|
export const WEBHOOK_NODE_NAME = 'n8n-nodes-base.webhook';
|
||||||
export const HTTP_REQUEST_NODE_NAME = 'n8n-nodes-base.httpRequest';
|
export const HTTP_REQUEST_NODE_NAME = 'n8n-nodes-base.httpRequest';
|
||||||
export const REQUEST_NODE_FORM_URL = 'https://n8n-community.typeform.com/to/K1fBVTZ3';
|
export const REQUEST_NODE_FORM_URL = 'https://n8n-community.typeform.com/to/K1fBVTZ3';
|
||||||
|
|
||||||
|
// General
|
||||||
|
export const INSTANCE_ID_HEADER = 'n8n-instance-id';
|
|
@ -21,6 +21,7 @@ import { runExternalHook } from './components/mixins/externalHooks';
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import vClickOutside from 'v-click-outside';
|
import vClickOutside from 'v-click-outside';
|
||||||
|
import Fragment from 'vue-fragment';
|
||||||
|
|
||||||
import { library } from '@fortawesome/fontawesome-svg-core';
|
import { library } from '@fortawesome/fontawesome-svg-core';
|
||||||
import {
|
import {
|
||||||
|
@ -62,6 +63,7 @@ import {
|
||||||
faFileImport,
|
faFileImport,
|
||||||
faFilePdf,
|
faFilePdf,
|
||||||
faFolderOpen,
|
faFolderOpen,
|
||||||
|
faGift,
|
||||||
faHdd,
|
faHdd,
|
||||||
faHome,
|
faHome,
|
||||||
faHourglass,
|
faHourglass,
|
||||||
|
@ -151,6 +153,7 @@ library.add(faFileExport);
|
||||||
library.add(faFileImport);
|
library.add(faFileImport);
|
||||||
library.add(faFilePdf);
|
library.add(faFilePdf);
|
||||||
library.add(faFolderOpen);
|
library.add(faFolderOpen);
|
||||||
|
library.add(faGift);
|
||||||
library.add(faHdd);
|
library.add(faHdd);
|
||||||
library.add(faHome);
|
library.add(faHome);
|
||||||
library.add(faHourglass);
|
library.add(faHourglass);
|
||||||
|
@ -194,6 +197,7 @@ library.add(faUsers);
|
||||||
library.add(faClock);
|
library.add(faClock);
|
||||||
|
|
||||||
Vue.component('font-awesome-icon', FontAwesomeIcon);
|
Vue.component('font-awesome-icon', FontAwesomeIcon);
|
||||||
|
Vue.use(Fragment.Plugin);
|
||||||
|
|
||||||
Vue.config.productionTip = false;
|
Vue.config.productionTip = false;
|
||||||
router.afterEach((to, from) => {
|
router.afterEach((to, from) => {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { DUPLICATE_MODAL_KEY, TAGS_MANAGER_MODAL_KEY, WORKLOW_OPEN_MODAL_KEY } from '@/constants';
|
import { DUPLICATE_MODAL_KEY, TAGS_MANAGER_MODAL_KEY, VERSIONS_MODAL_KEY, WORKLOW_OPEN_MODAL_KEY } from '@/constants';
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import { ActionContext, Module } from 'vuex';
|
import { ActionContext, Module } from 'vuex';
|
||||||
import {
|
import {
|
||||||
|
@ -19,12 +19,18 @@ const module: Module<IUiState, IRootState> = {
|
||||||
[WORKLOW_OPEN_MODAL_KEY]: {
|
[WORKLOW_OPEN_MODAL_KEY]: {
|
||||||
open: false,
|
open: false,
|
||||||
},
|
},
|
||||||
|
[VERSIONS_MODAL_KEY]: {
|
||||||
|
open: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
modalStack: [],
|
modalStack: [],
|
||||||
sidebarMenuCollapsed: true,
|
sidebarMenuCollapsed: true,
|
||||||
isPageLoading: true,
|
isPageLoading: true,
|
||||||
},
|
},
|
||||||
getters: {
|
getters: {
|
||||||
|
isVersionsOpen: (state: IUiState) => {
|
||||||
|
return state.modals[VERSIONS_MODAL_KEY].open;
|
||||||
|
},
|
||||||
isModalOpen: (state: IUiState) => {
|
isModalOpen: (state: IUiState) => {
|
||||||
return (name: string) => state.modals[name].open;
|
return (name: string) => state.modals[name].open;
|
||||||
},
|
},
|
||||||
|
@ -58,6 +64,9 @@ const module: Module<IUiState, IRootState> = {
|
||||||
openDuplicateModal: async (context: ActionContext<IUiState, IRootState>) => {
|
openDuplicateModal: async (context: ActionContext<IUiState, IRootState>) => {
|
||||||
context.commit('openModal', DUPLICATE_MODAL_KEY);
|
context.commit('openModal', DUPLICATE_MODAL_KEY);
|
||||||
},
|
},
|
||||||
|
openUpdatesPanel: async (context: ActionContext<IUiState, IRootState>) => {
|
||||||
|
context.commit('openModal', VERSIONS_MODAL_KEY);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
62
packages/editor-ui/src/modules/versions.ts
Normal file
62
packages/editor-ui/src/modules/versions.ts
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
import { getNextVersions } from '@/api/versions';
|
||||||
|
import { ActionContext, Module } from 'vuex';
|
||||||
|
import {
|
||||||
|
IRootState,
|
||||||
|
IVersion,
|
||||||
|
IVersionsState,
|
||||||
|
} from '../Interface';
|
||||||
|
|
||||||
|
const module: Module<IVersionsState, IRootState> = {
|
||||||
|
namespaced: true,
|
||||||
|
state: {
|
||||||
|
versionNotificationSettings: {
|
||||||
|
enabled: false,
|
||||||
|
endpoint: '',
|
||||||
|
infoUrl: '',
|
||||||
|
},
|
||||||
|
nextVersions: [],
|
||||||
|
currentVersion: undefined,
|
||||||
|
},
|
||||||
|
getters: {
|
||||||
|
hasVersionUpdates(state: IVersionsState) {
|
||||||
|
return state.nextVersions.length > 0;
|
||||||
|
},
|
||||||
|
nextVersions(state: IVersionsState) {
|
||||||
|
return state.nextVersions;
|
||||||
|
},
|
||||||
|
currentVersion(state: IVersionsState) {
|
||||||
|
return state.currentVersion;
|
||||||
|
},
|
||||||
|
areNotificationsEnabled(state: IVersionsState) {
|
||||||
|
return state.versionNotificationSettings.enabled;
|
||||||
|
},
|
||||||
|
infoUrl(state: IVersionsState) {
|
||||||
|
return state.versionNotificationSettings.infoUrl;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mutations: {
|
||||||
|
setVersions(state: IVersionsState, {versions, currentVersion}: {versions: IVersion[], currentVersion: string}) {
|
||||||
|
state.nextVersions = versions.filter((version) => version.name !== currentVersion);
|
||||||
|
state.currentVersion = versions.find((version) => version.name === currentVersion);
|
||||||
|
},
|
||||||
|
setVersionNotificationSettings(state: IVersionsState, settings: {enabled: true, endpoint: string, infoUrl: string}) {
|
||||||
|
state.versionNotificationSettings = settings;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
async fetchVersions(context: ActionContext<IVersionsState, IRootState>) {
|
||||||
|
try {
|
||||||
|
const { enabled, endpoint } = context.state.versionNotificationSettings;
|
||||||
|
if (enabled && endpoint) {
|
||||||
|
const currentVersion = context.rootState.versionCli;
|
||||||
|
const instanceId = context.rootState.instanceId;
|
||||||
|
const versions = await getNextVersions(endpoint, currentVersion, instanceId);
|
||||||
|
context.commit('setVersions', {versions, currentVersion});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default module;
|
|
@ -28,6 +28,16 @@ $--custom-success-text : #40c351;
|
||||||
$--custom-warning-background : #ffffe5;
|
$--custom-warning-background : #ffffe5;
|
||||||
$--custom-warning-text : #eb9422;
|
$--custom-warning-text : #eb9422;
|
||||||
|
|
||||||
|
// Badge
|
||||||
|
$--badge-danger-color: #f45959;
|
||||||
|
$--badge-danger-background-color: #fef0f0;
|
||||||
|
$--badge-danger-border-color: #fde2e2;
|
||||||
|
$--badge-warning-background-color: rgba(255, 229, 100, 0.3);
|
||||||
|
$--badge-warning-color: #6b5900;
|
||||||
|
|
||||||
|
// Warning tooltip
|
||||||
|
$--warning-tooltip-color: #ff8080;
|
||||||
|
|
||||||
$--custom-node-view-background : #faf9fe;
|
$--custom-node-view-background : #faf9fe;
|
||||||
|
|
||||||
// Table
|
// Table
|
||||||
|
@ -44,8 +54,18 @@ $--custom-input-border-shadow: 1px solid $--custom-input-border-color;
|
||||||
|
|
||||||
$--header-height: 65px;
|
$--header-height: 65px;
|
||||||
|
|
||||||
|
// sidebar
|
||||||
$--sidebar-width: 65px;
|
$--sidebar-width: 65px;
|
||||||
$--sidebar-expanded-width: 200px;
|
$--sidebar-expanded-width: 200px;
|
||||||
|
$--sidebar-inactive-color: #909399;
|
||||||
|
$--sidebar-active-color: $--color-primary;
|
||||||
|
|
||||||
|
// gifts notification
|
||||||
|
$--gift-notification-active-color: $--color-primary;
|
||||||
|
$--gift-notification-inner-color: $--color-primary;
|
||||||
|
$--gift-notification-outer-color: #fff;
|
||||||
|
|
||||||
|
// tags manager
|
||||||
$--tags-manager-min-height: 300px;
|
$--tags-manager-min-height: 300px;
|
||||||
|
|
||||||
// based on element.io breakpoints
|
// based on element.io breakpoints
|
||||||
|
@ -83,3 +103,23 @@ $--node-creator-description-color: #7d7d87;
|
||||||
// trigger icon
|
// trigger icon
|
||||||
$--trigger-icon-border-color: #dcdfe6;
|
$--trigger-icon-border-color: #dcdfe6;
|
||||||
$--trigger-icon-background-color: #fff;
|
$--trigger-icon-background-color: #fff;
|
||||||
|
|
||||||
|
// drawer
|
||||||
|
$--drawer-background-color: #fff;
|
||||||
|
|
||||||
|
// updates-panel
|
||||||
|
$--updates-panel-info-icon-color: #909399;
|
||||||
|
$--updates-panel-info-url-color: $--color-primary;
|
||||||
|
$--updates-panel-border: 1px #dbdfe7 solid;
|
||||||
|
$--updates-panel-dark-background-color: #f8f9fb;
|
||||||
|
$--updates-panel-description-text-color: #7d7d87;
|
||||||
|
$--updates-panel-text-color: #555;
|
||||||
|
|
||||||
|
// versions card
|
||||||
|
$--version-card-name-text-color: #666;
|
||||||
|
$--version-card-background-color: #fff;
|
||||||
|
$--version-card-border: 1px #dbdfe7 solid;
|
||||||
|
$--version-card-description-text-color: #7d7d87;
|
||||||
|
$--version-card-release-date-text-color: #909399;
|
||||||
|
$--version-card-box-shadow-color: rgba(109, 48, 40, 0.07);
|
||||||
|
|
||||||
|
|
|
@ -36,6 +36,7 @@ import {
|
||||||
import tags from './modules/tags';
|
import tags from './modules/tags';
|
||||||
import ui from './modules/ui';
|
import ui from './modules/ui';
|
||||||
import workflows from './modules/workflows';
|
import workflows from './modules/workflows';
|
||||||
|
import versions from './modules/versions';
|
||||||
|
|
||||||
Vue.use(Vuex);
|
Vue.use(Vuex);
|
||||||
|
|
||||||
|
@ -86,12 +87,14 @@ const state: IRootState = {
|
||||||
tags: [],
|
tags: [],
|
||||||
},
|
},
|
||||||
sidebarMenuItems: [],
|
sidebarMenuItems: [],
|
||||||
|
instanceId: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
const modules = {
|
const modules = {
|
||||||
tags,
|
tags,
|
||||||
ui,
|
ui,
|
||||||
workflows,
|
workflows,
|
||||||
|
versions,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const store = new Vuex.Store({
|
export const store = new Vuex.Store({
|
||||||
|
@ -543,9 +546,12 @@ export const store = new Vuex.Store({
|
||||||
setMaxExecutionTimeout (state, maxExecutionTimeout: number) {
|
setMaxExecutionTimeout (state, maxExecutionTimeout: number) {
|
||||||
Vue.set(state, 'maxExecutionTimeout', maxExecutionTimeout);
|
Vue.set(state, 'maxExecutionTimeout', maxExecutionTimeout);
|
||||||
},
|
},
|
||||||
setVersionCli (state, version: string) {
|
setVersionCli(state, version: string) {
|
||||||
Vue.set(state, 'versionCli', version);
|
Vue.set(state, 'versionCli', version);
|
||||||
},
|
},
|
||||||
|
setInstanceId(state, instanceId: string) {
|
||||||
|
Vue.set(state, 'instanceId', instanceId);
|
||||||
|
},
|
||||||
setOauthCallbackUrls(state, urls: IDataObject) {
|
setOauthCallbackUrls(state, urls: IDataObject) {
|
||||||
Vue.set(state, 'oauthCallbackUrls', urls);
|
Vue.set(state, 'oauthCallbackUrls', urls);
|
||||||
},
|
},
|
||||||
|
|
|
@ -125,6 +125,7 @@ import { moveNodeWorkflow } from '@/components/mixins/moveNodeWorkflow';
|
||||||
import { restApi } from '@/components/mixins/restApi';
|
import { restApi } from '@/components/mixins/restApi';
|
||||||
import { showMessage } from '@/components/mixins/showMessage';
|
import { showMessage } from '@/components/mixins/showMessage';
|
||||||
import { titleChange } from '@/components/mixins/titleChange';
|
import { titleChange } from '@/components/mixins/titleChange';
|
||||||
|
import { newVersions } from '@/components/mixins/newVersions';
|
||||||
|
|
||||||
import { workflowHelpers } from '@/components/mixins/workflowHelpers';
|
import { workflowHelpers } from '@/components/mixins/workflowHelpers';
|
||||||
import { workflowRun } from '@/components/mixins/workflowRun';
|
import { workflowRun } from '@/components/mixins/workflowRun';
|
||||||
|
@ -198,6 +199,7 @@ export default mixins(
|
||||||
titleChange,
|
titleChange,
|
||||||
workflowHelpers,
|
workflowHelpers,
|
||||||
workflowRun,
|
workflowRun,
|
||||||
|
newVersions,
|
||||||
)
|
)
|
||||||
.extend({
|
.extend({
|
||||||
name: 'NodeView',
|
name: 'NodeView',
|
||||||
|
@ -2200,8 +2202,10 @@ export default mixins(
|
||||||
this.$store.commit('setExecutionTimeout', settings.executionTimeout);
|
this.$store.commit('setExecutionTimeout', settings.executionTimeout);
|
||||||
this.$store.commit('setMaxExecutionTimeout', settings.maxExecutionTimeout);
|
this.$store.commit('setMaxExecutionTimeout', settings.maxExecutionTimeout);
|
||||||
this.$store.commit('setVersionCli', settings.versionCli);
|
this.$store.commit('setVersionCli', settings.versionCli);
|
||||||
|
this.$store.commit('setInstanceId', settings.instanceId);
|
||||||
this.$store.commit('setOauthCallbackUrls', settings.oauthCallbackUrls);
|
this.$store.commit('setOauthCallbackUrls', settings.oauthCallbackUrls);
|
||||||
this.$store.commit('setN8nMetadata', settings.n8nMetadata || {});
|
this.$store.commit('setN8nMetadata', settings.n8nMetadata || {});
|
||||||
|
this.$store.commit('versions/setVersionNotificationSettings', settings.versionNotifications);
|
||||||
},
|
},
|
||||||
async loadNodeTypes (): Promise<void> {
|
async loadNodeTypes (): Promise<void> {
|
||||||
const nodeTypes = await this.restApi().getNodeTypes();
|
const nodeTypes = await this.restApi().getNodeTypes();
|
||||||
|
@ -2228,6 +2232,7 @@ export default mixins(
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
async mounted () {
|
async mounted () {
|
||||||
this.$root.$on('importWorkflowData', async (data: IDataObject) => {
|
this.$root.$on('importWorkflowData', async (data: IDataObject) => {
|
||||||
const resData = await this.importWorkflowData(data.data as IWorkflowDataUpdate);
|
const resData = await this.importWorkflowData(data.data as IWorkflowDataUpdate);
|
||||||
|
@ -2267,6 +2272,10 @@ export default mixins(
|
||||||
this.$showError(error, 'Init Problem', 'There was a problem initializing the workflow:');
|
this.$showError(error, 'Init Problem', 'There was a problem initializing the workflow:');
|
||||||
}
|
}
|
||||||
this.stopLoading();
|
this.stopLoading();
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
this.checkForNewVersions();
|
||||||
|
}, 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.$externalHooks().run('nodeView.mount');
|
this.$externalHooks().run('nodeView.mount');
|
||||||
|
|
Loading…
Reference in a new issue