mirror of
https://github.com/n8n-io/n8n.git
synced 2024-11-09 22:24:05 -08:00
✨ Update credentials modal (#2154)
* ⚡ Generalize unique entity name generation * ⚡ Standardize variable names * redo credentials * revert some changes, replace got with was * fix v-if order * fix v-if order * update linting * update gulpfile * update ssh display name * update height * update params * update info tip sizes * address design comments * update google button disabled * update icon size to 28px * update design issues * update info tab design * address design comments * update tab size * update run data spacing * address comments, update logo design * fix spacing issues * clean up store * fix create new bug * add loading state * rename prop * remove unused prop * fix select bug * remove label tag * update word break * build * address design comments * update font family of button * update menu opacity * update text * update title * address more comments * update oauth messages * add oauth validation * hide disabled state * update warning modal * show button on text input * clean up cred details * add validation errors * fix bug when deleting cred * Frontend hack to display test button * Created interfaces for testing and endpoint * Testing slack node credentials working * Adding test with node to endpoint for credential testing * Fixed linting and test detectability * Adding required for slack token * Added google sheets credential testing * update message * Adding suggestions by Ivan and Mutasem * Address comments * keep blurred when focused * update font weight of errors * add oauth banner * remove toast * Fixed code bug and added telegram credential testing * scroll to top on success * clean up duplication * Fixed telegram trigger node and added tests to typeform * refactor modal * add more validation support * refactor info tab * scroll to bottom on save, handle cred saving * refactor save button * save cred on valid * save cred on valid * scroll to top if has error * add targets on input labels * delete credentails input * revert fe changes * update validation logic * clean interface * test credentials * update banner design * show testing state * update x position * fix issues * fix focus issues * clean up validation behavior * make error relative * update banner component * update error spacing * don't close dialog * rename button * update how banners behave * if has unsaved changes first * move confirm message * add success banner * update time state * disable transitions * test on open * clean up banner behavior * update banner styling * capitalize * update error banner styling to handle long texts * avoid unnessary content jostling * add loading label * show validation warnings when opening modal * retest cred if not all props req * update scroll to auto * add error warning * update color saturation * set overflow to auto * fix bug to get credentials when connected * round down to minutes * change tab name * update casing oauth * disable credential testing if it has expressions * label same as title * add more space between close and save * remove check on making any changes * hide close on confirm modals * don't accept clicks outside dialog * fix build issues * undo test changes * fix table scrollbar logs * rename modals * fix bug with same name * refactor modal * fix tslint issue * refactor name * update name behavior * update monospace font * remove comment * refactor inputs * refactor error handling * reduce spacing changes * fix doc url oauth1 oauth2 * build * hide infotip if no inputs * address most comments * rename file * fix menu alignment * gst * update types Co-authored-by: Iván Ovejero <ivov.src@gmail.com> Co-authored-by: Omar Ajoue <krynble@gmail.com>
This commit is contained in:
parent
63e2bd25c9
commit
3d6b40b852
|
@ -8,8 +8,8 @@
|
|||
"dev": "lerna exec npm run dev --parallel",
|
||||
"clean:dist": "lerna exec -- rimraf ./dist",
|
||||
"format": "lerna exec npm run format",
|
||||
"lint": "lerna exec npm run lint",
|
||||
"lintfix": "lerna exec npm run lintfix",
|
||||
"lint": "lerna exec npm run lint",
|
||||
"lintfix": "lerna exec npm run lintfix",
|
||||
"optimize-svg": "find ./packages -name '*.svg' ! -name 'pipedrive.svg' -print0 | xargs -0 -P16 -L20 npx svgo",
|
||||
"start": "run-script-os",
|
||||
"start:default": "cd packages/cli/bin && ./n8n",
|
||||
|
|
|
@ -148,6 +148,12 @@ const config = convict({
|
|||
env: 'CREDENTIALS_OVERWRITE_ENDPOINT',
|
||||
},
|
||||
},
|
||||
defaultName: {
|
||||
doc: 'Default name for credentials',
|
||||
format: String,
|
||||
default: 'My credentials',
|
||||
env: 'CREDENTIALS_DEFAULT_NAME',
|
||||
},
|
||||
},
|
||||
|
||||
workflows: {
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||
/* eslint-disable @typescript-eslint/restrict-template-expressions */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-return */
|
||||
/* eslint-disable no-param-reassign */
|
||||
|
@ -11,7 +12,11 @@ import { IDataObject } from 'n8n-workflow';
|
|||
import * as config from '../config';
|
||||
|
||||
// eslint-disable-next-line import/no-cycle
|
||||
import { IPackageVersions } from '.';
|
||||
import { Db, ICredentialsDb, IPackageVersions } from '.';
|
||||
// eslint-disable-next-line import/order
|
||||
import { Like } from 'typeorm';
|
||||
// eslint-disable-next-line import/no-cycle
|
||||
import { WorkflowEntity } from './databases/entities/WorkflowEntity';
|
||||
|
||||
let versionCache: IPackageVersions | undefined;
|
||||
|
||||
|
@ -170,3 +175,56 @@ export function getConfigValueSync(configKey: string): string | boolean | number
|
|||
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a unique name for a workflow or credentials entity.
|
||||
*
|
||||
* - If the name does not yet exist, it returns the requested name.
|
||||
* - If the name already exists once, it returns the requested name suffixed with 2.
|
||||
* - If the name already exists more than once with suffixes, it looks for the max suffix
|
||||
* and returns the requested name with max suffix + 1.
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||
export async function generateUniqueName(
|
||||
requestedName: string,
|
||||
entityType: 'workflow' | 'credentials',
|
||||
) {
|
||||
const findConditions = {
|
||||
select: ['name' as const],
|
||||
where: {
|
||||
name: Like(`${requestedName}%`),
|
||||
},
|
||||
};
|
||||
|
||||
const found: Array<WorkflowEntity | ICredentialsDb> =
|
||||
entityType === 'workflow'
|
||||
? await Db.collections.Workflow!.find(findConditions)
|
||||
: await Db.collections.Credentials!.find(findConditions);
|
||||
|
||||
// name is unique
|
||||
if (found.length === 0) {
|
||||
return { name: requestedName };
|
||||
}
|
||||
|
||||
const maxSuffix = found.reduce((acc, { name }) => {
|
||||
const parts = name.split(`${requestedName} `);
|
||||
|
||||
if (parts.length > 2) return acc;
|
||||
|
||||
const suffix = Number(parts[1]);
|
||||
|
||||
// eslint-disable-next-line no-restricted-globals
|
||||
if (!isNaN(suffix) && Math.ceil(suffix) > acc) {
|
||||
acc = Math.ceil(suffix);
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, 0);
|
||||
|
||||
// name is duplicate but no numeric suffixes exist yet
|
||||
if (maxSuffix === 0) {
|
||||
return { name: `${requestedName} 2` };
|
||||
}
|
||||
|
||||
return { name: `${requestedName} ${maxSuffix + 1}` };
|
||||
}
|
||||
|
|
|
@ -121,6 +121,7 @@ export interface ICredentialsBase {
|
|||
|
||||
export interface ICredentialsDb extends ICredentialsBase, ICredentialsEncrypted {
|
||||
id: number | string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface ICredentialsResponse extends ICredentialsDb {
|
||||
|
|
|
@ -151,6 +151,14 @@ class LoadNodesAndCredentialsClass {
|
|||
let tempCredential: ICredentialType;
|
||||
try {
|
||||
tempCredential = new tempModule[credentialName]() as ICredentialType;
|
||||
|
||||
if (tempCredential.icon && tempCredential.icon.startsWith('file:')) {
|
||||
// If a file icon gets used add the full path
|
||||
tempCredential.icon = `file:${path.join(
|
||||
path.dirname(filePath),
|
||||
tempCredential.icon.substr(5),
|
||||
)}`;
|
||||
}
|
||||
} catch (e) {
|
||||
if (e instanceof TypeError) {
|
||||
throw new Error(
|
||||
|
|
|
@ -52,9 +52,16 @@ import { createHash, createHmac } from 'crypto';
|
|||
import { compare } from 'bcryptjs';
|
||||
import * as promClient from 'prom-client';
|
||||
|
||||
import { Credentials, LoadNodeParameterOptions, UserSettings } from 'n8n-core';
|
||||
import {
|
||||
Credentials,
|
||||
ICredentialTestFunctions,
|
||||
LoadNodeParameterOptions,
|
||||
NodeExecuteFunctions,
|
||||
UserSettings,
|
||||
} from 'n8n-core';
|
||||
|
||||
import {
|
||||
ICredentialsDecrypted,
|
||||
ICredentialsEncrypted,
|
||||
ICredentialType,
|
||||
IDataObject,
|
||||
|
@ -66,6 +73,8 @@ import {
|
|||
IWorkflowBase,
|
||||
IWorkflowCredentials,
|
||||
LoggerProxy,
|
||||
NodeCredentialTestRequest,
|
||||
NodeCredentialTestResult,
|
||||
Workflow,
|
||||
WorkflowExecuteMode,
|
||||
} from 'n8n-workflow';
|
||||
|
@ -131,7 +140,7 @@ import * as config from '../config';
|
|||
import * as TagHelpers from './TagHelpers';
|
||||
import { TagEntity } from './databases/entities/TagEntity';
|
||||
import { WorkflowEntity } from './databases/entities/WorkflowEntity';
|
||||
import { WorkflowNameRequest } from './WorkflowHelpers';
|
||||
import { NameRequest } from './WorkflowHelpers';
|
||||
|
||||
require('body-parser-xml')(bodyParser);
|
||||
|
||||
|
@ -156,6 +165,8 @@ class App {
|
|||
|
||||
defaultWorkflowName: string;
|
||||
|
||||
defaultCredentialsName: string;
|
||||
|
||||
saveDataErrorExecution: string;
|
||||
|
||||
saveDataSuccessExecution: string;
|
||||
|
@ -196,6 +207,7 @@ class App {
|
|||
this.endpointWebhookTest = config.get('endpoints.webhookTest') as string;
|
||||
|
||||
this.defaultWorkflowName = config.get('workflows.defaultName') as string;
|
||||
this.defaultCredentialsName = config.get('credentials.defaultName') as string;
|
||||
|
||||
this.saveDataErrorExecution = config.get('executions.saveDataOnError') as string;
|
||||
this.saveDataSuccessExecution = config.get('executions.saveDataOnSuccess') as string;
|
||||
|
@ -720,41 +732,11 @@ class App {
|
|||
this.app.get(
|
||||
`/${this.restEndpoint}/workflows/new`,
|
||||
ResponseHelper.send(
|
||||
async (req: WorkflowNameRequest, res: express.Response): Promise<{ name: string }> => {
|
||||
const nameToReturn =
|
||||
async (req: NameRequest, res: express.Response): Promise<{ name: string }> => {
|
||||
const requestedName =
|
||||
req.query.name && req.query.name !== '' ? req.query.name : this.defaultWorkflowName;
|
||||
|
||||
const workflows = await Db.collections.Workflow!.find({
|
||||
select: ['name'],
|
||||
where: { name: Like(`${nameToReturn}%`) },
|
||||
});
|
||||
|
||||
// name is unique
|
||||
if (workflows.length === 0) {
|
||||
return { name: nameToReturn };
|
||||
}
|
||||
|
||||
const maxSuffix = workflows.reduce((acc: number, { name }) => {
|
||||
const parts = name.split(`${nameToReturn} `);
|
||||
|
||||
if (parts.length > 2) return acc;
|
||||
|
||||
const suffix = Number(parts[1]);
|
||||
|
||||
// eslint-disable-next-line no-restricted-globals
|
||||
if (!isNaN(suffix) && Math.ceil(suffix) > acc) {
|
||||
acc = Math.ceil(suffix);
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, 0);
|
||||
|
||||
// name is duplicate but no numeric suffixes exist yet
|
||||
if (maxSuffix === 0) {
|
||||
return { name: `${nameToReturn} 2` };
|
||||
}
|
||||
|
||||
return { name: `${nameToReturn} ${maxSuffix + 1}` };
|
||||
return await GenericHelpers.generateUniqueName(requestedName, 'workflow');
|
||||
},
|
||||
),
|
||||
);
|
||||
|
@ -1237,6 +1219,18 @@ class App {
|
|||
// Credentials
|
||||
// ----------------------------------------
|
||||
|
||||
this.app.get(
|
||||
`/${this.restEndpoint}/credentials/new`,
|
||||
ResponseHelper.send(
|
||||
async (req: NameRequest, res: express.Response): Promise<{ name: string }> => {
|
||||
const requestedName =
|
||||
req.query.name && req.query.name !== '' ? req.query.name : this.defaultCredentialsName;
|
||||
|
||||
return await GenericHelpers.generateUniqueName(requestedName, 'credentials');
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
// Deletes a specific credential
|
||||
this.app.delete(
|
||||
`/${this.restEndpoint}/credentials/:id`,
|
||||
|
@ -1323,6 +1317,67 @@ class App {
|
|||
),
|
||||
);
|
||||
|
||||
// Test credentials
|
||||
this.app.post(
|
||||
`/${this.restEndpoint}/credentials-test`,
|
||||
ResponseHelper.send(
|
||||
async (req: express.Request, res: express.Response): Promise<NodeCredentialTestResult> => {
|
||||
const incomingData = req.body as NodeCredentialTestRequest;
|
||||
const credentialType = incomingData.credentials.type;
|
||||
|
||||
// Find nodes that can test this credential.
|
||||
const nodeTypes = NodeTypes();
|
||||
const allNodes = nodeTypes.getAll();
|
||||
|
||||
let foundTestFunction:
|
||||
| ((
|
||||
this: ICredentialTestFunctions,
|
||||
credential: ICredentialsDecrypted,
|
||||
) => Promise<NodeCredentialTestResult>)
|
||||
| undefined;
|
||||
const nodeThatCanTestThisCredential = allNodes.find((node) => {
|
||||
if (
|
||||
incomingData.nodeToTestWith &&
|
||||
node.description.name !== incomingData.nodeToTestWith
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
const credentialTestable = node.description.credentials?.find((credential) => {
|
||||
const testFunctionSearch =
|
||||
credential.name === credentialType && !!credential.testedBy;
|
||||
if (testFunctionSearch) {
|
||||
foundTestFunction = node.methods!.credentialTest![credential.testedBy!];
|
||||
}
|
||||
return testFunctionSearch;
|
||||
});
|
||||
return !!credentialTestable;
|
||||
});
|
||||
|
||||
if (!nodeThatCanTestThisCredential) {
|
||||
return Promise.resolve({
|
||||
status: 'Error',
|
||||
message: 'There are no nodes that can test this credential.',
|
||||
});
|
||||
}
|
||||
|
||||
if (foundTestFunction === undefined) {
|
||||
return Promise.resolve({
|
||||
status: 'Error',
|
||||
message: 'No testing function found for this credential.',
|
||||
});
|
||||
}
|
||||
|
||||
const credentialTestFunctions = NodeExecuteFunctions.getCredentialTestFunctions();
|
||||
|
||||
const output = await foundTestFunction.call(
|
||||
credentialTestFunctions,
|
||||
incomingData.credentials,
|
||||
);
|
||||
return Promise.resolve(output);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
// Updates existing credentials
|
||||
this.app.patch(
|
||||
`/${this.restEndpoint}/credentials/:id`,
|
||||
|
@ -1540,6 +1595,42 @@ class App {
|
|||
),
|
||||
);
|
||||
|
||||
this.app.get(
|
||||
`/${this.restEndpoint}/credential-icon/:credentialType`,
|
||||
async (req: express.Request, res: express.Response): Promise<void> => {
|
||||
try {
|
||||
const credentialName = req.params.credentialType;
|
||||
|
||||
const credentialType = CredentialTypes().getByName(credentialName);
|
||||
|
||||
if (credentialType === undefined) {
|
||||
res.status(404).send('The credentialType is not known.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (credentialType.icon === undefined) {
|
||||
res.status(404).send('No icon found for credential.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!credentialType.icon.startsWith('file:')) {
|
||||
res.status(404).send('Credential does not have a file icon.');
|
||||
return;
|
||||
}
|
||||
|
||||
const filepath = credentialType.icon.substr(5);
|
||||
|
||||
const maxAge = 7 * 24 * 60 * 60 * 1000; // 7 days
|
||||
res.setHeader('Cache-control', `private max-age=${maxAge}`);
|
||||
|
||||
res.sendFile(filepath);
|
||||
} catch (error) {
|
||||
// Error response
|
||||
return ResponseHelper.sendErrorResponse(res, error);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// ----------------------------------------
|
||||
// OAuth1-Credential/Auth
|
||||
// ----------------------------------------
|
||||
|
|
|
@ -408,9 +408,8 @@ export function throwDuplicateEntryError(error: Error) {
|
|||
throw new ResponseHelper.ResponseError(errorMessage, undefined, 400);
|
||||
}
|
||||
|
||||
export type WorkflowNameRequest = Express.Request & {
|
||||
export type NameRequest = Express.Request & {
|
||||
query: {
|
||||
name?: string;
|
||||
offset?: string;
|
||||
};
|
||||
};
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
import {
|
||||
IAllExecuteFunctions,
|
||||
IBinaryData,
|
||||
ICredentialTestFunctions as ICredentialTestFunctionsBase,
|
||||
ICredentialType,
|
||||
IDataObject,
|
||||
IExecuteFunctions as IExecuteFunctionsBase,
|
||||
|
@ -158,6 +159,12 @@ export interface ILoadOptionsFunctions extends ILoadOptionsFunctionsBase {
|
|||
};
|
||||
}
|
||||
|
||||
export interface ICredentialTestFunctions extends ICredentialTestFunctionsBase {
|
||||
helpers: {
|
||||
request: requestPromise.RequestPromiseAPI;
|
||||
};
|
||||
}
|
||||
|
||||
export interface IHookFunctions extends IHookFunctionsBase {
|
||||
helpers: {
|
||||
request: requestPromise.RequestPromiseAPI;
|
||||
|
|
|
@ -65,6 +65,7 @@ import { lookup } from 'mime-types';
|
|||
// eslint-disable-next-line import/no-cycle
|
||||
import {
|
||||
BINARY_ENCODING,
|
||||
ICredentialTestFunctions,
|
||||
IHookFunctions,
|
||||
ILoadOptionsFunctions,
|
||||
IResponseError,
|
||||
|
@ -1280,6 +1281,14 @@ export function getExecuteSingleFunctions(
|
|||
})(workflow, runExecutionData, connectionInputData, inputData, node, itemIndex);
|
||||
}
|
||||
|
||||
export function getCredentialTestFunctions(): ICredentialTestFunctions {
|
||||
return {
|
||||
helpers: {
|
||||
request: requestPromiseWithDefaults,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the execute functions regular nodes have access to in load-options-function.
|
||||
*
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
:size="props.size"
|
||||
:loading="props.loading"
|
||||
:title="props.title || props.label"
|
||||
:class="$style[$options.getClass(props)]"
|
||||
:class="$options.getClass(props, $style)"
|
||||
:round="!props.circle && props.round"
|
||||
:circle="props.circle"
|
||||
:style="$options.styles(props)"
|
||||
|
@ -91,6 +91,10 @@ export default {
|
|||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
transparentBackground: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
components: {
|
||||
ElButton,
|
||||
|
@ -106,10 +110,16 @@ export default {
|
|||
...(props.fullWidth ? { width: '100%' } : {}),
|
||||
};
|
||||
},
|
||||
getClass(props: { type: string; theme?: string }): string {
|
||||
return props.type === 'text'
|
||||
getClass(props: { type: string; theme?: string, transparentBackground: boolean }, $style: any): string {
|
||||
const theme = props.type === 'text'
|
||||
? 'text'
|
||||
: `${props.type}-${props.theme || 'primary'}`;
|
||||
|
||||
if (props.transparentBackground) {
|
||||
return `${$style[theme]} ${$style['transparent']}`;
|
||||
}
|
||||
|
||||
return $style[theme];
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
@ -289,6 +299,11 @@ $color-danger-shade: lightness(
|
|||
--button-active-border-color: transparent;
|
||||
}
|
||||
|
||||
.transparent {
|
||||
--button-background-color: transparent;
|
||||
--button-active-background-color: transparent;
|
||||
}
|
||||
|
||||
.icon {
|
||||
display: inline-flex;
|
||||
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
import N8nInfoTip from './InfoTip.vue';
|
||||
|
||||
export default {
|
||||
title: 'Atoms/InfoTip',
|
||||
component: N8nInfoTip,
|
||||
};
|
||||
|
||||
const Template = (args, { argTypes }) => ({
|
||||
props: Object.keys(argTypes),
|
||||
components: {
|
||||
N8nInfoTip,
|
||||
},
|
||||
template:
|
||||
'<n8n-info-tip>Need help doing something? <a href="/docs" target="_blank">Open docs</a></n8n-info-tip>',
|
||||
});
|
||||
|
||||
export const InputLabel = Template.bind({});
|
36
packages/design-system/src/components/N8nInfoTip/InfoTip.vue
Normal file
36
packages/design-system/src/components/N8nInfoTip/InfoTip.vue
Normal file
|
@ -0,0 +1,36 @@
|
|||
<template functional>
|
||||
<div :class="$style.infotip">
|
||||
<n8n-icon icon="info-circle" /> <span><slot></slot></span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
|
||||
import N8nIcon from '../N8nIcon';
|
||||
|
||||
Vue.component('N8nIcon', N8nIcon);
|
||||
|
||||
export default {
|
||||
name: 'n8n-info-tip',
|
||||
props: {
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.infotip {
|
||||
color: var(--color-text-light);
|
||||
font-size: var(--font-size-2xs);
|
||||
font-weight: var(--font-weight-bold);
|
||||
line-height: var(--font-size-s);
|
||||
word-break: normal;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
svg {
|
||||
font-size: var(--font-size-s);
|
||||
margin-right: var(--spacing-4xs);
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,3 @@
|
|||
import InfoTip from './InfoTip.vue';
|
||||
|
||||
export default InfoTip;
|
|
@ -1,16 +1,18 @@
|
|||
<template functional>
|
||||
<div :class="$style.inputLabel">
|
||||
<label>
|
||||
<div :class="$style.label">
|
||||
<span>{{ props.label }}</span>
|
||||
<span v-if="props.tooltipText" :class="$style.infoIcon">
|
||||
<n8n-tooltip :content="props.tooltipText" placement="top">
|
||||
<n8n-icon icon="info-circle" />
|
||||
</n8n-tooltip>
|
||||
</span>
|
||||
</div>
|
||||
<slot></slot>
|
||||
</label>
|
||||
<div :class="$style.label">
|
||||
<span>
|
||||
{{ $options.methods.addTargetBlank(props.label) }}
|
||||
<span v-if="props.required" :class="$style.required">*</span>
|
||||
</span>
|
||||
<span :class="$style.infoIcon" v-if="props.tooltipText">
|
||||
<n8n-tooltip placement="top" :popper-class="$style.tooltipPopper">
|
||||
<n8n-icon icon="question-circle" />
|
||||
<div slot="content" v-html="props.tooltipText"></div>
|
||||
</n8n-tooltip>
|
||||
</span>
|
||||
</div>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -20,6 +22,8 @@ import Vue from 'vue';
|
|||
import N8nTooltip from '../N8nTooltip';
|
||||
import N8nIcon from '../N8nIcon';
|
||||
|
||||
import { addTargetBlank } from '../utils/helpers';
|
||||
|
||||
Vue.component('N8nIcon', N8nIcon);
|
||||
Vue.component('N8nTooltip', N8nTooltip);
|
||||
|
||||
|
@ -33,6 +37,12 @@ export default {
|
|||
tooltipText: {
|
||||
type: String,
|
||||
},
|
||||
required: {
|
||||
type: Boolean,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
addTargetBlank,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
@ -48,10 +58,22 @@ export default {
|
|||
font-weight: var(--font-weight-bold);
|
||||
font-size: var(--font-size-s);
|
||||
margin-bottom: var(--spacing-2xs);
|
||||
|
||||
* {
|
||||
margin-right: var(--spacing-4xs);
|
||||
}
|
||||
}
|
||||
|
||||
.infoIcon {
|
||||
color: var(--color-text-light);
|
||||
display: var(--info-icon-display, none);
|
||||
}
|
||||
|
||||
.required {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.tooltipPopper {
|
||||
max-width: 400px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
import N8nMenu from './Menu.vue';
|
||||
import N8nMenuItem from '../N8nMenuItem';
|
||||
|
||||
import { action } from '@storybook/addon-actions';
|
||||
|
||||
export default {
|
||||
title: 'Atoms/Menu',
|
||||
component: N8nMenu,
|
||||
argTypes: {
|
||||
type: {
|
||||
control: 'select',
|
||||
options: ['primary', 'secondary'],
|
||||
},
|
||||
},
|
||||
parameters: {
|
||||
backgrounds: { default: '--color-background-xlight' },
|
||||
},
|
||||
};
|
||||
|
||||
const methods = {
|
||||
onSelect: action('select'),
|
||||
};
|
||||
|
||||
const Template = (args, { argTypes }) => ({
|
||||
props: Object.keys(argTypes),
|
||||
components: {
|
||||
N8nMenu,
|
||||
N8nMenuItem,
|
||||
},
|
||||
template:
|
||||
`<n8n-menu v-bind="$props" @select="onSelect">
|
||||
<n8n-menu-item index="1"> <span slot="title">Item 1</span> </n8n-menu-item>
|
||||
<n8n-menu-item index="2"> <span slot="title">Item 2</span> </n8n-menu-item>
|
||||
</n8n-menu>`,
|
||||
methods,
|
||||
});
|
||||
|
||||
export const Primary = Template.bind({});
|
||||
Primary.parameters = {
|
||||
backgrounds: { default: '--color-background-light' },
|
||||
};
|
||||
|
||||
export const Secondary = Template.bind({});
|
||||
Secondary.args = {
|
||||
type: 'secondary',
|
||||
};
|
70
packages/design-system/src/components/N8nMenu/Menu.vue
Normal file
70
packages/design-system/src/components/N8nMenu/Menu.vue
Normal file
|
@ -0,0 +1,70 @@
|
|||
<template functional>
|
||||
<component
|
||||
:is="$options.components.ElMenu"
|
||||
:defaultActive="props.defaultActive"
|
||||
:collapse="props.collapse"
|
||||
:class="$style[props.type + (props.light ? '-light' : '')]"
|
||||
@select="listeners.select"
|
||||
>
|
||||
<slot></slot>
|
||||
</component>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import ElMenu from 'element-ui/lib/menu';
|
||||
|
||||
export default {
|
||||
name: 'n8n-menu',
|
||||
props: {
|
||||
type: {
|
||||
type: String,
|
||||
default: 'primary',
|
||||
validator: (value: string): boolean => ['primary', 'secondary'].includes(value),
|
||||
},
|
||||
defaultActive: {
|
||||
type: String,
|
||||
},
|
||||
collapse: {
|
||||
type: Boolean,
|
||||
},
|
||||
light: {
|
||||
type: Boolean,
|
||||
},
|
||||
},
|
||||
components: {
|
||||
ElMenu,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.menu {
|
||||
max-width: 200px;
|
||||
}
|
||||
|
||||
.primary {
|
||||
composes: menu;
|
||||
}
|
||||
|
||||
.secondary {
|
||||
composes: menu;
|
||||
--menu-font-color: var(--color-text-base);
|
||||
--menu-item-font-color: var(--font-weight-regular);
|
||||
--menu-background-color: transparent;
|
||||
--menu-item-hover-font-color: var(--color-primary);
|
||||
--menu-item-active-font-color: var(--color-text-dark);
|
||||
--menu-item-active-background-color: var(--color-foreground-base);
|
||||
--menu-item-border-radius: 4px;
|
||||
--menu-item-height: 38px;
|
||||
}
|
||||
|
||||
.secondary-light {
|
||||
composes: secondary;
|
||||
--menu-item-active-background-color: hsl(
|
||||
var(--color-foreground-base-h),
|
||||
var(--color-foreground-base-s),
|
||||
var(--color-foreground-base-l),
|
||||
0.7
|
||||
);
|
||||
}
|
||||
</style>
|
3
packages/design-system/src/components/N8nMenu/index.js
Normal file
3
packages/design-system/src/components/N8nMenu/index.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
import N8nMenu from './Menu.vue';
|
||||
|
||||
export default N8nMenu;
|
|
@ -0,0 +1,7 @@
|
|||
<script lang="ts">
|
||||
import ElMenuItem from 'element-ui/lib/menu-item';
|
||||
|
||||
ElMenuItem.name = 'n8n-menu-item';
|
||||
|
||||
export default ElMenuItem;
|
||||
</script>
|
|
@ -0,0 +1,3 @@
|
|||
import N8nMenuItem from './MenuItem.vue';
|
||||
|
||||
export default N8nMenuItem;
|
|
@ -2,22 +2,28 @@ import N8nButton from './N8nButton';
|
|||
import N8nIcon from './N8nIcon';
|
||||
import N8nIconButton from './N8nIconButton';
|
||||
import N8nInput from './N8nInput';
|
||||
import N8nInputLabel from './N8nInputLabel';
|
||||
import N8nInfoTip from './N8nInfoTip';
|
||||
import N8nInputNumber from './N8nInputNumber';
|
||||
import N8nOption from './N8nOption';
|
||||
import N8nInputLabel from './N8nInputLabel';
|
||||
import N8nMenu from './N8nMenu';
|
||||
import N8nMenuItem from './N8nMenuItem';
|
||||
import N8nSelect from './N8nSelect';
|
||||
import N8nSpinner from './N8nSpinner';
|
||||
import N8nTooltip from './N8nTooltip';
|
||||
import N8nOption from './N8nOption';
|
||||
|
||||
export {
|
||||
N8nButton,
|
||||
N8nIcon,
|
||||
N8nIconButton,
|
||||
N8nInfoTip,
|
||||
N8nInput,
|
||||
N8nInputLabel,
|
||||
N8nInputNumber,
|
||||
N8nOption,
|
||||
N8nMenu,
|
||||
N8nMenuItem,
|
||||
N8nSelect,
|
||||
N8nSpinner,
|
||||
N8nTooltip,
|
||||
N8nOption,
|
||||
};
|
||||
|
|
5
packages/design-system/src/components/utils/helpers.ts
Normal file
5
packages/design-system/src/components/utils/helpers.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
export function addTargetBlank(html: string) {
|
||||
return html.includes('href=')
|
||||
? html.replace(/href=/g, 'target="_blank" href=')
|
||||
: html;
|
||||
}
|
|
@ -4,4 +4,6 @@ declare module 'element-ui/lib/tooltip';
|
|||
declare module 'element-ui/lib/input-number';
|
||||
declare module 'element-ui/lib/select';
|
||||
declare module 'element-ui/lib/option';
|
||||
declare module 'element-ui/lib/menu';
|
||||
declare module 'element-ui/lib/menu-item';
|
||||
|
||||
|
|
|
@ -53,7 +53,7 @@
|
|||
);
|
||||
|
||||
--color-success-h: 150.4;
|
||||
--color-success-s: 73.8%;
|
||||
--color-success-s: 60%;
|
||||
--color-success-l: 40.4%;
|
||||
--color-success: hsl(
|
||||
var(--color-success-h),
|
||||
|
|
|
@ -5,7 +5,6 @@
|
|||
|
||||
@include mixins.b(checkbox) {
|
||||
color: var.$checkbox-font-color;
|
||||
font-weight: var.$checkbox-font-weight;
|
||||
font-size: var.$font-size-base;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
|
@ -156,10 +155,6 @@
|
|||
transform: rotate(45deg) scaleY(1);
|
||||
}
|
||||
}
|
||||
|
||||
& + .el-checkbox__label {
|
||||
color: var.$checkbox-checked-font-color;
|
||||
}
|
||||
}
|
||||
@include mixins.when(focus) {
|
||||
/*focus时 视觉上区分*/
|
||||
|
@ -253,7 +248,6 @@
|
|||
@include mixins.e(inner) {
|
||||
display: inline-block;
|
||||
line-height: 1;
|
||||
font-weight: var.$checkbox-font-weight;
|
||||
white-space: nowrap;
|
||||
vertical-align: middle;
|
||||
cursor: pointer;
|
||||
|
|
|
@ -12,16 +12,20 @@
|
|||
@keyframes v-modal-in {
|
||||
0% {
|
||||
opacity: 0;
|
||||
backdrop-filter: blur(4px) opacity(0);
|
||||
}
|
||||
100% {
|
||||
backdrop-filter: blur(4px) opacity(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes v-modal-out {
|
||||
0% {
|
||||
backdrop-filter: blur(4px) opacity(1);
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
backdrop-filter: blur(4px) opacity(0);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -31,8 +35,8 @@
|
|||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
opacity: var.$popup-modal-opacity;
|
||||
background: var.$popup-modal-background-color;
|
||||
background-color: var.$popup-modal-background-color;
|
||||
backdrop-filter: blur(4px) opacity(1);
|
||||
}
|
||||
|
||||
@include mixins.b(popup-parent) {
|
||||
|
|
|
@ -277,10 +277,8 @@ $icon-color-base: var(--color-info);
|
|||
-------------------------- */
|
||||
/// fontSize||Font|1
|
||||
$checkbox-font-size: 14px;
|
||||
/// fontWeight||Font|1
|
||||
$checkbox-font-weight: $font-weight-primary;
|
||||
/// color||Color|0
|
||||
$checkbox-font-color: var(--color-text-dark);
|
||||
$checkbox-font-color: var(--color-text-base);
|
||||
$checkbox-input-height: 14px;
|
||||
$checkbox-input-width: 14px;
|
||||
/// borderRadius||Border|2
|
||||
|
@ -761,7 +759,8 @@ $dialog-content-font-size: 14px;
|
|||
/// fontLineHeight||LineHeight|2
|
||||
$dialog-font-line-height: $font-line-height-primary;
|
||||
/// padding||Spacing|3
|
||||
$dialog-padding-primary: 20px;
|
||||
$dialog-padding-primary: var(--spacing-l);
|
||||
$dialog-close-top: var(--dialog-close-top, var(--spacing-l));
|
||||
|
||||
/* Table
|
||||
-------------------------- */
|
||||
|
@ -804,9 +803,9 @@ $pagination-hover-color: var(--color-primary);
|
|||
/* Popup
|
||||
-------------------------- */
|
||||
/// color||Color|0
|
||||
$popup-modal-background-color: $color-black;
|
||||
$popup-modal-background-color: hsla(247,14%, 70%, 0.75);
|
||||
/// opacity||Other|1
|
||||
$popup-modal-opacity: 0.5;
|
||||
$popup-modal-opacity: 0.65;
|
||||
|
||||
/* Popover
|
||||
-------------------------- */
|
||||
|
@ -852,9 +851,9 @@ $tag-warning-color: var(--color-warning);
|
|||
/// color||Color|0
|
||||
$tag-danger-color: var(--color-danger);
|
||||
/// fontSize||Font|1
|
||||
$tag-font-size: 12px;
|
||||
$tag-font-size: var(--font-size-s);
|
||||
$tag-border-radius: 4px;
|
||||
$tag-padding: 0 10px;
|
||||
$tag-padding: 16px;
|
||||
|
||||
/* Tree
|
||||
-------------------------- */
|
||||
|
@ -924,11 +923,17 @@ $steps-padding: 20px;
|
|||
--------------------------*/
|
||||
/// fontSize||Font|1
|
||||
$menu-item-font-size: $font-size-base;
|
||||
$menu-item-font-weight: var(--menu-item-font-color, 300);
|
||||
/// color||Color|0
|
||||
$menu-item-font-color: var(--color-text-dark);
|
||||
$menu-item-font-color: var(--menu-font-color, var(--color-text-dark));
|
||||
/// color||Color|0
|
||||
$menu-background-color: $color-white;
|
||||
$menu-item-hover-fill: $color-primary-light-9;
|
||||
$menu-background-color: var(--menu-background-color, var(--color-background-xlight));
|
||||
$menu-item-hover-fill: var(--menu-item-hover-fill, transparent);
|
||||
$menu-item-hover-font-color: var(--menu-item-hover-font-color, var(--color-text-dark));
|
||||
$menu-item-active-font-color: var(--menu-item-active-font-color, var(--color-primary));
|
||||
$menu-item-active-background-color: var(--menu-item-active-background-color, transparent);
|
||||
$menu-item-border-radius: var(--menu-item-border-radius, 0);
|
||||
$menu-item-height: var(--menu-item-height, 56px);
|
||||
|
||||
/* Rate
|
||||
--------------------------*/
|
||||
|
|
|
@ -32,12 +32,12 @@
|
|||
|
||||
@include mixins.e(header) {
|
||||
padding: var.$dialog-padding-primary;
|
||||
padding-bottom: 10px;
|
||||
padding-bottom: 0px;
|
||||
}
|
||||
|
||||
@include mixins.e(headerbtn) {
|
||||
position: absolute;
|
||||
top: var.$dialog-padding-primary;
|
||||
top: var.$dialog-close-top;
|
||||
right: var.$dialog-padding-primary;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
|
@ -65,8 +65,8 @@
|
|||
}
|
||||
|
||||
@include mixins.e(body) {
|
||||
padding: (var.$dialog-padding-primary + 10px) var.$dialog-padding-primary;
|
||||
color: var(--color-text-dark);
|
||||
padding: var.$dialog-padding-primary;
|
||||
color: var(--color-text-base);
|
||||
font-size: var.$dialog-content-font-size;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
|
|
@ -113,8 +113,7 @@ $directions: rtl, ltr, ttb, btt;
|
|||
background-color: var.$dialog-background-color;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-shadow: 0 8px 10px -5px rgba(0, 0, 0, 0.2),
|
||||
0 16px 24px 2px rgba(0, 0, 0, 0.14), 0 6px 30px 5px rgba(0, 0, 0, 0.12);
|
||||
box-shadow: 0px 6px 16px rgb(68 28 23 / 6%);
|
||||
overflow: hidden;
|
||||
outline: 0;
|
||||
|
||||
|
@ -183,10 +182,12 @@ $directions: rtl, ltr, ttb, btt;
|
|||
|
||||
&.ltr {
|
||||
left: 0;
|
||||
border-right: var(--border-base);
|
||||
}
|
||||
|
||||
&.rtl {
|
||||
right: 0;
|
||||
border-left: var(--border-base);
|
||||
}
|
||||
|
||||
&.ttb {
|
||||
|
|
|
@ -4,10 +4,11 @@
|
|||
@use "common/transition";
|
||||
|
||||
@mixin menu-item {
|
||||
height: 56px;
|
||||
line-height: 56px;
|
||||
height: var.$menu-item-height;
|
||||
line-height: var.$menu-item-height;
|
||||
font-size: var.$menu-item-font-size;
|
||||
color: var.$menu-item-font-color;
|
||||
font-weight: var.$menu-item-font-weight;
|
||||
padding: 0 20px;
|
||||
list-style: none;
|
||||
cursor: pointer;
|
||||
|
@ -15,10 +16,7 @@
|
|||
transition: border-color 0.3s, background-color 0.3s, color 0.3s;
|
||||
box-sizing: border-box;
|
||||
white-space: nowrap;
|
||||
|
||||
* {
|
||||
vertical-align: middle;
|
||||
}
|
||||
border-radius: var.$menu-item-border-radius;
|
||||
|
||||
i {
|
||||
color: var(--color-text-light);
|
||||
|
@ -28,6 +26,7 @@
|
|||
&:focus {
|
||||
outline: none;
|
||||
background-color: var.$menu-item-hover-fill;
|
||||
color: var.$menu-item-hover-font-color;
|
||||
}
|
||||
|
||||
@include mixins.when(disabled) {
|
||||
|
@ -38,7 +37,6 @@
|
|||
}
|
||||
|
||||
@include mixins.b(menu) {
|
||||
border-right: solid 1px #e6e6e6;
|
||||
list-style: none;
|
||||
position: relative;
|
||||
margin: 0;
|
||||
|
@ -208,7 +206,9 @@
|
|||
vertical-align: middle;
|
||||
}
|
||||
@include mixins.when(active) {
|
||||
color: var(--color-primary);
|
||||
color: var.$menu-item-active-font-color;
|
||||
background-color: var.$menu-item-active-background-color;
|
||||
|
||||
i {
|
||||
color: inherit;
|
||||
}
|
||||
|
|
|
@ -226,7 +226,7 @@ hr {
|
|||
height: 1px;
|
||||
border: 0;
|
||||
border-top: 1px solid var(--color-foreground-light);
|
||||
margin: 1em 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
background-color: var.$color-primary-lighter;
|
||||
border-color: var.$color-primary-light-5;
|
||||
color: var.$color-primary-light-1;
|
||||
font-weight: var(--font-weight-regular);
|
||||
|
||||
@include mixins.when(hit) {
|
||||
border-color: var.$tag-primary-color;
|
||||
|
@ -37,9 +38,9 @@
|
|||
}
|
||||
|
||||
&.el-tag--success {
|
||||
background-color: var.$color-success-lighter;
|
||||
border-color: var.$color-success-light-5;
|
||||
color: var.$color-success-light-3;
|
||||
background-color: var(--color-success-tint-2);
|
||||
border-color: var(--color-success-tint-1);
|
||||
color: var(--color-success);
|
||||
|
||||
@include mixins.when(hit) {
|
||||
border-color: var.$tag-success-color;
|
||||
|
@ -94,9 +95,7 @@
|
|||
@include mixins.b(tag) {
|
||||
@include genTheme();
|
||||
display: inline-block;
|
||||
height: 32px;
|
||||
padding: var.$tag-padding;
|
||||
line-height: 30px;
|
||||
font-size: var.$tag-font-size;
|
||||
color: var.$tag-primary-color;
|
||||
border-width: 1px;
|
||||
|
@ -132,8 +131,7 @@
|
|||
}
|
||||
|
||||
@include mixins.m(medium) {
|
||||
height: 28px;
|
||||
line-height: 26px;
|
||||
padding: 12px;
|
||||
|
||||
.el-icon-close {
|
||||
transform: scale(0.8);
|
||||
|
|
Before Width: | Height: | Size: 7.9 KiB After Width: | Height: | Size: 7.9 KiB |
|
@ -140,19 +140,10 @@ export interface IRestApi {
|
|||
getWorkflow(id: string): Promise<IWorkflowDb>;
|
||||
getWorkflows(filter?: object): Promise<IWorkflowShortResponse[]>;
|
||||
getWorkflowFromUrl(url: string): Promise<IWorkflowDb>;
|
||||
createNewCredentials(sendData: ICredentialsDecrypted): Promise<ICredentialsResponse>;
|
||||
deleteCredentials(id: string): Promise<void>;
|
||||
updateCredentials(id: string, data: ICredentialsDecrypted): Promise<ICredentialsResponse>;
|
||||
getAllCredentials(filter?: object): Promise<ICredentialsResponse[]>;
|
||||
getCredentials(id: string, includeData?: boolean): Promise<ICredentialsDecryptedResponse | ICredentialsResponse | undefined>;
|
||||
getCredentialTypes(): Promise<ICredentialType[]>;
|
||||
getExecution(id: string): Promise<IExecutionResponse>;
|
||||
deleteExecutions(sendData: IExecutionDeleteFilter): Promise<void>;
|
||||
retryExecution(id: string, loadWorkflow?: boolean): Promise<boolean>;
|
||||
getTimezones(): Promise<IDataObject>;
|
||||
oAuth1CredentialAuthorize(sendData: ICredentialsResponse): Promise<string>;
|
||||
oAuth2CredentialAuthorize(sendData: ICredentialsResponse): Promise<string>;
|
||||
oAuth2Callback(code: string, state: string): Promise<string>;
|
||||
}
|
||||
|
||||
export interface IBinaryDisplayData {
|
||||
|
@ -163,13 +154,6 @@ export interface IBinaryDisplayData {
|
|||
runIndex: number;
|
||||
}
|
||||
|
||||
export interface ICredentialsCreatedEvent {
|
||||
data: ICredentialsDecryptedResponse;
|
||||
options: {
|
||||
closeDialog: boolean,
|
||||
};
|
||||
}
|
||||
|
||||
export interface IStartRunData {
|
||||
workflowData: IWorkflowData;
|
||||
startNodes?: string[];
|
||||
|
@ -585,8 +569,6 @@ export interface IRootState {
|
|||
activeActions: string[];
|
||||
activeNode: string | null;
|
||||
baseUrl: string;
|
||||
credentials: ICredentialsResponse[] | null;
|
||||
credentialTypes: ICredentialType[] | null;
|
||||
endpointWebhook: string;
|
||||
endpointWebhookTest: string;
|
||||
executionId: string | null;
|
||||
|
@ -618,6 +600,19 @@ export interface IRootState {
|
|||
instanceId: string;
|
||||
}
|
||||
|
||||
export interface ICredentialTypeMap {
|
||||
[name: string]: ICredentialType;
|
||||
}
|
||||
|
||||
export interface ICredentialMap {
|
||||
[name: string]: ICredentialsResponse;
|
||||
}
|
||||
|
||||
export interface ICredentialsState {
|
||||
credentialTypes: ICredentialTypeMap;
|
||||
credentials: ICredentialMap;
|
||||
}
|
||||
|
||||
export interface ITagsState {
|
||||
tags: { [id: string]: ITag };
|
||||
isLoading: boolean;
|
||||
|
@ -627,6 +622,8 @@ export interface ITagsState {
|
|||
|
||||
export interface IModalState {
|
||||
open: boolean;
|
||||
mode?: string | null;
|
||||
activeId?: string | null;
|
||||
}
|
||||
|
||||
export interface IUiState {
|
||||
|
|
53
packages/editor-ui/src/api/credentials.ts
Normal file
53
packages/editor-ui/src/api/credentials.ts
Normal file
|
@ -0,0 +1,53 @@
|
|||
import { ICredentialsDecryptedResponse, ICredentialsResponse, IRestApiContext } from '@/Interface';
|
||||
import { makeRestApiRequest } from './helpers';
|
||||
import {
|
||||
ICredentialsDecrypted,
|
||||
ICredentialType,
|
||||
IDataObject,
|
||||
NodeCredentialTestRequest,
|
||||
NodeCredentialTestResult,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
export async function getCredentialTypes(context: IRestApiContext): Promise<ICredentialType[]> {
|
||||
return await makeRestApiRequest(context, 'GET', '/credential-types');
|
||||
}
|
||||
|
||||
export async function getCredentialsNewName(context: IRestApiContext, name?: string): Promise<{name: string}> {
|
||||
return await makeRestApiRequest(context, 'GET', '/credentials/new', name ? { name } : {});
|
||||
}
|
||||
|
||||
export async function getAllCredentials(context: IRestApiContext): Promise<ICredentialType[]> {
|
||||
return await makeRestApiRequest(context, 'GET', '/credentials');
|
||||
}
|
||||
|
||||
export async function createNewCredential(context: IRestApiContext, data: ICredentialsDecrypted): Promise<ICredentialsResponse> {
|
||||
return makeRestApiRequest(context, 'POST', `/credentials`, data as unknown as IDataObject);
|
||||
}
|
||||
|
||||
export async function deleteCredential(context: IRestApiContext, id: string): Promise<boolean> {
|
||||
return makeRestApiRequest(context, 'DELETE', `/credentials/${id}`);
|
||||
}
|
||||
|
||||
export async function updateCredential(context: IRestApiContext, id: string, data: ICredentialsDecrypted): Promise<ICredentialsResponse> {
|
||||
return makeRestApiRequest(context, 'PATCH', `/credentials/${id}`, data as unknown as IDataObject);
|
||||
}
|
||||
|
||||
export async function getCredentialData(context: IRestApiContext, id: string): Promise<ICredentialsDecryptedResponse | ICredentialsResponse | undefined> {
|
||||
return makeRestApiRequest(context, 'GET', `/credentials/${id}`, {
|
||||
includeData: true,
|
||||
});
|
||||
}
|
||||
|
||||
// Get OAuth1 Authorization URL using the stored credentials
|
||||
export async function oAuth1CredentialAuthorize(context: IRestApiContext, data: ICredentialsResponse): Promise<string> {
|
||||
return makeRestApiRequest(context, 'GET', `/oauth1-credential/auth`, data as unknown as IDataObject);
|
||||
}
|
||||
|
||||
// Get OAuth2 Authorization URL using the stored credentials
|
||||
export async function oAuth2CredentialAuthorize(context: IRestApiContext, data: ICredentialsResponse): Promise<string> {
|
||||
return makeRestApiRequest(context, 'GET', `/oauth2-credential/auth`, data as unknown as IDataObject);
|
||||
}
|
||||
|
||||
export async function testCredential(context: IRestApiContext, data: NodeCredentialTestRequest): Promise<NodeCredentialTestResult> {
|
||||
return makeRestApiRequest(context, 'POST', '/credentials-test', data as unknown as IDataObject);
|
||||
}
|
147
packages/editor-ui/src/components/Banner.vue
Normal file
147
packages/editor-ui/src/components/Banner.vue
Normal file
|
@ -0,0 +1,147 @@
|
|||
<template>
|
||||
<el-tag
|
||||
:type="theme"
|
||||
size="medium"
|
||||
:disable-transitions="true"
|
||||
:class="$style.container"
|
||||
>
|
||||
<font-awesome-icon
|
||||
:icon="theme === 'success' ? 'check-circle' : 'exclamation-triangle'"
|
||||
:class="theme === 'success' ? $style.icon : $style.dangerIcon"
|
||||
/>
|
||||
<div
|
||||
:class="$style.banner"
|
||||
>
|
||||
<div :class="$style.content">
|
||||
<div>
|
||||
<span
|
||||
:class="theme === 'success' ? $style.message : $style.dangerMessage"
|
||||
>
|
||||
{{ message }}
|
||||
</span>
|
||||
<a v-if="details && !expanded" :class="$style.expandButton" @click="expand">More details</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<n8n-button
|
||||
v-if="buttonLabel"
|
||||
:label="buttonLoading && buttonLoadingLabel ? buttonLoadingLabel : buttonLabel"
|
||||
:title="buttonTitle"
|
||||
:theme="theme"
|
||||
:loading="buttonLoading"
|
||||
size="small"
|
||||
type="outline"
|
||||
:transparentBackground="true"
|
||||
@click.stop="onClick"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-if="expanded" :class="$style.details">
|
||||
{{details}}
|
||||
</div>
|
||||
</el-tag>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'Banner',
|
||||
data() {
|
||||
return {
|
||||
expanded: false,
|
||||
};
|
||||
},
|
||||
props: {
|
||||
theme: {
|
||||
type: String,
|
||||
validator: (value: string): boolean =>
|
||||
['success', 'danger'].indexOf(value) !== -1,
|
||||
},
|
||||
message: {
|
||||
type: String,
|
||||
},
|
||||
buttonLabel: {
|
||||
type: String,
|
||||
},
|
||||
buttonLoadingLabel: {
|
||||
type: String,
|
||||
},
|
||||
buttonTitle: {
|
||||
type: String,
|
||||
},
|
||||
details: {
|
||||
type: String,
|
||||
},
|
||||
buttonLoading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
expand() {
|
||||
this.expanded = true;
|
||||
},
|
||||
onClick() {
|
||||
this.expanded = false;
|
||||
this.$emit('click');
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style module lang="scss">
|
||||
.icon {
|
||||
position: absolute;
|
||||
left: 14px;
|
||||
top: 18px;
|
||||
}
|
||||
|
||||
.dangerIcon {
|
||||
composes: icon;
|
||||
color: var(--color-danger);
|
||||
}
|
||||
|
||||
.container {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
padding-left: 40px;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.message {
|
||||
white-space: normal;
|
||||
line-height: var(--font-line-height-regular);
|
||||
overflow: hidden;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.dangerMessage {
|
||||
composes: message;
|
||||
color: var(--color-danger);
|
||||
}
|
||||
|
||||
.banner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.content {
|
||||
flex-grow: 1;
|
||||
min-height: 26px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.expandButton {
|
||||
font-weight: var(--font-weight-bold);
|
||||
}
|
||||
|
||||
.details {
|
||||
composes: message;
|
||||
margin-top: var(--spacing-3xs);
|
||||
color: var(--color-text-base);
|
||||
font-size: var(--font-size-2xs);
|
||||
}
|
||||
|
||||
</style>
|
96
packages/editor-ui/src/components/CopyInput.vue
Normal file
96
packages/editor-ui/src/components/CopyInput.vue
Normal file
|
@ -0,0 +1,96 @@
|
|||
<template>
|
||||
<div>
|
||||
<n8n-input-label :label="label">
|
||||
<div :class="$style.copyText" @click="copy">
|
||||
<span>{{ copyContent }}</span>
|
||||
<div :class="$style.copyButton"><span>{{ copyButtonText }}</span></div>
|
||||
</div>
|
||||
</n8n-input-label>
|
||||
<div :class="$style.subtitle">{{ subtitle }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import mixins from 'vue-typed-mixins';
|
||||
import { copyPaste } from './mixins/copyPaste';
|
||||
import { showMessage } from './mixins/showMessage';
|
||||
|
||||
export default mixins(copyPaste, showMessage).extend({
|
||||
props: {
|
||||
label: {
|
||||
type: String,
|
||||
},
|
||||
subtitle: {
|
||||
type: String,
|
||||
},
|
||||
copyContent: {
|
||||
type: String,
|
||||
},
|
||||
copyButtonText: {
|
||||
type: String,
|
||||
},
|
||||
successMessage: {
|
||||
type: String,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
copy(): void {
|
||||
this.copyToClipboard(this.$props.copyContent);
|
||||
|
||||
this.$showMessage({
|
||||
title: 'Copied',
|
||||
message: this.$props.successMessage,
|
||||
type: 'success',
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
|
||||
.copyText {
|
||||
span {
|
||||
font-family: Monaco, Consolas;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
padding: var(--spacing-xs);
|
||||
background-color: var(--color-background-light);
|
||||
border: var(--border-base);
|
||||
border-radius: var(--border-radius-base);
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
font-weight: var(--font-weight-regular);
|
||||
|
||||
&:hover {
|
||||
--display-copy-button: flex;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.copyButton {
|
||||
display: var(--display-copy-button, none);
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
padding: var(--spacing-xs);
|
||||
background-color: var(--color-background-light);
|
||||
height: 100%;
|
||||
align-items: center;
|
||||
border-radius: var(--border-radius-base);
|
||||
|
||||
span {
|
||||
font-family: unset;
|
||||
}
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
margin-top: var(--spacing-2xs);
|
||||
font-size: var(--font-size-2xs);
|
||||
line-height: var(--font-line-height-loose);
|
||||
font-weight: var(--font-weight-regular);
|
||||
word-break: normal;
|
||||
}
|
||||
|
||||
</style>
|
|
@ -0,0 +1,189 @@
|
|||
<template>
|
||||
<div :class="$style.container">
|
||||
<banner
|
||||
v-show="showValidationWarning"
|
||||
theme="danger"
|
||||
message="Please check the errors below"
|
||||
/>
|
||||
|
||||
<banner
|
||||
v-if="authError && !showValidationWarning"
|
||||
theme="danger"
|
||||
message="Couldn’t connect with these settings"
|
||||
:details="authError"
|
||||
buttonLabel="Retry"
|
||||
buttonLoadingLabel="Retrying"
|
||||
buttonTitle="Retry credentials test"
|
||||
:buttonLoading="isRetesting"
|
||||
@click="$emit('retest')"
|
||||
/>
|
||||
|
||||
<banner
|
||||
v-show="showOAuthSuccessBanner && !showValidationWarning"
|
||||
theme="success"
|
||||
message="Account connected"
|
||||
buttonLabel="Reconnect"
|
||||
buttonTitle="Reconnect OAuth Credentials"
|
||||
@click="$emit('oauth')"
|
||||
/>
|
||||
|
||||
<banner
|
||||
v-show="testedSuccessfully && !showValidationWarning"
|
||||
theme="success"
|
||||
message="Connection tested successfully"
|
||||
buttonLabel="Retry"
|
||||
buttonLoadingLabel="Retrying"
|
||||
buttonTitle="Retry credentials test"
|
||||
:buttonLoading="isRetesting"
|
||||
@click="$emit('retest')"
|
||||
/>
|
||||
|
||||
<n8n-info-tip v-if="documentationUrl && credentialProperties.length">
|
||||
Need help filling out these fields?
|
||||
<a :href="documentationUrl" target="_blank">Open docs</a>
|
||||
</n8n-info-tip>
|
||||
|
||||
<CopyInput
|
||||
v-if="isOAuthType && credentialProperties.length"
|
||||
label="OAuth Redirect URL"
|
||||
:copyContent="oAuthCallbackUrl"
|
||||
copyButtonText="Click to copy"
|
||||
:subtitle="`In ${appName}, use the URL above when prompted to enter an OAuth callback or redirect URL`"
|
||||
successMessage="Redirect URL copied to clipboard"
|
||||
/>
|
||||
|
||||
<CredentialInputs
|
||||
v-if="credentialType"
|
||||
:credentialData="credentialData"
|
||||
:credentialProperties="credentialProperties"
|
||||
:documentationUrl="documentationUrl"
|
||||
:showValidationWarnings="showValidationWarning"
|
||||
@change="onDataChange"
|
||||
/>
|
||||
|
||||
<OauthButton
|
||||
v-if="isOAuthType && requiredPropertiesFilled && !isOAuthConnected"
|
||||
:isGoogleOAuthType="isGoogleOAuthType"
|
||||
@click="$emit('oauth')"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { ICredentialType } from 'n8n-workflow';
|
||||
import { getAppNameFromCredType } from '../helpers';
|
||||
|
||||
import Vue from 'vue';
|
||||
import Banner from '../Banner.vue';
|
||||
import CopyInput from '../CopyInput.vue';
|
||||
import CredentialInputs from './CredentialInputs.vue';
|
||||
import OauthButton from './OauthButton.vue';
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'CredentialConfig',
|
||||
components: {
|
||||
Banner,
|
||||
CopyInput,
|
||||
CredentialInputs,
|
||||
OauthButton,
|
||||
},
|
||||
props: {
|
||||
credentialType: {
|
||||
},
|
||||
credentialProperties: {
|
||||
type: Array,
|
||||
},
|
||||
parentTypes: {
|
||||
type: Array,
|
||||
},
|
||||
credentialData: {
|
||||
},
|
||||
showValidationWarning: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
authError: {
|
||||
type: String,
|
||||
},
|
||||
testedSuccessfully: {
|
||||
type: Boolean,
|
||||
},
|
||||
isOAuthType: {
|
||||
type: Boolean,
|
||||
},
|
||||
isOAuthConnected: {
|
||||
type: Boolean,
|
||||
},
|
||||
isRetesting: {
|
||||
type: Boolean,
|
||||
},
|
||||
requiredPropertiesFilled: {
|
||||
type: Boolean,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
appName(): string {
|
||||
if (!this.credentialType) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const appName = getAppNameFromCredType(
|
||||
(this.credentialType as ICredentialType).displayName,
|
||||
);
|
||||
|
||||
return appName || "the service you're connecting to";
|
||||
},
|
||||
credentialTypeName(): string {
|
||||
return (this.credentialType as ICredentialType).name;
|
||||
},
|
||||
documentationUrl(): string {
|
||||
const type = this.credentialType as ICredentialType;
|
||||
|
||||
if (!type || !type.documentationUrl) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (type.documentationUrl.startsWith('https://') || type.documentationUrl.startsWith('http://')) {
|
||||
return type.documentationUrl;
|
||||
}
|
||||
|
||||
return `https://docs.n8n.io/credentials/${type.documentationUrl}/?utm_source=n8n_app&utm_medium=left_nav_menu&utm_campaign=create_new_credentials_modal`;
|
||||
},
|
||||
isGoogleOAuthType(): boolean {
|
||||
return this.credentialTypeName === 'googleOAuth2Api' || this.parentTypes.includes('googleOAuth2Api');
|
||||
},
|
||||
oAuthCallbackUrl(): string {
|
||||
const oauthType =
|
||||
this.credentialTypeName === 'oAuth2Api' ||
|
||||
this.parentTypes.includes('oAuth2Api')
|
||||
? 'oauth2'
|
||||
: 'oauth1';
|
||||
return this.$store.getters.oauthCallbackUrls[oauthType];
|
||||
},
|
||||
showOAuthSuccessBanner(): boolean {
|
||||
return this.isOAuthType && this.requiredPropertiesFilled && this.isOAuthConnected && !this.authError;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onDataChange(event: { name: string; value: string | number | boolean | Date | null }): void {
|
||||
this.$emit('change', event);
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
showOAuthSuccessBanner(newValue, oldValue) {
|
||||
if (newValue && !oldValue) {
|
||||
this.$emit('scrollToTop');
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.container {
|
||||
> * {
|
||||
margin-bottom: var(--spacing-l);
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
|
@ -0,0 +1,853 @@
|
|||
<template>
|
||||
<Modal
|
||||
:name="modalName"
|
||||
size="lg"
|
||||
:customClass="$style.credentialModal"
|
||||
:eventBus="modalBus"
|
||||
:loading="loading"
|
||||
:beforeClose="beforeClose"
|
||||
>
|
||||
<template slot="header">
|
||||
<div v-if="credentialType" :class="$style.header">
|
||||
<div :class="$style.credInfo">
|
||||
<div :class="$style.credIcon">
|
||||
<CredentialIcon :credentialTypeName="credentialTypeName" />
|
||||
</div>
|
||||
<InlineNameEdit
|
||||
:name="credentialName"
|
||||
:subtitle="credentialType.displayName"
|
||||
type="Credential"
|
||||
@input="onNameEdit"
|
||||
/>
|
||||
</div>
|
||||
<div :class="$style.credActions">
|
||||
<n8n-icon-button
|
||||
v-if="currentCredential"
|
||||
size="medium"
|
||||
title="Delete"
|
||||
icon="trash"
|
||||
type="text"
|
||||
:disabled="isSaving"
|
||||
:loading="isDeleting"
|
||||
@click="deleteCredential"
|
||||
/>
|
||||
<SaveButton
|
||||
v-if="hasUnsavedChanges || credentialId"
|
||||
:saved="!hasUnsavedChanges && !isTesting"
|
||||
:isSaving="isSaving || isTesting"
|
||||
:savingLabel="isTesting ? 'Testing' : 'Saving'"
|
||||
@click="saveCredential"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
</template>
|
||||
<template slot="content">
|
||||
<div :class="$style.container">
|
||||
<div :class="$style.sidebar">
|
||||
<n8n-menu
|
||||
type="secondary"
|
||||
@select="onTabSelect"
|
||||
defaultActive="connection"
|
||||
:light="true"
|
||||
>
|
||||
<n8n-menu-item index="connection" :class="$style.credTab"
|
||||
><span slot="title">Connection</span></n8n-menu-item
|
||||
>
|
||||
<n8n-menu-item index="details" :class="$style.credTab"
|
||||
><span slot="title">Details</span></n8n-menu-item
|
||||
>
|
||||
</n8n-menu>
|
||||
</div>
|
||||
<div v-if="activeTab === 'connection'" :class="$style.mainContent" ref="content">
|
||||
<CredentialConfig
|
||||
:credentialType="credentialType"
|
||||
:credentialProperties="credentialProperties"
|
||||
:credentialData="credentialData"
|
||||
:showValidationWarning="showValidationWarning"
|
||||
:authError="authError"
|
||||
:testedSuccessfully="testedSuccessfully"
|
||||
:isOAuthType="isOAuthType"
|
||||
:isOAuthConnected="isOAuthConnected"
|
||||
:isRetesting="isRetesting"
|
||||
:parentTypes="parentTypes"
|
||||
:requiredPropertiesFilled="requiredPropertiesFilled"
|
||||
@change="onDataChange"
|
||||
@oauth="oAuthCredentialAuthorize"
|
||||
@retest="retestCredential"
|
||||
@scrollToTop="scrollToTop"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="activeTab === 'details'" :class="$style.mainContent">
|
||||
<CredentialInfo
|
||||
:nodeAccess="nodeAccess"
|
||||
:nodesWithAccess="nodesWithAccess"
|
||||
:currentCredential="currentCredential"
|
||||
@accessChange="onNodeAccessChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
|
||||
import {
|
||||
ICredentialsDecryptedResponse,
|
||||
ICredentialsResponse,
|
||||
} from '@/Interface';
|
||||
|
||||
import {
|
||||
CredentialInformation,
|
||||
ICredentialDataDecryptedObject,
|
||||
ICredentialNodeAccess,
|
||||
ICredentialsDecrypted,
|
||||
ICredentialType,
|
||||
INodeParameters,
|
||||
INodeProperties,
|
||||
INodeTypeDescription,
|
||||
NodeCredentialTestResult,
|
||||
NodeHelpers,
|
||||
} from 'n8n-workflow';
|
||||
import CredentialIcon from '../CredentialIcon.vue';
|
||||
|
||||
import mixins from 'vue-typed-mixins';
|
||||
import { nodeHelpers } from '../mixins/nodeHelpers';
|
||||
import { showMessage } from '../mixins/showMessage';
|
||||
|
||||
import CredentialConfig from './CredentialConfig.vue';
|
||||
import CredentialInfo from './CredentialInfo.vue';
|
||||
import SaveButton from '../SaveButton.vue';
|
||||
import Modal from '../Modal.vue';
|
||||
import InlineNameEdit from '../InlineNameEdit.vue';
|
||||
|
||||
interface NodeAccessMap {
|
||||
[nodeType: string]: ICredentialNodeAccess | null;
|
||||
}
|
||||
|
||||
export default mixins(showMessage, nodeHelpers).extend({
|
||||
name: 'CredentialsDetail',
|
||||
components: {
|
||||
CredentialConfig,
|
||||
CredentialIcon,
|
||||
CredentialInfo,
|
||||
InlineNameEdit,
|
||||
Modal,
|
||||
SaveButton,
|
||||
},
|
||||
props: {
|
||||
modalName: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
activeId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
mode: {
|
||||
type: String,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
activeTab: 'connection',
|
||||
authError: '',
|
||||
credentialId: '',
|
||||
credentialName: '',
|
||||
credentialData: {} as ICredentialDataDecryptedObject,
|
||||
modalBus: new Vue(),
|
||||
nodeAccess: {} as NodeAccessMap,
|
||||
isDeleting: false,
|
||||
isSaving: false,
|
||||
isTesting: false,
|
||||
hasUnsavedChanges: false,
|
||||
loading: true,
|
||||
showValidationWarning: false,
|
||||
testedSuccessfully: false,
|
||||
isRetesting: false,
|
||||
};
|
||||
},
|
||||
async mounted() {
|
||||
this.nodeAccess = this.nodesWithAccess.reduce(
|
||||
(accu: NodeAccessMap, node: { name: string }) => {
|
||||
if (this.mode === 'new') {
|
||||
accu[node.name] = { nodeType: node.name }; // enable all nodes by default
|
||||
} else {
|
||||
accu[node.name] = null;
|
||||
}
|
||||
|
||||
return accu;
|
||||
},
|
||||
{},
|
||||
);
|
||||
|
||||
if (this.mode === 'new') {
|
||||
this.credentialName = await this.$store.dispatch(
|
||||
'credentials/getNewCredentialName',
|
||||
{ credentialTypeName: this.credentialTypeName },
|
||||
);
|
||||
} else {
|
||||
await this.loadCurrentCredential();
|
||||
}
|
||||
|
||||
if (this.credentialType) {
|
||||
for (const property of this.credentialType.properties) {
|
||||
if (!this.credentialData.hasOwnProperty(property.name)) {
|
||||
this.credentialData[property.name] =
|
||||
property.default as CredentialInformation;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this.credentialId) {
|
||||
if (!this.requiredPropertiesFilled) {
|
||||
this.showValidationWarning = true;
|
||||
}
|
||||
else {
|
||||
this.retestCredential();
|
||||
}
|
||||
}
|
||||
|
||||
this.loading = false;
|
||||
},
|
||||
computed: {
|
||||
currentCredential(): ICredentialsResponse | null {
|
||||
if (!this.credentialId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.$store.getters['credentials/getCredentialById'](
|
||||
this.credentialId,
|
||||
);
|
||||
},
|
||||
credentialTypeName(): string | null {
|
||||
if (this.mode === 'edit') {
|
||||
if (this.currentCredential) {
|
||||
return this.currentCredential.type;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.activeId;
|
||||
},
|
||||
credentialType(): ICredentialType | null {
|
||||
if (!this.credentialTypeName) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const type = this.$store.getters['credentials/getCredentialTypeByName'](
|
||||
this.credentialTypeName,
|
||||
);
|
||||
|
||||
return {
|
||||
...type,
|
||||
properties: this.getCredentialProperties(this.credentialTypeName),
|
||||
};
|
||||
},
|
||||
isCredentialTestable (): boolean {
|
||||
if (this.isOAuthType || !this.requiredPropertiesFilled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const hasExpressions = Object.values(this.credentialData).reduce((accu: boolean, value: CredentialInformation) => accu || (typeof value === 'string' && value.startsWith('=')), false);
|
||||
if (hasExpressions) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const nodesThatCanTest = this.nodesWithAccess.filter(node => {
|
||||
if (node.credentials) {
|
||||
// Returns a list of nodes that can test this credentials
|
||||
const eligibleTesters = node.credentials.filter(credential => {
|
||||
return credential.name === this.credentialTypeName && credential.testedBy;
|
||||
});
|
||||
// If we have any node that can test, return true.
|
||||
return !!eligibleTesters.length;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
return !!nodesThatCanTest.length;
|
||||
},
|
||||
nodesWithAccess(): INodeTypeDescription[] {
|
||||
if (this.credentialTypeName) {
|
||||
return this.$store.getters['credentials/getNodesWithAccess'](
|
||||
this.credentialTypeName,
|
||||
);
|
||||
}
|
||||
|
||||
return [];
|
||||
},
|
||||
parentTypes(): string[] {
|
||||
if (this.credentialTypeName) {
|
||||
return this.getParentTypes(this.credentialTypeName);
|
||||
}
|
||||
|
||||
return [];
|
||||
},
|
||||
isOAuthType(): boolean {
|
||||
return !!this.credentialTypeName && (
|
||||
['oAuth1Api', 'oAuth2Api'].includes(this.credentialTypeName) ||
|
||||
this.parentTypes.includes('oAuth1Api') ||
|
||||
this.parentTypes.includes('oAuth2Api')
|
||||
);
|
||||
},
|
||||
isOAuthConnected(): boolean {
|
||||
return this.isOAuthType && !!this.credentialData.oauthTokenData;
|
||||
},
|
||||
credentialProperties(): INodeProperties[] {
|
||||
if (!this.credentialType) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return this.credentialType.properties.filter(
|
||||
(propertyData: INodeProperties) => {
|
||||
if (!this.displayCredentialParameter(propertyData)) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
!this.credentialType!.__overwrittenProperties ||
|
||||
!this.credentialType!.__overwrittenProperties.includes(
|
||||
propertyData.name,
|
||||
)
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
requiredPropertiesFilled(): boolean {
|
||||
for (const property of this.credentialProperties) {
|
||||
if (property.required !== true) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!this.credentialData[property.name]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async beforeClose(done: () => void) {
|
||||
let keepEditing = false;
|
||||
|
||||
if (this.hasUnsavedChanges) {
|
||||
const displayName = this.credentialType ? this.credentialType.displayName : '';
|
||||
keepEditing = await this.confirmMessage(
|
||||
`Are you sure you want to throw away the changes you made to the ${displayName} credential?`,
|
||||
'Close without saving?',
|
||||
null,
|
||||
'Keep editing',
|
||||
'Close',
|
||||
);
|
||||
}
|
||||
else if (this.isOAuthType && !this.isOAuthConnected) {
|
||||
keepEditing = await this.confirmMessage(
|
||||
`You need to connect your credential for it to work`,
|
||||
'Close without connecting?',
|
||||
null,
|
||||
'Keep editing',
|
||||
'Close',
|
||||
);
|
||||
}
|
||||
|
||||
if (!keepEditing) {
|
||||
done();
|
||||
return;
|
||||
}
|
||||
else if (!this.requiredPropertiesFilled) {
|
||||
this.showValidationWarning = true;
|
||||
this.scrollToTop();
|
||||
}
|
||||
else if (this.isOAuthType) {
|
||||
this.scrollToBottom();
|
||||
}
|
||||
},
|
||||
|
||||
displayCredentialParameter(parameter: INodeProperties): boolean {
|
||||
if (parameter.type === 'hidden') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (parameter.displayOptions === undefined) {
|
||||
// If it is not defined no need to do a proper check
|
||||
return true;
|
||||
}
|
||||
|
||||
return this.displayParameter(
|
||||
this.credentialData as INodeParameters,
|
||||
parameter,
|
||||
'',
|
||||
);
|
||||
},
|
||||
getCredentialProperties(name: string): INodeProperties[] {
|
||||
const credentialsData =
|
||||
this.$store.getters['credentials/getCredentialTypeByName'](name);
|
||||
|
||||
if (!credentialsData) {
|
||||
throw new Error(`Could not find credentials of type: ${name}`);
|
||||
}
|
||||
|
||||
if (credentialsData.extends === undefined) {
|
||||
return credentialsData.properties;
|
||||
}
|
||||
|
||||
const combineProperties = [] as INodeProperties[];
|
||||
for (const credentialsTypeName of credentialsData.extends) {
|
||||
const mergeCredentialProperties =
|
||||
this.getCredentialProperties(credentialsTypeName);
|
||||
NodeHelpers.mergeNodeProperties(
|
||||
combineProperties,
|
||||
mergeCredentialProperties,
|
||||
);
|
||||
}
|
||||
|
||||
// The properties defined on the parent credentials take presidence
|
||||
NodeHelpers.mergeNodeProperties(
|
||||
combineProperties,
|
||||
credentialsData.properties,
|
||||
);
|
||||
|
||||
return combineProperties;
|
||||
},
|
||||
|
||||
async loadCurrentCredential() {
|
||||
this.credentialId = this.activeId;
|
||||
|
||||
try {
|
||||
const currentCredentials: ICredentialsDecryptedResponse =
|
||||
await this.$store.dispatch('credentials/getCredentialData', {
|
||||
id: this.credentialId,
|
||||
});
|
||||
if (!currentCredentials) {
|
||||
throw new Error(
|
||||
`Could not find the credentials with the id: ${this.credentialId}`,
|
||||
);
|
||||
}
|
||||
|
||||
this.credentialData = currentCredentials.data || {};
|
||||
this.credentialName = currentCredentials.name;
|
||||
currentCredentials.nodesAccess.forEach(
|
||||
(access: { nodeType: string }) => {
|
||||
// keep node access structure to keep dates when updating
|
||||
this.nodeAccess[access.nodeType] = access;
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
this.$showError(
|
||||
e,
|
||||
'Problem loading credentials',
|
||||
'There was a problem loading the credentials:',
|
||||
);
|
||||
this.closeDialog();
|
||||
|
||||
return;
|
||||
}
|
||||
},
|
||||
onTabSelect(tab: string) {
|
||||
this.activeTab = tab;
|
||||
},
|
||||
onNodeAccessChange({name, value}: {name: string, value: boolean}) {
|
||||
this.hasUnsavedChanges = true;
|
||||
|
||||
if (value) {
|
||||
this.nodeAccess = {
|
||||
...this.nodeAccess,
|
||||
[name]: {
|
||||
nodeType: name,
|
||||
},
|
||||
};
|
||||
} else {
|
||||
this.nodeAccess = {
|
||||
...this.nodeAccess,
|
||||
[name]: null,
|
||||
};
|
||||
}
|
||||
},
|
||||
onDataChange({ name, value }: { name: string; value: any }) { // tslint:disable-line:no-any
|
||||
this.hasUnsavedChanges = true;
|
||||
|
||||
const { oauthTokenData, ...credData } = this.credentialData;
|
||||
|
||||
this.credentialData = {
|
||||
...credData,
|
||||
[name]: value,
|
||||
};
|
||||
},
|
||||
closeDialog() {
|
||||
this.modalBus.$emit('close');
|
||||
},
|
||||
|
||||
getParentTypes(name: string): string[] {
|
||||
const credentialType =
|
||||
this.$store.getters['credentials/getCredentialTypeByName'](name);
|
||||
|
||||
if (
|
||||
credentialType === undefined ||
|
||||
credentialType.extends === undefined
|
||||
) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const types: string[] = [];
|
||||
for (const typeName of credentialType.extends) {
|
||||
types.push(typeName);
|
||||
types.push.apply(types, this.getParentTypes(typeName));
|
||||
}
|
||||
|
||||
return types;
|
||||
},
|
||||
|
||||
onNameEdit(text: string) {
|
||||
this.hasUnsavedChanges = true;
|
||||
this.credentialName = text;
|
||||
},
|
||||
|
||||
scrollToTop() {
|
||||
setTimeout(() => {
|
||||
const content = this.$refs.content as Element;
|
||||
if (content) {
|
||||
content.scrollTop = 0;
|
||||
}
|
||||
}, 0);
|
||||
},
|
||||
|
||||
scrollToBottom() {
|
||||
setTimeout(() => {
|
||||
const content = this.$refs.content as Element;
|
||||
if (content) {
|
||||
content.scrollTop = content.scrollHeight;
|
||||
}
|
||||
}, 0);
|
||||
},
|
||||
|
||||
async retestCredential() {
|
||||
if (!this.isCredentialTestable) {
|
||||
this.authError = '';
|
||||
this.testedSuccessfully = false;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const nodesAccess = Object.values(this.nodeAccess).filter(
|
||||
(access) => !!access,
|
||||
) as ICredentialNodeAccess[];
|
||||
|
||||
// Save only the none default data
|
||||
const data = NodeHelpers.getNodeParameters(
|
||||
this.credentialType!.properties,
|
||||
this.credentialData as INodeParameters,
|
||||
false,
|
||||
false,
|
||||
);
|
||||
|
||||
const details: ICredentialsDecrypted = {
|
||||
name: this.credentialName,
|
||||
type: this.credentialTypeName!,
|
||||
data: data as unknown as ICredentialDataDecryptedObject,
|
||||
nodesAccess,
|
||||
};
|
||||
|
||||
this.isRetesting = true;
|
||||
await this.testCredential(details);
|
||||
this.isRetesting = false;
|
||||
},
|
||||
|
||||
async testCredential(credentialDetails: ICredentialsDecrypted) {
|
||||
const result: NodeCredentialTestResult = await this.$store.dispatch('credentials/testCredential', credentialDetails);
|
||||
if (result.status === 'Error') {
|
||||
this.authError = result.message;
|
||||
this.testedSuccessfully = false;
|
||||
}
|
||||
else {
|
||||
this.authError = '';
|
||||
this.testedSuccessfully = true;
|
||||
}
|
||||
|
||||
this.scrollToTop();
|
||||
},
|
||||
|
||||
async saveCredential(): Promise<ICredentialsResponse | null> {
|
||||
if (!this.requiredPropertiesFilled) {
|
||||
this.showValidationWarning = true;
|
||||
this.scrollToTop();
|
||||
}
|
||||
else {
|
||||
this.showValidationWarning = false;
|
||||
}
|
||||
|
||||
this.isSaving = true;
|
||||
const nodesAccess = Object.values(this.nodeAccess).filter(
|
||||
(access) => !!access,
|
||||
) as ICredentialNodeAccess[];
|
||||
|
||||
// Save only the none default data
|
||||
const data = NodeHelpers.getNodeParameters(
|
||||
this.credentialType!.properties,
|
||||
this.credentialData as INodeParameters,
|
||||
false,
|
||||
false,
|
||||
);
|
||||
|
||||
const credentialDetails: ICredentialsDecrypted = {
|
||||
name: this.credentialName,
|
||||
type: this.credentialTypeName!,
|
||||
data: data as unknown as ICredentialDataDecryptedObject,
|
||||
nodesAccess,
|
||||
};
|
||||
|
||||
let credential;
|
||||
|
||||
if (this.mode === 'new' && !this.credentialId) {
|
||||
credential = await this.createCredential(
|
||||
credentialDetails,
|
||||
);
|
||||
} else {
|
||||
credential = await this.updateCredential(
|
||||
credentialDetails,
|
||||
);
|
||||
}
|
||||
|
||||
this.isSaving = false;
|
||||
if (credential) {
|
||||
this.credentialId = credential.id as string;
|
||||
|
||||
if (this.isCredentialTestable) {
|
||||
this.isTesting = true;
|
||||
await this.testCredential(credentialDetails);
|
||||
this.isTesting = false;
|
||||
}
|
||||
else {
|
||||
this.authError = '';
|
||||
this.testedSuccessfully = false;
|
||||
}
|
||||
}
|
||||
|
||||
return credential;
|
||||
},
|
||||
|
||||
async createCredential(
|
||||
credentialDetails: ICredentialsDecrypted,
|
||||
): Promise<ICredentialsResponse | null> {
|
||||
let credential;
|
||||
|
||||
try {
|
||||
credential = (await this.$store.dispatch(
|
||||
'credentials/createNewCredential',
|
||||
credentialDetails,
|
||||
)) as ICredentialsResponse;
|
||||
this.hasUnsavedChanges = false;
|
||||
} catch (error) {
|
||||
this.$showError(
|
||||
error,
|
||||
'Problem creating credentials',
|
||||
'There was a problem creating the credentials:',
|
||||
);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
this.$externalHooks().run('credentials.create', {
|
||||
credentialTypeData: this.credentialData,
|
||||
});
|
||||
|
||||
return credential;
|
||||
},
|
||||
|
||||
async updateCredential(
|
||||
credentialDetails: ICredentialsDecrypted,
|
||||
): Promise<ICredentialsResponse | null> {
|
||||
let credential;
|
||||
try {
|
||||
credential = (await this.$store.dispatch(
|
||||
'credentials/updateCredential',
|
||||
{ id: this.credentialId, data: credentialDetails },
|
||||
)) as ICredentialsResponse;
|
||||
this.hasUnsavedChanges = false;
|
||||
} catch (error) {
|
||||
this.$showError(
|
||||
error,
|
||||
'Problem updating credentials',
|
||||
'There was a problem updating the credentials:',
|
||||
);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// Now that the credentials changed check if any nodes use credentials
|
||||
// which have now a different name
|
||||
this.updateNodesCredentialsIssues();
|
||||
|
||||
return credential;
|
||||
},
|
||||
|
||||
async deleteCredential() {
|
||||
if (!this.currentCredential) {
|
||||
return;
|
||||
}
|
||||
|
||||
const savedCredentialName = this.currentCredential.name;
|
||||
|
||||
const deleteConfirmed = await this.confirmMessage(
|
||||
`Are you sure you want to delete "${savedCredentialName}" credentials?`,
|
||||
'Delete Credentials?',
|
||||
null,
|
||||
'Yes, delete!',
|
||||
);
|
||||
|
||||
if (deleteConfirmed === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this.isDeleting = true;
|
||||
await this.$store.dispatch('credentials/deleteCredential', {
|
||||
id: this.credentialId,
|
||||
});
|
||||
this.hasUnsavedChanges = false;
|
||||
} catch (error) {
|
||||
this.$showError(
|
||||
error,
|
||||
'Problem deleting credentials',
|
||||
'There was a problem deleting the credentials:',
|
||||
);
|
||||
this.isDeleting = false;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.isDeleting = false;
|
||||
// Now that the credentials were removed check if any nodes used them
|
||||
this.updateNodesCredentialsIssues();
|
||||
|
||||
this.$showMessage({
|
||||
title: 'Credentials deleted',
|
||||
message: `The credential "${savedCredentialName}" was deleted!`,
|
||||
type: 'success',
|
||||
});
|
||||
this.closeDialog();
|
||||
},
|
||||
|
||||
async oAuthCredentialAuthorize() {
|
||||
let url;
|
||||
|
||||
const credential = await this.saveCredential();
|
||||
if (!credential) {
|
||||
return;
|
||||
}
|
||||
|
||||
const types = this.parentTypes;
|
||||
|
||||
try {
|
||||
if (
|
||||
this.credentialTypeName === 'oAuth2Api' ||
|
||||
types.includes('oAuth2Api')
|
||||
) {
|
||||
url = (await this.$store.dispatch('credentials/oAuth2Authorize', {
|
||||
...this.credentialData,
|
||||
id: credential.id,
|
||||
})) as string;
|
||||
} else if (
|
||||
this.credentialTypeName === 'oAuth1Api' ||
|
||||
types.includes('oAuth1Api')
|
||||
) {
|
||||
url = (await this.$store.dispatch('credentials/oAuth1Authorize', {
|
||||
...this.credentialData,
|
||||
id: credential.id,
|
||||
})) as string;
|
||||
}
|
||||
} catch (error) {
|
||||
this.$showError(
|
||||
error,
|
||||
'OAuth Authorization Error',
|
||||
'Error generating authorization URL:',
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const params = `scrollbars=no,resizable=yes,status=no,titlebar=noe,location=no,toolbar=no,menubar=no,width=500,height=700`;
|
||||
const oauthPopup = window.open(url, 'OAuth2 Authorization', params);
|
||||
Vue.set(this.credentialData, 'oauthTokenData', null);
|
||||
|
||||
const receiveMessage = (event: MessageEvent) => {
|
||||
// // TODO: Add check that it came from n8n
|
||||
// if (event.origin !== 'http://example.org:8080') {
|
||||
// return;
|
||||
// }
|
||||
if (event.data === 'success') {
|
||||
window.removeEventListener('message', receiveMessage, false);
|
||||
|
||||
// Set some kind of data that status changes.
|
||||
// As data does not get displayed directly it does not matter what data.
|
||||
Vue.set(this.credentialData, 'oauthTokenData', {});
|
||||
this.$store.commit('credentials/enableOAuthCredential', credential);
|
||||
|
||||
// Close the window
|
||||
if (oauthPopup) {
|
||||
oauthPopup.close();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('message', receiveMessage, false);
|
||||
},
|
||||
},
|
||||
|
||||
});
|
||||
</script>
|
||||
|
||||
<style module lang="scss">
|
||||
.credentialModal {
|
||||
max-width: 900px;
|
||||
--dialog-close-top: 28px;
|
||||
}
|
||||
|
||||
.mainContent {
|
||||
flex-grow: 1;
|
||||
overflow: auto;
|
||||
padding-bottom: 100px;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
max-width: 170px;
|
||||
min-width: 170px;
|
||||
margin-right: var(--spacing-l);
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.credInfo {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
margin-bottom: var(--spacing-s);
|
||||
}
|
||||
|
||||
.credTab {
|
||||
padding-left: 12px !important;
|
||||
}
|
||||
|
||||
.credActions {
|
||||
margin-right: var(--spacing-xl);
|
||||
> * {
|
||||
margin-left: var(--spacing-2xs);
|
||||
}
|
||||
}
|
||||
|
||||
.credIcon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-right: var(--spacing-xs);
|
||||
}
|
||||
|
||||
</style>
|
|
@ -0,0 +1,91 @@
|
|||
<template>
|
||||
<div :class="$style.container">
|
||||
<el-row>
|
||||
<el-col :span="8" :class="$style.accessLabel">
|
||||
<span>Allow use by</span>
|
||||
</el-col>
|
||||
<el-col :span="16">
|
||||
<div
|
||||
v-for="node in nodesWithAccess"
|
||||
:key="node.name"
|
||||
:class="$style.valueLabel"
|
||||
>
|
||||
<el-checkbox
|
||||
:label="node.displayName"
|
||||
:value="!!nodeAccess[node.name]"
|
||||
@change="(val) => onNodeAccessChange(node.name, val)"
|
||||
/>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row v-if="currentCredential">
|
||||
<el-col :span="8" :class="$style.label">
|
||||
<span>Created</span>
|
||||
</el-col>
|
||||
<el-col :span="16" :class="$style.valueLabel">
|
||||
<TimeAgo :date="currentCredential.createdAt" :capitalize="true" />
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row v-if="currentCredential">
|
||||
<el-col :span="8" :class="$style.label">
|
||||
<span>Last modified</span>
|
||||
</el-col>
|
||||
<el-col :span="16" :class="$style.valueLabel">
|
||||
<TimeAgo :date="currentCredential.updatedAt" :capitalize="true" />
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row v-if="currentCredential">
|
||||
<el-col :span="8" :class="$style.label">
|
||||
<span>ID</span>
|
||||
</el-col>
|
||||
<el-col :span="16" :class="$style.valueLabel">
|
||||
<span>{{currentCredential.id}}</span>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
|
||||
import TimeAgo from '../TimeAgo.vue';
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'CredentialInfo',
|
||||
props: ['nodesWithAccess', 'nodeAccess', 'currentCredential'],
|
||||
components: {
|
||||
TimeAgo,
|
||||
},
|
||||
methods: {
|
||||
onNodeAccessChange(name: string, value: string) {
|
||||
this.$emit('accessChange', {
|
||||
name,
|
||||
value,
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.container {
|
||||
> * {
|
||||
margin-bottom: var(--spacing-l);
|
||||
}
|
||||
}
|
||||
|
||||
.label {
|
||||
font-weight: var(--font-weight-bold);
|
||||
max-width: 230px;
|
||||
}
|
||||
|
||||
.accessLabel {
|
||||
composes: label;
|
||||
margin-top: var(--spacing-5xs);
|
||||
}
|
||||
|
||||
.valueLabel {
|
||||
font-weight: var(--font-weight-regular);
|
||||
}
|
||||
|
||||
</style>
|
|
@ -0,0 +1,52 @@
|
|||
<template>
|
||||
<div @keydown.stop :class="$style.container">
|
||||
<div v-for="parameter in credentialProperties" :key="parameter.name">
|
||||
<ParameterInputExpanded
|
||||
:parameter="parameter"
|
||||
:value="credentialData[parameter.name]"
|
||||
:documentationUrl="documentationUrl"
|
||||
:showValidationWarnings="showValidationWarnings"
|
||||
@change="valueChanged"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
|
||||
import { IUpdateInformation } from '../../Interface';
|
||||
|
||||
import ParameterInputExpanded from '../ParameterInputExpanded.vue';
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'CredentialsInput',
|
||||
props: [
|
||||
'credentialProperties',
|
||||
'credentialData', // ICredentialsDecryptedResponse
|
||||
'documentationUrl',
|
||||
'showValidationWarnings',
|
||||
],
|
||||
components: {
|
||||
ParameterInputExpanded,
|
||||
},
|
||||
methods: {
|
||||
valueChanged(parameterData: IUpdateInformation) {
|
||||
const name = parameterData.name.split('.').pop();
|
||||
|
||||
this.$emit('change', {
|
||||
name,
|
||||
value: parameterData.value,
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.container {
|
||||
> * {
|
||||
margin-bottom: var(--spacing-l);
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,41 @@
|
|||
<template>
|
||||
<span>
|
||||
<img
|
||||
v-if="isGoogleOAuthType"
|
||||
:src="basePath + 'google-signin-light.png'"
|
||||
:class="$style.googleIcon"
|
||||
alt="Sign in with Google"
|
||||
@click.stop="$emit('click')"
|
||||
/>
|
||||
<n8n-button
|
||||
v-else
|
||||
label="Connect my account"
|
||||
size="large"
|
||||
@click.stop="$emit('click')"
|
||||
/>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
|
||||
export default Vue.extend({
|
||||
props: {
|
||||
isGoogleOAuthType: {
|
||||
type: Boolean,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
basePath(): string {
|
||||
return this.$store.getters.getBaseUrl;
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style module lang="scss">
|
||||
.googleIcon {
|
||||
width: 191px;
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
74
packages/editor-ui/src/components/CredentialIcon.vue
Normal file
74
packages/editor-ui/src/components/CredentialIcon.vue
Normal file
|
@ -0,0 +1,74 @@
|
|||
<template>
|
||||
<div>
|
||||
<img v-if="filePath" :class="$style.credIcon" :src="filePath" />
|
||||
<NodeIcon v-else-if="relevantNode" :nodeType="relevantNode" :size="28" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { ICredentialType, INodeTypeDescription } from 'n8n-workflow';
|
||||
import Vue from 'vue';
|
||||
|
||||
export default Vue.extend({
|
||||
props: {
|
||||
credentialTypeName: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
credentialWithIcon(): ICredentialType | null {
|
||||
return this.getCredentialWithIcon(this.credentialTypeName);
|
||||
},
|
||||
|
||||
filePath(): string | null {
|
||||
if (!this.credentialWithIcon || !this.credentialWithIcon.icon || !this.credentialWithIcon.icon.startsWith('file:')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const restUrl = this.$store.getters.getRestUrl;
|
||||
|
||||
return `${restUrl}/credential-icon/${this.credentialWithIcon.name}`;
|
||||
},
|
||||
relevantNode(): INodeTypeDescription | null {
|
||||
if (this.credentialWithIcon && this.credentialWithIcon.icon && this.credentialWithIcon.icon.startsWith('node:')) {
|
||||
const nodeType = this.credentialWithIcon.icon.replace('node:', '');
|
||||
|
||||
return this.$store.getters.nodeType(nodeType);
|
||||
}
|
||||
|
||||
const nodesWithAccess = this.$store.getters['credentials/getNodesWithAccess'](this.credentialTypeName);
|
||||
|
||||
if (nodesWithAccess.length) {
|
||||
return nodesWithAccess[0];
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
getCredentialWithIcon(name: string): ICredentialType | null {
|
||||
const type = this.$store.getters['credentials/getCredentialTypeByName'](name);
|
||||
if (type.icon) {
|
||||
return type;
|
||||
}
|
||||
|
||||
if (type.extends) {
|
||||
return type.extends.reduce((accu: string | null, type: string) => {
|
||||
return accu || this.getCredentialWithIcon(type);
|
||||
}, null);
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
|
||||
.credIcon {
|
||||
height: 26px;
|
||||
}
|
||||
|
||||
</style>
|
|
@ -1,388 +0,0 @@
|
|||
<template>
|
||||
<div v-if="dialogVisible" @keydown.stop>
|
||||
<el-dialog :visible="dialogVisible" append-to-body width="75%" class="credentials-edit-wrapper" :title="title" :nodeType="nodeType" :before-close="closeDialog">
|
||||
<div name="title" class="title-container" slot="title">
|
||||
<div class="title-left">{{title}}</div>
|
||||
<div class="title-right">
|
||||
<div v-if="credentialType && documentationUrl" class="docs-container">
|
||||
<svg class="help-logo" target="_blank" width="18px" height="18px" viewBox="0 0 18 18" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<title>Node Documentation</title>
|
||||
<g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g transform="translate(-1127.000000, -836.000000)" fill-rule="nonzero">
|
||||
<g transform="translate(1117.000000, 825.000000)">
|
||||
<g transform="translate(10.000000, 11.000000)">
|
||||
<g transform="translate(2.250000, 2.250000)" fill="#FF6150">
|
||||
<path d="M6,11.25 L7.5,11.25 L7.5,9.75 L6,9.75 L6,11.25 M6.75,2.25 C5.09314575,2.25 3.75,3.59314575 3.75,5.25 L5.25,5.25 C5.25,4.42157288 5.92157288,3.75 6.75,3.75 C7.57842712,3.75 8.25,4.42157288 8.25,5.25 C8.25,6.75 6,6.5625 6,9 L7.5,9 C7.5,7.3125 9.75,7.125 9.75,5.25 C9.75,3.59314575 8.40685425,2.25 6.75,2.25 M1.5,0 L12,0 C12.8284271,0 13.5,0.671572875 13.5,1.5 L13.5,12 C13.5,12.8284271 12.8284271,13.5 12,13.5 L1.5,13.5 C0.671572875,13.5 0,12.8284271 0,12 L0,1.5 C0,0.671572875 0.671572875,0 1.5,0 Z"></path>
|
||||
</g>
|
||||
<rect x="0" y="0" width="18" height="18"></rect>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
<span class="doc-link-text">Need help? <a class="doc-hyperlink" :href="documentationUrl" target="_blank">Open credential docs</a></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="credential-type-item">
|
||||
<el-row v-if="!setCredentialType">
|
||||
<el-col :span="6">
|
||||
Credential type:
|
||||
</el-col>
|
||||
<el-col :span="18">
|
||||
<n8n-select v-model="credentialType" filterable placeholder="Select Type" size="medium" ref="credentialsDropdown">
|
||||
<n8n-option
|
||||
v-for="item in credentialTypes"
|
||||
:key="item.name"
|
||||
:label="item.displayName"
|
||||
:value="item.name">
|
||||
</n8n-option>
|
||||
</n8n-select>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
|
||||
<credentials-input v-if="credentialType" @credentialsCreated="credentialsCreated" @credentialsUpdated="credentialsUpdated" :credentialTypeData="getCredentialTypeData(credentialType)" :credentialData="credentialData" :nodesInit="nodesInit"></credentials-input>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
|
||||
import { externalHooks } from '@/components/mixins/externalHooks';
|
||||
import { restApi } from '@/components/mixins/restApi';
|
||||
import { showMessage } from '@/components/mixins/showMessage';
|
||||
import CredentialsInput from '@/components/CredentialsInput.vue';
|
||||
import {
|
||||
ICredentialsCreatedEvent,
|
||||
ICredentialsDecryptedResponse,
|
||||
} from '@/Interface';
|
||||
|
||||
import {
|
||||
NodeHelpers,
|
||||
ICredentialType,
|
||||
INodeProperties,
|
||||
INodeTypeDescription,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import mixins from 'vue-typed-mixins';
|
||||
import { INodeUi } from '../Interface';
|
||||
|
||||
export default mixins(
|
||||
restApi,
|
||||
showMessage,
|
||||
externalHooks,
|
||||
).extend({
|
||||
name: 'CredentialsEdit',
|
||||
props: [
|
||||
'dialogVisible', // Boolean
|
||||
'editCredentials',
|
||||
'setCredentialType', // String
|
||||
'nodesInit', // Array
|
||||
],
|
||||
components: {
|
||||
CredentialsInput,
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
credentialData: null as ICredentialsDecryptedResponse | null,
|
||||
credentialType: null as string | null,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
credentialTypes (): ICredentialType[] {
|
||||
const credentialTypes = this.$store.getters.allCredentialTypes;
|
||||
if (credentialTypes === null) {
|
||||
return [];
|
||||
}
|
||||
return credentialTypes;
|
||||
},
|
||||
title (): string {
|
||||
if (this.editCredentials) {
|
||||
const credentialType = this.$store.getters.credentialType(this.editCredentials.type);
|
||||
return `Edit Credentials: "${credentialType.displayName}"`;
|
||||
} else {
|
||||
if (this.credentialType) {
|
||||
const credentialType = this.$store.getters.credentialType(this.credentialType);
|
||||
return `Create New Credentials: "${credentialType.displayName}"`;
|
||||
} else {
|
||||
return `Create New Credentials`;
|
||||
}
|
||||
}
|
||||
},
|
||||
documentationUrl (): string | undefined {
|
||||
let credentialTypeName = '';
|
||||
if (this.editCredentials) {
|
||||
credentialTypeName = this.editCredentials.type as string;
|
||||
} else {
|
||||
credentialTypeName = this.credentialType as string;
|
||||
}
|
||||
|
||||
const credentialType = this.$store.getters.credentialType(credentialTypeName);
|
||||
if (credentialType.documentationUrl !== undefined) {
|
||||
if (credentialType.documentationUrl.startsWith('http')) {
|
||||
return credentialType.documentationUrl;
|
||||
} else {
|
||||
return 'https://docs.n8n.io/credentials/' + credentialType.documentationUrl + '/?utm_source=n8n_app&utm_medium=left_nav_menu&utm_campaign=create_new_credentials_modal';
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
node (): INodeUi {
|
||||
return this.$store.getters.activeNode;
|
||||
},
|
||||
nodeType (): INodeTypeDescription | null {
|
||||
const activeNode = this.node;
|
||||
if (this.node) {
|
||||
return this.$store.getters.nodeType(this.node.type);
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
async dialogVisible (newValue, oldValue): Promise<void> {
|
||||
if (newValue) {
|
||||
if (this.editCredentials) {
|
||||
// Credentials which should be edited are given
|
||||
const credentialType = this.$store.getters.credentialType(this.editCredentials.type);
|
||||
|
||||
if (credentialType === null) {
|
||||
this.$showMessage({
|
||||
title: 'Credential type not known',
|
||||
message: `Credentials of type "${this.editCredentials.type}" are not known.`,
|
||||
type: 'error',
|
||||
duration: 0,
|
||||
});
|
||||
this.closeDialog();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.editCredentials.id === undefined) {
|
||||
this.$showMessage({
|
||||
title: 'Credential ID missing',
|
||||
message: 'The ID of the credentials which should be edited is missing!',
|
||||
type: 'error',
|
||||
});
|
||||
this.closeDialog();
|
||||
return;
|
||||
}
|
||||
|
||||
let currentCredentials: ICredentialsDecryptedResponse | undefined;
|
||||
try {
|
||||
currentCredentials = await this.restApi().getCredentials(this.editCredentials.id as string, true) as ICredentialsDecryptedResponse | undefined;
|
||||
} catch (error) {
|
||||
this.$showError(error, 'Problem loading credentials', 'There was a problem loading the credentials:');
|
||||
this.closeDialog();
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentCredentials === undefined) {
|
||||
this.$showMessage({
|
||||
title: 'Credentials not found',
|
||||
message: `Could not find the credentials with the id: ${this.editCredentials.id}`,
|
||||
type: 'error',
|
||||
duration: 0,
|
||||
});
|
||||
this.closeDialog();
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentCredentials === undefined) {
|
||||
this.$showMessage({
|
||||
title: 'Problem loading credentials',
|
||||
message: 'No credentials could be loaded!',
|
||||
type: 'error',
|
||||
});
|
||||
return;
|
||||
}
|
||||
this.credentialData = currentCredentials;
|
||||
} else {
|
||||
Vue.nextTick(() => {
|
||||
(this.$refs.credentialsDropdown as HTMLDivElement).focus();
|
||||
});
|
||||
if (this.credentialType || this.setCredentialType) {
|
||||
const credentialType = this.$store.getters.credentialType(this.credentialType || this.setCredentialType);
|
||||
if (credentialType === null) {
|
||||
this.$showMessage({
|
||||
title: 'Credential type not known',
|
||||
message: `Credentials of type "${this.credentialType || this.setCredentialType}" are not known.`,
|
||||
type: 'error',
|
||||
duration: 0,
|
||||
});
|
||||
this.closeDialog();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.credentialData = null;
|
||||
}
|
||||
|
||||
if (this.setCredentialType || (this.credentialData && this.credentialData.type)) {
|
||||
this.credentialType = this.setCredentialType || (this.credentialData && this.credentialData.type);
|
||||
}
|
||||
} else {
|
||||
// Make sure that it gets always reset else it uses by default
|
||||
// again the last selection from when it was open the previous time.
|
||||
this.credentialType = null;
|
||||
}
|
||||
},
|
||||
async credentialType (newValue, oldValue) {
|
||||
this.$externalHooks().run('credentialsEdit.credentialTypeChanged', { newValue, oldValue, editCredentials: !!this.editCredentials, credentialType: this.credentialType, setCredentialType: this.setCredentialType });
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
getCredentialProperties (name: string): INodeProperties[] {
|
||||
const credentialsData = this.$store.getters.credentialType(name);
|
||||
|
||||
if (credentialsData === null) {
|
||||
throw new Error(`Could not find credentials of type: ${name}`);
|
||||
}
|
||||
|
||||
if (credentialsData.extends === undefined) {
|
||||
return credentialsData.properties;
|
||||
}
|
||||
|
||||
const combineProperties = [] as INodeProperties[];
|
||||
for (const credentialsTypeName of credentialsData.extends) {
|
||||
const mergeCredentialProperties = this.getCredentialProperties(credentialsTypeName);
|
||||
NodeHelpers.mergeNodeProperties(combineProperties, mergeCredentialProperties);
|
||||
}
|
||||
|
||||
// The properties defined on the parent credentials take presidence
|
||||
NodeHelpers.mergeNodeProperties(combineProperties, credentialsData.properties);
|
||||
|
||||
return combineProperties;
|
||||
},
|
||||
getCredentialTypeData (name: string): ICredentialType | null {
|
||||
let credentialData = this.$store.getters.credentialType(name);
|
||||
|
||||
if (credentialData === null || credentialData.extends === undefined) {
|
||||
return credentialData;
|
||||
}
|
||||
|
||||
// Credentials extends another one. So get the properties of the one it
|
||||
// extends and add them.
|
||||
credentialData = JSON.parse(JSON.stringify(credentialData));
|
||||
credentialData.properties = this.getCredentialProperties(credentialData.name);
|
||||
|
||||
return credentialData;
|
||||
},
|
||||
credentialsCreated (eventData: ICredentialsCreatedEvent): void {
|
||||
this.$emit('credentialsCreated', eventData);
|
||||
|
||||
this.$showMessage({
|
||||
title: 'Credentials created',
|
||||
message: `"${eventData.data.name}" credentials were successfully created!`,
|
||||
type: 'success',
|
||||
});
|
||||
|
||||
if (eventData.options.closeDialog === true) {
|
||||
this.closeDialog();
|
||||
}
|
||||
},
|
||||
credentialsUpdated (eventData: ICredentialsCreatedEvent): void {
|
||||
this.$emit('credentialsUpdated', eventData);
|
||||
|
||||
this.$showMessage({
|
||||
title: 'Credentials updated',
|
||||
message: `"${eventData.data.name}" credentials were successfully updated!`,
|
||||
type: 'success',
|
||||
});
|
||||
|
||||
if (eventData.options.closeDialog === true) {
|
||||
this.closeDialog();
|
||||
}
|
||||
},
|
||||
closeDialog (): void {
|
||||
// Handle the close externally as the visible parameter is an external prop
|
||||
// and is so not allowed to be changed here.
|
||||
this.$emit('closeDialog');
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.credentials-edit-wrapper {
|
||||
.credential-type-item {
|
||||
> .el-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
@media (min-width: 1200px){
|
||||
.title-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
max-width: 100%;
|
||||
line-height: 17px;
|
||||
}
|
||||
|
||||
.docs-container {
|
||||
margin-left: auto;
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1199px){
|
||||
.title-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 100%;
|
||||
line-height: 17px;
|
||||
}
|
||||
|
||||
.docs-container {
|
||||
margin-top: 10px;
|
||||
margin-left: 0;
|
||||
margin-right: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.title-left {
|
||||
flex: 7;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #7a7a7a;
|
||||
vertical-align:middle;
|
||||
}
|
||||
|
||||
.title-right {
|
||||
vertical-align: middle;
|
||||
flex: 3;
|
||||
font-family: "Open Sans";
|
||||
color: #666666;
|
||||
font-size: 12px;
|
||||
font-weight: 510;
|
||||
letter-spacing: 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
min-width: 40%;
|
||||
}
|
||||
|
||||
.help-logo {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.doc-link-text {
|
||||
margin-left: 2px;
|
||||
float: right;
|
||||
word-break: break-word;
|
||||
flex: 9;
|
||||
}
|
||||
|
||||
.doc-hyperlink,
|
||||
.doc-hyperlink:visited,
|
||||
.doc-hyperlink:focus,
|
||||
.doc-hyperlink:active {
|
||||
text-decoration: none;
|
||||
color: #FF6150;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
|
@ -1,618 +0,0 @@
|
|||
<template>
|
||||
<div @keydown.stop class="credentials-input-wrapper">
|
||||
<el-row class="credential-name-wrapper">
|
||||
<el-col :span="6" class="headline-regular">
|
||||
Credentials Name:
|
||||
<n8n-tooltip class="credentials-info" placement="top" >
|
||||
<div slot="content" v-html="helpTexts.credentialsName"></div>
|
||||
<font-awesome-icon icon="question-circle" />
|
||||
</n8n-tooltip>
|
||||
</el-col>
|
||||
<el-col :span="18">
|
||||
<n8n-input v-model="name" type="text" size="medium"></n8n-input>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<br />
|
||||
<div class="headline" v-if="credentialProperties.length">
|
||||
Credential Data:
|
||||
<n8n-tooltip class="credentials-info" placement="top" >
|
||||
<div slot="content" v-html="helpTexts.credentialsData"></div>
|
||||
<font-awesome-icon icon="question-circle" />
|
||||
</n8n-tooltip>
|
||||
</div>
|
||||
<div v-for="parameter in credentialProperties" :key="parameter.name">
|
||||
<el-row class="parameter-wrapper">
|
||||
<el-col :span="6" class="parameter-name">
|
||||
{{parameter.displayName}}:
|
||||
<n8n-tooltip placement="top" class="parameter-info" v-if="parameter.description" >
|
||||
<div slot="content" v-html="addTargetBlank(parameter.description)"></div>
|
||||
<font-awesome-icon icon="question-circle"/>
|
||||
</n8n-tooltip>
|
||||
</el-col>
|
||||
<el-col :span="18">
|
||||
<parameter-input :parameter="parameter" :value="propertyValue[parameter.name]" :path="parameter.name" :isCredential="true" :displayOptions="true" @valueChanged="valueChanged" inputSize="medium" />
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
|
||||
<el-row v-if="isOAuthType" class="oauth-information">
|
||||
<el-col :span="6" class="headline">
|
||||
OAuth
|
||||
</el-col>
|
||||
<el-col :span="18">
|
||||
<span v-if="requiredPropertiesFilled === false">
|
||||
<n8n-icon-button title="Connect OAuth Credentials" icon="redo" :disabled="true" size="large" />
|
||||
Enter all required properties
|
||||
</span>
|
||||
<span v-else-if="isOAuthConnected === true">
|
||||
<n8n-icon-button title="Reconnect OAuth Credentials" @click.stop="oAuthCredentialAuthorize()" icon="redo" size="large" />
|
||||
Connected
|
||||
</span>
|
||||
<span v-else>
|
||||
<span v-if="isGoogleOAuthType">
|
||||
<img :src="basePath + 'google-signin.png'" class="google-icon clickable" alt="Sign in with Google" @click.stop="oAuthCredentialAuthorize()" />
|
||||
</span>
|
||||
<span v-else>
|
||||
<n8n-icon-button title="Connect OAuth Credentials" @click.stop="oAuthCredentialAuthorize()" icon="sign-in-alt" size="large" />
|
||||
Not connected
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<div v-if="credentialProperties.length">
|
||||
<div class="clickable oauth-callback-headline" :class="{expanded: !isMinimized}" @click="isMinimized=!isMinimized" :title="isMinimized ? 'Click to display Webhook URLs' : 'Click to hide Webhook URLs'">
|
||||
<font-awesome-icon icon="angle-up" class="minimize-button minimize-icon" />
|
||||
OAuth Callback URL
|
||||
</div>
|
||||
<n8n-tooltip v-if="!isMinimized" class="item" content="Click to copy Callback URL" placement="right">
|
||||
<div class="callback-url left-ellipsis clickable" @click="copyCallbackUrl">
|
||||
{{oAuthCallbackUrl}}
|
||||
</div>
|
||||
</n8n-tooltip>
|
||||
</div>
|
||||
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row class="nodes-access-wrapper">
|
||||
<el-col :span="6" class="headline">
|
||||
Nodes with access:
|
||||
<n8n-tooltip class="credentials-info" placement="top" >
|
||||
<div slot="content" v-html="helpTexts.nodesWithAccess"></div>
|
||||
<font-awesome-icon icon="question-circle" />
|
||||
</n8n-tooltip>
|
||||
</el-col>
|
||||
<el-col :span="18">
|
||||
<el-transfer
|
||||
:titles="['No Access', 'Access ']"
|
||||
v-model="nodesAccess"
|
||||
:data="allNodesRequestingAccess">
|
||||
</el-transfer>
|
||||
|
||||
<div v-if="nodesAccess.length === 0" class="no-nodes-access">
|
||||
<strong>
|
||||
Important
|
||||
</strong><br />
|
||||
Add at least one node which has access to the credentials!
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<div class="action-buttons">
|
||||
<n8n-button type="success" @click="updateCredentials(true)" label="Save" size="large" v-if="credentialDataDynamic" />
|
||||
<n8n-button @click="createCredentials(true)" label="Create" size="large" v-else />
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
|
||||
import { copyPaste } from '@/components/mixins/copyPaste';
|
||||
import { externalHooks } from '@/components/mixins/externalHooks';
|
||||
import { restApi } from '@/components/mixins/restApi';
|
||||
import { nodeHelpers } from '@/components/mixins/nodeHelpers';
|
||||
import { showMessage } from '@/components/mixins/showMessage';
|
||||
|
||||
import {
|
||||
ICredentialsDecryptedResponse,
|
||||
ICredentialsResponse,
|
||||
IUpdateInformation,
|
||||
} from '@/Interface';
|
||||
import {
|
||||
CredentialInformation,
|
||||
ICredentialDataDecryptedObject,
|
||||
ICredentialsDecrypted,
|
||||
ICredentialType,
|
||||
ICredentialNodeAccess,
|
||||
INodeCredentialDescription,
|
||||
INodeParameters,
|
||||
INodeProperties,
|
||||
INodeTypeDescription,
|
||||
NodeHelpers,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import ParameterInput from '@/components/ParameterInput.vue';
|
||||
|
||||
import mixins from 'vue-typed-mixins';
|
||||
|
||||
import { addTargetBlank } from './helpers';
|
||||
|
||||
export default mixins(
|
||||
copyPaste,
|
||||
externalHooks,
|
||||
nodeHelpers,
|
||||
restApi,
|
||||
showMessage,
|
||||
).extend({
|
||||
name: 'CredentialsInput',
|
||||
props: [
|
||||
'credentialTypeData', // ICredentialType
|
||||
'credentialData', // ICredentialsDecryptedResponse
|
||||
'nodesInit', // {
|
||||
// type: Array,
|
||||
// default: () => { [] },
|
||||
// }
|
||||
],
|
||||
components: {
|
||||
ParameterInput,
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
basePath: this.$store.getters.getBaseUrl,
|
||||
isMinimized: true,
|
||||
helpTexts: {
|
||||
credentialsData: 'The credentials to set.',
|
||||
credentialsName: 'A recognizable label for the credentials. Descriptive names work <br />best here, so you can easily select it from a list later.',
|
||||
nodesWithAccess: 'Nodes with access to these credentials.',
|
||||
},
|
||||
credentialDataTemp: null as ICredentialsDecryptedResponse | null,
|
||||
nodesAccess: [] as string[],
|
||||
name: '',
|
||||
propertyValue: {} as ICredentialDataDecryptedObject,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
allNodesRequestingAccess (): Array<{key: string, label: string}> {
|
||||
const returnNodeTypes: string[] = [];
|
||||
|
||||
const nodeTypes: INodeTypeDescription[] = this.$store.getters.allNodeTypes;
|
||||
|
||||
let nodeType: INodeTypeDescription;
|
||||
let credentialTypeDescription: INodeCredentialDescription;
|
||||
|
||||
// Find the node types which need the credentials
|
||||
for (nodeType of nodeTypes) {
|
||||
if (!nodeType.credentials) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (credentialTypeDescription of nodeType.credentials) {
|
||||
if (credentialTypeDescription.name === (this.credentialTypeData as ICredentialType).name && !returnNodeTypes.includes(credentialTypeDescription.name)) {
|
||||
returnNodeTypes.push(nodeType.name);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Return the data in the correct format el-transfer expects
|
||||
return returnNodeTypes.map((nodeTypeName: string) => {
|
||||
return {
|
||||
key: nodeTypeName,
|
||||
label: this.$store.getters.nodeType(nodeTypeName).displayName as string,
|
||||
};
|
||||
});
|
||||
},
|
||||
credentialProperties (): INodeProperties[] {
|
||||
return this.credentialTypeData.properties.filter((propertyData: INodeProperties) => {
|
||||
if (!this.displayCredentialParameter(propertyData)) {
|
||||
return false;
|
||||
}
|
||||
return !this.credentialTypeData.__overwrittenProperties || !this.credentialTypeData.__overwrittenProperties.includes(propertyData.name);
|
||||
});
|
||||
},
|
||||
credentialDataDynamic (): ICredentialsDecryptedResponse | null {
|
||||
if (this.credentialData) {
|
||||
return this.credentialData;
|
||||
}
|
||||
|
||||
return this.credentialDataTemp;
|
||||
},
|
||||
isGoogleOAuthType (): boolean {
|
||||
if (this.credentialTypeData.name === 'googleOAuth2Api') {
|
||||
return true;
|
||||
}
|
||||
const types = this.parentTypes(this.credentialTypeData.name);
|
||||
return types.includes('googleOAuth2Api');
|
||||
},
|
||||
isOAuthType (): boolean {
|
||||
if (['oAuth1Api', 'oAuth2Api'].includes(this.credentialTypeData.name)) {
|
||||
return true;
|
||||
}
|
||||
const types = this.parentTypes(this.credentialTypeData.name);
|
||||
return types.includes('oAuth1Api') || types.includes('oAuth2Api');
|
||||
},
|
||||
isOAuthConnected (): boolean {
|
||||
if (this.isOAuthType === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.credentialDataDynamic !== null && !!this.credentialDataDynamic.data!.oauthTokenData;
|
||||
},
|
||||
oAuthCallbackUrl (): string {
|
||||
const types = this.parentTypes(this.credentialTypeData.name);
|
||||
const oauthType = (this.credentialTypeData.name === 'oAuth2Api' || types.includes('oAuth2Api')) ? 'oauth2' : 'oauth1';
|
||||
return this.$store.getters.oauthCallbackUrls[oauthType];
|
||||
},
|
||||
requiredPropertiesFilled (): boolean {
|
||||
for (const property of this.credentialProperties) {
|
||||
if (property.required !== true) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!this.propertyValue[property.name]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
addTargetBlank,
|
||||
copyCallbackUrl (): void {
|
||||
this.copyToClipboard(this.oAuthCallbackUrl);
|
||||
|
||||
this.$showMessage({
|
||||
title: 'Copied',
|
||||
message: `Callback URL was successfully copied!`,
|
||||
type: 'success',
|
||||
});
|
||||
},
|
||||
parentTypes (name: string): string[] {
|
||||
const credentialType = this.$store.getters.credentialType(name);
|
||||
|
||||
if (credentialType === undefined || credentialType.extends === undefined) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const types: string[] = [];
|
||||
for (const typeName of credentialType.extends) {
|
||||
types.push(typeName);
|
||||
types.push.apply(types, this.parentTypes(typeName));
|
||||
}
|
||||
|
||||
return types;
|
||||
},
|
||||
valueChanged (parameterData: IUpdateInformation) {
|
||||
const name = parameterData.name.split('.').pop() as string;
|
||||
// For a currently for me unknown reason can In not simply just
|
||||
// set the value and it has to be this way.
|
||||
const tempValue = JSON.parse(JSON.stringify(this.propertyValue));
|
||||
tempValue[name] = parameterData.value;
|
||||
Vue.set(this, 'propertyValue', tempValue);
|
||||
},
|
||||
displayCredentialParameter (parameter: INodeProperties): boolean {
|
||||
if (parameter.type === 'hidden') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (parameter.displayOptions === undefined) {
|
||||
// If it is not defined no need to do a proper check
|
||||
return true;
|
||||
}
|
||||
|
||||
return this.displayParameter(this.propertyValue as INodeParameters, parameter, '');
|
||||
},
|
||||
async createCredentials (closeDialog: boolean): Promise<ICredentialsResponse | null> {
|
||||
const nodesAccess = this.nodesAccess.map((nodeType) => {
|
||||
return {
|
||||
nodeType,
|
||||
};
|
||||
});
|
||||
|
||||
const newCredentials = {
|
||||
name: this.name,
|
||||
type: (this.credentialTypeData as ICredentialType).name,
|
||||
nodesAccess,
|
||||
// Save only the none default data
|
||||
data: NodeHelpers.getNodeParameters(this.credentialTypeData.properties as INodeProperties[], this.propertyValue as INodeParameters, false, false),
|
||||
} as ICredentialsDecrypted;
|
||||
|
||||
let result;
|
||||
try {
|
||||
result = await this.restApi().createNewCredentials(newCredentials);
|
||||
} catch (error) {
|
||||
this.$showError(error, 'Problem Creating Credentials', 'There was a problem creating the credentials:');
|
||||
return null;
|
||||
}
|
||||
|
||||
// Add also to local store
|
||||
this.$store.commit('addCredentials', result);
|
||||
|
||||
this.$emit('credentialsCreated', {data: result, options: { closeDialog }});
|
||||
|
||||
this.$externalHooks().run('credentials.create', { credentialTypeData: this.credentialTypeData });
|
||||
|
||||
return result;
|
||||
},
|
||||
async oAuthCredentialAuthorize () {
|
||||
let url;
|
||||
|
||||
let credentialData = this.credentialDataDynamic;
|
||||
let newCredentials = false;
|
||||
if (!credentialData) {
|
||||
// Credentials did not get created yet. So create first before
|
||||
// doing oauth authorize
|
||||
credentialData = await this.createCredentials(false) as ICredentialsDecryptedResponse;
|
||||
newCredentials = true;
|
||||
if (credentialData === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Set the internal data directly so that even if it fails it displays a "Save" instead
|
||||
// of the "Create" button. If that would not be done, people could not retry after a
|
||||
// connect issue as it woult try to create credentials again which would fail as they
|
||||
// exist already.
|
||||
Vue.set(this, 'credentialDataTemp', credentialData);
|
||||
} else {
|
||||
// Exists already but got maybe changed. So save first
|
||||
credentialData = await this.updateCredentials(false) as ICredentialsDecryptedResponse;
|
||||
if (credentialData === null) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const types = this.parentTypes(this.credentialTypeData.name);
|
||||
|
||||
try {
|
||||
if (this.credentialTypeData.name === 'oAuth2Api' || types.includes('oAuth2Api')) {
|
||||
url = await this.restApi().oAuth2CredentialAuthorize(credentialData as ICredentialsResponse) as string;
|
||||
} else if (this.credentialTypeData.name === 'oAuth1Api' || types.includes('oAuth1Api')) {
|
||||
url = await this.restApi().oAuth1CredentialAuthorize(credentialData as ICredentialsResponse) as string;
|
||||
}
|
||||
} catch (error) {
|
||||
this.$showError(error, 'OAuth Authorization Error', 'Error generating authorization URL:');
|
||||
return;
|
||||
}
|
||||
|
||||
const params = `scrollbars=no,resizable=yes,status=no,titlebar=noe,location=no,toolbar=no,menubar=no,width=500,height=700`;
|
||||
const oauthPopup = window.open(url, 'OAuth2 Authorization', params);
|
||||
|
||||
const receiveMessage = (event: MessageEvent) => {
|
||||
// // TODO: Add check that it came from n8n
|
||||
// if (event.origin !== 'http://example.org:8080') {
|
||||
// return;
|
||||
// }
|
||||
|
||||
if (event.data === 'success') {
|
||||
|
||||
// Set some kind of data that status changes.
|
||||
// As data does not get displayed directly it does not matter what data.
|
||||
if (this.credentialData === null) {
|
||||
// Are new credentials so did not get send via "credentialData"
|
||||
Vue.set(this, 'credentialDataTemp', credentialData);
|
||||
Vue.set(this.credentialDataTemp!.data!, 'oauthTokenData', {});
|
||||
} else {
|
||||
// Credentials did already exist so can be set directly
|
||||
Vue.set(this.credentialData.data, 'oauthTokenData', {});
|
||||
}
|
||||
|
||||
// Save that OAuth got authorized locally
|
||||
this.$store.commit('updateCredentials', this.credentialDataDynamic);
|
||||
|
||||
// Close the window
|
||||
if (oauthPopup) {
|
||||
oauthPopup.close();
|
||||
}
|
||||
|
||||
if (newCredentials === true) {
|
||||
this.$emit('credentialsCreated', {data: credentialData, options: { closeDialog: false }});
|
||||
}
|
||||
|
||||
this.$showMessage({
|
||||
title: 'Connected',
|
||||
message: 'Connected successfully!',
|
||||
type: 'success',
|
||||
});
|
||||
|
||||
// Make sure that the event gets removed again
|
||||
window.removeEventListener('message', receiveMessage, false);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
window.addEventListener('message', receiveMessage, false);
|
||||
},
|
||||
async updateCredentials (closeDialog: boolean): Promise<ICredentialsResponse | null> {
|
||||
const nodesAccess: ICredentialNodeAccess[] = [];
|
||||
const addedNodeTypes: string[] = [];
|
||||
|
||||
// Add Node-type which already had access to keep the original added date
|
||||
let nodeAccessData: ICredentialNodeAccess;
|
||||
for (nodeAccessData of (this.credentialDataDynamic as ICredentialsDecryptedResponse).nodesAccess) {
|
||||
if (this.nodesAccess.includes((nodeAccessData.nodeType))) {
|
||||
nodesAccess.push(nodeAccessData);
|
||||
addedNodeTypes.push(nodeAccessData.nodeType);
|
||||
}
|
||||
}
|
||||
|
||||
// Add Node-type which did not have access before
|
||||
for (const nodeType of this.nodesAccess) {
|
||||
if (!addedNodeTypes.includes(nodeType)) {
|
||||
nodesAccess.push({
|
||||
nodeType,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const newCredentials = {
|
||||
name: this.name,
|
||||
type: (this.credentialTypeData as ICredentialType).name,
|
||||
nodesAccess,
|
||||
// Save only the none default data
|
||||
data: NodeHelpers.getNodeParameters(this.credentialTypeData.properties as INodeProperties[], this.propertyValue as INodeParameters, false, false),
|
||||
} as ICredentialsDecrypted;
|
||||
|
||||
let result;
|
||||
try {
|
||||
result = await this.restApi().updateCredentials((this.credentialDataDynamic as ICredentialsDecryptedResponse).id as string, newCredentials);
|
||||
} catch (error) {
|
||||
this.$showError(error, 'Problem Updating Credentials', 'There was a problem updating the credentials:');
|
||||
return null;
|
||||
}
|
||||
|
||||
// Update also in local store
|
||||
this.$store.commit('updateCredentials', result);
|
||||
|
||||
// Now that the credentials changed check if any nodes use credentials
|
||||
// which have now a different name
|
||||
this.updateNodesCredentialsIssues();
|
||||
|
||||
this.$emit('credentialsUpdated', {data: result, options: { closeDialog }});
|
||||
|
||||
return result;
|
||||
},
|
||||
init () {
|
||||
if (this.credentialData) {
|
||||
// Initialize with the given data
|
||||
this.name = (this.credentialData as ICredentialsDecryptedResponse).name;
|
||||
this.propertyValue = (this.credentialData as ICredentialsDecryptedResponse).data as ICredentialDataDecryptedObject;
|
||||
const nodesAccess = (this.credentialData as ICredentialsDecryptedResponse).nodesAccess.map((nodeAccess) => {
|
||||
return nodeAccess.nodeType;
|
||||
});
|
||||
|
||||
Vue.set(this, 'nodesAccess', nodesAccess);
|
||||
} else {
|
||||
// No data supplied so init empty
|
||||
this.name = '';
|
||||
this.propertyValue = {} as ICredentialDataDecryptedObject;
|
||||
const nodesAccess = [] as string[];
|
||||
nodesAccess.push.apply(nodesAccess, this.nodesInit);
|
||||
|
||||
Vue.set(this, 'nodesAccess', nodesAccess);
|
||||
}
|
||||
|
||||
// Set default values
|
||||
for (const property of (this.credentialTypeData as ICredentialType).properties) {
|
||||
if (!this.propertyValue.hasOwnProperty(property.name)) {
|
||||
this.propertyValue[property.name] = property.default as CredentialInformation;
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
credentialData () {
|
||||
this.init();
|
||||
},
|
||||
credentialTypeData () {
|
||||
this.init();
|
||||
},
|
||||
},
|
||||
mounted () {
|
||||
this.init();
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
.credentials-input-wrapper {
|
||||
.credential-name-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
margin-top: 2em;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.headline {
|
||||
font-weight: 600;
|
||||
color: $--color-primary;
|
||||
margin-bottom: 1em;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.headline-regular {
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.nodes-access-wrapper {
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
.no-nodes-access {
|
||||
margin: 1em 0;
|
||||
color: $--color-primary;
|
||||
line-height: 1.75em;
|
||||
}
|
||||
|
||||
.oauth-information {
|
||||
line-height: 2.5em;
|
||||
margin: 2em 0;
|
||||
|
||||
.google-icon {
|
||||
width: 191px;
|
||||
}
|
||||
}
|
||||
|
||||
.parameter-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 8px 0;
|
||||
|
||||
.parameter-name {
|
||||
position: relative;
|
||||
|
||||
&:hover {
|
||||
.parameter-info {
|
||||
display: inline;
|
||||
}
|
||||
}
|
||||
|
||||
.parameter-info {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.credentials-info {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.callback-url {
|
||||
position: relative;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
font-size: 0.9em;
|
||||
white-space: normal;
|
||||
overflow: visible;
|
||||
text-overflow: initial;
|
||||
color: #404040;
|
||||
text-align: left;
|
||||
direction: ltr;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.headline:hover,
|
||||
.headline-regular:hover {
|
||||
.credentials-info {
|
||||
display: inline;
|
||||
}
|
||||
}
|
||||
|
||||
.expanded .minimize-button {
|
||||
-webkit-transform: rotate(180deg);
|
||||
-moz-transform: rotate(180deg);
|
||||
-o-transform: rotate(180deg);
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.oauth-callback-headline {
|
||||
padding-top: 1em;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
|
@ -1,7 +1,5 @@
|
|||
<template>
|
||||
<div v-if="dialogVisible">
|
||||
<credentials-edit :dialogVisible="credentialEditDialogVisible" @closeDialog="closeCredentialEditDialog" @credentialsUpdated="reloadCredentialList" @credentialsCreated="reloadCredentialList" :setCredentialType="editCredentials && editCredentials.type" :editCredentials="editCredentials"></credentials-edit>
|
||||
|
||||
<el-dialog :visible="dialogVisible" append-to-body width="80%" title="Credentials" :before-close="closeDialog">
|
||||
<div class="text-very-light">
|
||||
Your saved credentials:
|
||||
|
@ -17,13 +15,9 @@
|
|||
/>
|
||||
</div>
|
||||
|
||||
<el-table :data="credentials" :default-sort = "{prop: 'name', order: 'ascending'}" stripe @row-click="editCredential" max-height="450" v-loading="isDataLoading">
|
||||
<el-table :data="credentialsToDisplay" :default-sort = "{prop: 'name', order: 'ascending'}" stripe max-height="450" @row-click="editCredential">
|
||||
<el-table-column property="name" label="Name" class-name="clickable" sortable></el-table-column>
|
||||
<el-table-column property="type" label="Type" class-name="clickable" sortable>
|
||||
<template slot-scope="scope">
|
||||
{{credentialTypeDisplayNames[scope.row.type]}}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column property="type" label="Type" class-name="clickable" sortable></el-table-column>
|
||||
<el-table-column property="createdAt" label="Created" class-name="clickable" sortable></el-table-column>
|
||||
<el-table-column property="updatedAt" label="Updated" class-name="clickable" sortable></el-table-column>
|
||||
<el-table-column
|
||||
|
@ -43,136 +37,89 @@
|
|||
|
||||
<script lang="ts">
|
||||
import { externalHooks } from '@/components/mixins/externalHooks';
|
||||
import { restApi } from '@/components/mixins/restApi';
|
||||
import { ICredentialsResponse } from '@/Interface';
|
||||
import { nodeHelpers } from '@/components/mixins/nodeHelpers';
|
||||
import { showMessage } from '@/components/mixins/showMessage';
|
||||
import CredentialsEdit from '@/components/CredentialsEdit.vue';
|
||||
import { genericHelpers } from '@/components/mixins/genericHelpers';
|
||||
|
||||
import { mapGetters } from "vuex";
|
||||
|
||||
import mixins from 'vue-typed-mixins';
|
||||
import { convertToDisplayDate } from './helpers';
|
||||
|
||||
export default mixins(
|
||||
externalHooks,
|
||||
genericHelpers,
|
||||
nodeHelpers,
|
||||
restApi,
|
||||
showMessage,
|
||||
).extend({
|
||||
name: 'CredentialsList',
|
||||
props: [
|
||||
'dialogVisible',
|
||||
],
|
||||
components: {
|
||||
CredentialsEdit,
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
credentialEditDialogVisible: false,
|
||||
credentialTypeDisplayNames: {} as { [key: string]: string; },
|
||||
credentials: [] as ICredentialsResponse[],
|
||||
displayAddCredentials: false,
|
||||
editCredentials: null as ICredentialsResponse | null,
|
||||
isDataLoading: false,
|
||||
};
|
||||
computed: {
|
||||
...mapGetters('credentials', ['allCredentials']),
|
||||
credentialsToDisplay() {
|
||||
return this.allCredentials.reduce((accu: ICredentialsResponse[], cred: ICredentialsResponse) => {
|
||||
const type = this.$store.getters['credentials/getCredentialTypeByName'](cred.type);
|
||||
|
||||
if (type) {
|
||||
accu.push({
|
||||
...cred,
|
||||
type: type.displayName,
|
||||
createdAt: convertToDisplayDate(cred.createdAt as number),
|
||||
updatedAt: convertToDisplayDate(cred.updatedAt as number),
|
||||
});
|
||||
}
|
||||
|
||||
return accu;
|
||||
}, []);
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
dialogVisible (newValue) {
|
||||
if (newValue) {
|
||||
this.loadCredentials();
|
||||
this.loadCredentialTypes();
|
||||
}
|
||||
this.$externalHooks().run('credentialsList.dialogVisibleChanged', { dialogVisible: newValue });
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
closeCredentialEditDialog () {
|
||||
this.credentialEditDialogVisible = false;
|
||||
},
|
||||
closeDialog () {
|
||||
// Handle the close externally as the visible parameter is an external prop
|
||||
// and is so not allowed to be changed here.
|
||||
this.$emit('closeDialog');
|
||||
return false;
|
||||
},
|
||||
|
||||
createCredential () {
|
||||
this.editCredentials = null;
|
||||
this.credentialEditDialogVisible = true;
|
||||
this.$store.dispatch('ui/openCredentialsSelectModal');
|
||||
},
|
||||
|
||||
editCredential (credential: ICredentialsResponse) {
|
||||
const editCredentials = {
|
||||
id: credential.id,
|
||||
name: credential.name,
|
||||
type: credential.type,
|
||||
} as ICredentialsResponse;
|
||||
|
||||
this.editCredentials = editCredentials;
|
||||
this.credentialEditDialogVisible = true;
|
||||
},
|
||||
reloadCredentialList () {
|
||||
this.loadCredentials();
|
||||
},
|
||||
loadCredentialTypes () {
|
||||
if (Object.keys(this.credentialTypeDisplayNames).length !== 0) {
|
||||
// Data is already loaded
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.$store.getters.allCredentialTypes === null) {
|
||||
// Data is not ready yet to be loaded
|
||||
return;
|
||||
}
|
||||
|
||||
for (const credentialType of this.$store.getters.allCredentialTypes) {
|
||||
this.credentialTypeDisplayNames[credentialType.name] = credentialType.displayName;
|
||||
}
|
||||
},
|
||||
loadCredentials () {
|
||||
this.isDataLoading = true;
|
||||
try {
|
||||
this.credentials = JSON.parse(JSON.stringify(this.$store.getters.allCredentials));
|
||||
} catch (error) {
|
||||
this.$showError(error, 'Problem loading credentials', 'There was a problem loading the credentials:');
|
||||
this.isDataLoading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
this.credentials.forEach((credentialData: ICredentialsResponse) => {
|
||||
credentialData.createdAt = this.convertToDisplayDate(credentialData.createdAt as number);
|
||||
credentialData.updatedAt = this.convertToDisplayDate(credentialData.updatedAt as number);
|
||||
});
|
||||
|
||||
this.isDataLoading = false;
|
||||
this.$store.dispatch('ui/openExisitngCredential', { id: credential.id});
|
||||
},
|
||||
|
||||
async deleteCredential (credential: ICredentialsResponse) {
|
||||
const deleteConfirmed = await this.confirmMessage(`Are you sure you want to delete "${credential.name}" credentials?`, 'Delete Credentials?', 'warning', 'Yes, delete!');
|
||||
const deleteConfirmed = await this.confirmMessage(`Are you sure you want to delete "${credential.name}" credentials?`, 'Delete Credentials?', null, 'Yes, delete!');
|
||||
|
||||
if (deleteConfirmed === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.restApi().deleteCredentials(credential.id!);
|
||||
await this.$store.dispatch('credentials/deleteCredential', {id: credential.id});
|
||||
} catch (error) {
|
||||
this.$showError(error, 'Problem deleting credentials', 'There was a problem deleting the credentials:');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove also from local store
|
||||
this.$store.commit('removeCredentials', credential);
|
||||
|
||||
// Now that the credentials got removed check if any nodes used them
|
||||
this.updateNodesCredentialsIssues();
|
||||
|
||||
this.$showMessage({
|
||||
title: 'Credentials deleted',
|
||||
message: `The credential "${credential.name}" got deleted!`,
|
||||
message: `The credential "${credential.name}" was deleted!`,
|
||||
type: 'success',
|
||||
});
|
||||
|
||||
// Refresh list
|
||||
this.loadCredentials();
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
107
packages/editor-ui/src/components/CredentialsSelectModal.vue
Normal file
107
packages/editor-ui/src/components/CredentialsSelectModal.vue
Normal file
|
@ -0,0 +1,107 @@
|
|||
<template>
|
||||
<Modal
|
||||
:name="modalName"
|
||||
:eventBus="modalBus"
|
||||
size="sm"
|
||||
>
|
||||
<template slot="header">
|
||||
<h2 :class="$style.title">Add new credential</h2>
|
||||
</template>
|
||||
<template slot="content">
|
||||
<div :class="$style.container">
|
||||
<div :class="$style.subtitle">Select an app or service to connect to</div>
|
||||
<n8n-select
|
||||
filterable
|
||||
defaultFirstOption
|
||||
placeholder="Search for app..."
|
||||
size="xlarge"
|
||||
ref="select"
|
||||
:value="selected"
|
||||
@change="onSelect"
|
||||
>
|
||||
<font-awesome-icon icon="search" slot="prefix" />
|
||||
<n8n-option
|
||||
v-for="credential in allCredentialTypes"
|
||||
:value="credential.name"
|
||||
:key="credential.name"
|
||||
:label="credential.displayName"
|
||||
filterable
|
||||
/>
|
||||
</n8n-select>
|
||||
</div>
|
||||
</template>
|
||||
<template slot="footer">
|
||||
<div :class="$style.footer">
|
||||
<n8n-button
|
||||
label="Continue"
|
||||
float="right"
|
||||
size="large"
|
||||
:disabled="!selected"
|
||||
@click="openCredentialType"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { mapGetters } from "vuex";
|
||||
|
||||
import Modal from './Modal.vue';
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'CredentialsSelectModal',
|
||||
components: {
|
||||
Modal,
|
||||
},
|
||||
mounted() {
|
||||
setTimeout(() => {
|
||||
const element = this.$refs.select as HTMLSelectElement;
|
||||
if (element) {
|
||||
element.focus();
|
||||
}
|
||||
}, 0);
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
modalBus: new Vue(),
|
||||
selected: '',
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters('credentials', ['allCredentialTypes']),
|
||||
},
|
||||
props: {
|
||||
modalName: {
|
||||
type: String,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onSelect(type: string) {
|
||||
this.selected = type;
|
||||
},
|
||||
openCredentialType () {
|
||||
this.modalBus.$emit('close');
|
||||
this.$store.dispatch('ui/openNewCredential', { type: this.selected });
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style module lang="scss">
|
||||
.container {
|
||||
margin-bottom: var(--spacing-l);
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: var(--font-size-xl);
|
||||
line-height: var(--font-line-height-regular);
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
margin-bottom: var(--spacing-s);
|
||||
font-size: var(--font-size-m);
|
||||
line-height: var(--font-line-height-xloose);
|
||||
}
|
||||
</style>
|
|
@ -1,39 +1,41 @@
|
|||
<template>
|
||||
<transition name="el-fade-in" @after-enter="showDocumentHelp = true">
|
||||
<div class="data-display-wrapper close-on-click" v-show="node" @click="close">
|
||||
<div class="data-display" >
|
||||
<NodeSettings @valueChanged="valueChanged" />
|
||||
<RunData />
|
||||
<div class="close-button clickable close-on-click" title="Close">
|
||||
<i class="el-icon-close close-on-click"></i>
|
||||
</div>
|
||||
<transition name="fade">
|
||||
<div v-if="showDocumentHelp && nodeType" class="doc-help-wrapper">
|
||||
<svg id="help-logo" v-if="showDocumentHelp && nodeType" :href="documentationUrl" target="_blank" width="18px" height="18px" viewBox="0 0 18 18" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<title>Node Documentation</title>
|
||||
<g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g transform="translate(-1127.000000, -836.000000)" fill-rule="nonzero">
|
||||
<g transform="translate(1117.000000, 825.000000)">
|
||||
<g transform="translate(10.000000, 11.000000)">
|
||||
<g transform="translate(2.250000, 2.250000)" fill="#FF6150">
|
||||
<path d="M6,11.25 L7.5,11.25 L7.5,9.75 L6,9.75 L6,11.25 M6.75,2.25 C5.09314575,2.25 3.75,3.59314575 3.75,5.25 L5.25,5.25 C5.25,4.42157288 5.92157288,3.75 6.75,3.75 C7.57842712,3.75 8.25,4.42157288 8.25,5.25 C8.25,6.75 6,6.5625 6,9 L7.5,9 C7.5,7.3125 9.75,7.125 9.75,5.25 C9.75,3.59314575 8.40685425,2.25 6.75,2.25 M1.5,0 L12,0 C12.8284271,0 13.5,0.671572875 13.5,1.5 L13.5,12 C13.5,12.8284271 12.8284271,13.5 12,13.5 L1.5,13.5 C0.671572875,13.5 0,12.8284271 0,12 L0,1.5 C0,0.671572875 0.671572875,0 1.5,0 Z"></path>
|
||||
</g>
|
||||
<rect x="0" y="0" width="18" height="18"></rect>
|
||||
</g>
|
||||
<el-dialog
|
||||
:visible="!!node"
|
||||
:before-close="close"
|
||||
:custom-class="`classic data-display-wrapper`"
|
||||
width="80%"
|
||||
append-to-body
|
||||
@opened="showDocumentHelp = true"
|
||||
>
|
||||
<div class="data-display" >
|
||||
<NodeSettings @valueChanged="valueChanged" />
|
||||
<RunData />
|
||||
|
||||
</div>
|
||||
<transition name="fade">
|
||||
<div v-if="nodeType && showDocumentHelp" class="doc-help-wrapper">
|
||||
<svg id="help-logo" :href="documentationUrl" target="_blank" width="18px" height="18px" viewBox="0 0 18 18" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<title>Node Documentation</title>
|
||||
<g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g transform="translate(-1127.000000, -836.000000)" fill-rule="nonzero">
|
||||
<g transform="translate(1117.000000, 825.000000)">
|
||||
<g transform="translate(10.000000, 11.000000)">
|
||||
<g transform="translate(2.250000, 2.250000)" fill="#FF6150">
|
||||
<path d="M6,11.25 L7.5,11.25 L7.5,9.75 L6,9.75 L6,11.25 M6.75,2.25 C5.09314575,2.25 3.75,3.59314575 3.75,5.25 L5.25,5.25 C5.25,4.42157288 5.92157288,3.75 6.75,3.75 C7.57842712,3.75 8.25,4.42157288 8.25,5.25 C8.25,6.75 6,6.5625 6,9 L7.5,9 C7.5,7.3125 9.75,7.125 9.75,5.25 C9.75,3.59314575 8.40685425,2.25 6.75,2.25 M1.5,0 L12,0 C12.8284271,0 13.5,0.671572875 13.5,1.5 L13.5,12 C13.5,12.8284271 12.8284271,13.5 12,13.5 L1.5,13.5 C0.671572875,13.5 0,12.8284271 0,12 L0,1.5 C0,0.671572875 0.671572875,0 1.5,0 Z"></path>
|
||||
</g>
|
||||
<rect x="0" y="0" width="18" height="18"></rect>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
<div v-if="showDocumentHelp && nodeType" class="text">
|
||||
Need help? <a id="doc-hyperlink" v-if="showDocumentHelp && nodeType" :href="documentationUrl" target="_blank" @click="onDocumentationUrlClick">Open {{nodeType.displayName}} documentation</a>
|
||||
</div>
|
||||
<div class="text">
|
||||
Need help? <a id="doc-hyperlink" :href="documentationUrl" target="_blank" @click="onDocumentationUrlClick">Open {{nodeType.displayName}} documentation</a>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
|
||||
</transition>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
|
@ -102,13 +104,10 @@ export default mixins(externalHooks, nodeHelpers, workflowHelpers).extend({
|
|||
nodeTypeSelected (nodeTypeName: string) {
|
||||
this.$emit('nodeTypeSelected', nodeTypeName);
|
||||
},
|
||||
close (e: MouseEvent) {
|
||||
// @ts-ignore
|
||||
if (e.target.className && e.target.className.includes && e.target.className.includes('close-on-click')) {
|
||||
this.$externalHooks().run('dataDisplay.nodeEditingFinished');
|
||||
this.showDocumentHelp = false;
|
||||
this.$store.commit('setActiveNode', null);
|
||||
}
|
||||
close () {
|
||||
this.$externalHooks().run('dataDisplay.nodeEditingFinished');
|
||||
this.showDocumentHelp = false;
|
||||
this.$store.commit('setActiveNode', null);
|
||||
},
|
||||
onDocumentationUrlClick () {
|
||||
this.$externalHooks().run('dataDisplay.onDocumentationUrlClick', { nodeType: this.nodeType, documentationUrl: this.documentationUrl });
|
||||
|
@ -119,105 +118,69 @@ export default mixins(externalHooks, nodeHelpers, workflowHelpers).extend({
|
|||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
|
||||
.data-display-wrapper {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 20;
|
||||
background-color: #9d8d9dd8;
|
||||
height: 85%;
|
||||
|
||||
.close-button {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: -50px;
|
||||
color: #fff;
|
||||
background-color: $--custom-header-background;
|
||||
border-radius: 0 18px 18px 0;
|
||||
z-index: 110;
|
||||
font-size: 1.7em;
|
||||
text-align: center;
|
||||
line-height: 50px;
|
||||
height: 50px;
|
||||
width: 50px;
|
||||
|
||||
.close-on-click {
|
||||
color: #fff;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.close-on-click:hover {
|
||||
transform: scale(1.2);
|
||||
}
|
||||
.el-dialog__header {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.data-display {
|
||||
position: relative;
|
||||
width: 80%;
|
||||
height: 80%;
|
||||
margin: 3em auto;
|
||||
background-color: #fff;
|
||||
border-radius: 2px;
|
||||
@media (max-height: 720px) {
|
||||
margin: 1em auto;
|
||||
height: 95%;
|
||||
}
|
||||
|
||||
.fade-enter-active, .fade-enter-to, .fade-leave-active {
|
||||
transition: all .75s ease;
|
||||
}
|
||||
|
||||
.fade-enter, .fade-leave-to /* .fade-leave-active below version 2.1.8 */ {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.doc-help-wrapper {
|
||||
transition-delay: 2s;
|
||||
background-color: #fff;
|
||||
margin-top: 1%;
|
||||
box-sizing: border-box;
|
||||
border: 1px solid #DCDFE6;
|
||||
border-radius: 4px;
|
||||
background-color: #FFFFFF;
|
||||
box-shadow: 0 2px 7px 0 rgba(0,0,0,0.15);
|
||||
min-width: 319px;
|
||||
height: 40px;
|
||||
float: right;
|
||||
padding: 5px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
padding-top: 10px;
|
||||
padding-right: 12px;
|
||||
|
||||
#help-logo {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.text {
|
||||
margin-left: 5px;
|
||||
flex: 9;
|
||||
font-family: "Open Sans";
|
||||
color: #666666;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0;
|
||||
line-height: 17px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
#doc-hyperlink, #doc-hyperlink:visited, #doc-hyperlink:focus, #doc-hyperlink:active {
|
||||
text-decoration: none;
|
||||
color: #FF6150;
|
||||
}
|
||||
}
|
||||
.el-dialog__body {
|
||||
padding: 0 !important;
|
||||
min-height: 400px;
|
||||
overflow: hidden;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
.data-display {
|
||||
background-color: #fff;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.doc-help-wrapper {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
transition-delay: 2s;
|
||||
background-color: #fff;
|
||||
margin-top: 1%;
|
||||
box-sizing: border-box;
|
||||
border: 1px solid #DCDFE6;
|
||||
border-radius: 4px;
|
||||
background-color: #FFFFFF;
|
||||
box-shadow: 0 2px 7px 0 rgba(0,0,0,0.15);
|
||||
min-width: 319px;
|
||||
height: 40px;
|
||||
float: right;
|
||||
padding: 5px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
padding-top: 10px;
|
||||
padding-right: 12px;
|
||||
|
||||
#help-logo {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.text {
|
||||
margin-left: 5px;
|
||||
flex: 9;
|
||||
font-family: "Open Sans";
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
line-height: 17px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.fade-enter-active, .fade-enter-to, .fade-leave-active {
|
||||
transition: all .75s ease;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.fade-enter, .fade-leave-to /* .fade-leave-active below version 2.1.8 */ {
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -173,6 +173,10 @@ import {
|
|||
IWorkflowShortResponse,
|
||||
} from '@/Interface';
|
||||
|
||||
import {
|
||||
convertToDisplayDate,
|
||||
} from './helpers';
|
||||
|
||||
import {
|
||||
IDataObject,
|
||||
} from 'n8n-workflow';
|
||||
|
@ -319,6 +323,7 @@ export default mixins(
|
|||
}
|
||||
return false;
|
||||
},
|
||||
convertToDisplayDate,
|
||||
displayExecution (execution: IExecutionShortResponse) {
|
||||
this.$router.push({
|
||||
name: 'ExecutionById',
|
||||
|
@ -380,7 +385,7 @@ export default mixins(
|
|||
|
||||
this.$showMessage({
|
||||
title: 'Execution deleted',
|
||||
message: 'The executions got deleted!',
|
||||
message: 'The executions were deleted!',
|
||||
type: 'success',
|
||||
});
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<div v-if="dialogVisible" @keydown.stop>
|
||||
<el-dialog :visible="dialogVisible" custom-class="expression-dialog" append-to-body width="80%" title="Edit Expression" :before-close="closeDialog">
|
||||
<el-dialog :visible="dialogVisible" custom-class="expression-dialog classic" append-to-body width="80%" title="Edit Expression" :before-close="closeDialog">
|
||||
<el-row>
|
||||
<el-col :span="8">
|
||||
<div class="header-side-menu">
|
||||
|
@ -145,11 +145,14 @@ export default mixins(
|
|||
|
||||
.right-side {
|
||||
background-color: #f9f9f9;
|
||||
border-top-right-radius: 8px;
|
||||
border-bottom-right-radius: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.header-side-menu {
|
||||
padding: 1em 0 0.5em 1.8em;
|
||||
border-top-left-radius: 8px;
|
||||
|
||||
background-color: $--custom-window-sidebar-top;
|
||||
color: #555;
|
||||
|
|
124
packages/editor-ui/src/components/InlineNameEdit.vue
Normal file
124
packages/editor-ui/src/components/InlineNameEdit.vue
Normal file
|
@ -0,0 +1,124 @@
|
|||
<template>
|
||||
<div :class="$style.container">
|
||||
<div
|
||||
:class="$style.headline"
|
||||
@keydown.stop
|
||||
@click="enableNameEdit"
|
||||
v-click-outside="disableNameEdit"
|
||||
>
|
||||
<div v-if="!isNameEdit">
|
||||
<span>{{ name }}</span>
|
||||
<i><font-awesome-icon icon="pen" /></i>
|
||||
</div>
|
||||
<div v-else :class="$style.nameInput">
|
||||
<n8n-input
|
||||
:value="name"
|
||||
size="xlarge"
|
||||
ref="nameInput"
|
||||
@input="onNameEdit"
|
||||
@change="disableNameEdit"
|
||||
:maxlength="64"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div :class="$style.subtitle" v-if="!isNameEdit">{{ subtitle }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import mixins from 'vue-typed-mixins';
|
||||
import { showMessage } from './mixins/showMessage';
|
||||
|
||||
export default mixins(showMessage).extend({
|
||||
name: 'InlineNameEdit',
|
||||
props: {
|
||||
name: {
|
||||
type: String,
|
||||
},
|
||||
subtitle: {
|
||||
type: String,
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isNameEdit: false,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
onNameEdit(value: string) {
|
||||
this.$emit('input', value);
|
||||
},
|
||||
enableNameEdit() {
|
||||
this.isNameEdit = true;
|
||||
|
||||
setTimeout(() => {
|
||||
const input = this.$refs.nameInput as HTMLInputElement;
|
||||
if (input) {
|
||||
input.focus();
|
||||
}
|
||||
}, 0);
|
||||
},
|
||||
disableNameEdit() {
|
||||
if (!this.name) {
|
||||
this.$emit('input', `Untitled ${this.type}`);
|
||||
|
||||
this.$showWarning('Error', `${this.type} name cannot be empty`);
|
||||
}
|
||||
|
||||
this.isNameEdit = false;
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
<style module lang="scss">
|
||||
.container {
|
||||
min-height: 36px;
|
||||
}
|
||||
|
||||
.headline {
|
||||
font-size: var(--font-size-m);
|
||||
line-height: 1.4;
|
||||
margin-bottom: var(--spacing-5xs);
|
||||
display: inline-block;
|
||||
cursor: pointer;
|
||||
padding: 0 var(--spacing-4xs);
|
||||
border-radius: var(--border-radius-base);
|
||||
position: relative;
|
||||
min-height: 22px;
|
||||
max-height: 22px;
|
||||
font-weight: 400;
|
||||
|
||||
i {
|
||||
display: var(--headline-icon-display, none);
|
||||
font-size: 0.75em;
|
||||
margin-left: 8px;
|
||||
color: var(--color-text-base);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-background-base);
|
||||
--headline-icon-display: inline-flex;
|
||||
}
|
||||
}
|
||||
|
||||
.nameInput {
|
||||
z-index: 1;
|
||||
position: absolute;
|
||||
top: -13px;
|
||||
left: -9px;
|
||||
width: 400px;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: var(--font-size-2xs);
|
||||
color: var(--color-text-light);
|
||||
margin-left: 4px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
</style>
|
|
@ -46,24 +46,6 @@ export default mixins(
|
|||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.el-menu--horizontal>.el-menu-item,
|
||||
.el-menu--horizontal>.el-submenu .el-submenu__title,
|
||||
.el-menu-item {
|
||||
height: 65px;
|
||||
line-height: 65px;
|
||||
}
|
||||
|
||||
.el-submenu .el-submenu__title,
|
||||
.el-menu--horizontal>.el-menu-item,
|
||||
.el-menu.el-menu--horizontal {
|
||||
border: none !important;
|
||||
}
|
||||
.el-menu--popup-bottom-start {
|
||||
margin-top: 0px;
|
||||
border-top: 1px solid #464646;
|
||||
border-radius: 0 0 2px 2px;
|
||||
}
|
||||
|
||||
.main-header {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
|
|
|
@ -65,7 +65,11 @@
|
|||
<span>Active:</span>
|
||||
<WorkflowActivator :workflow-active="isWorkflowActive" :workflow-id="currentWorkflowId" :disabled="!currentWorkflowId"/>
|
||||
</span>
|
||||
<SaveWorkflowButton />
|
||||
<SaveButton
|
||||
:saved="!this.isDirty && !this.isNewWorkflow"
|
||||
:disabled="isWorkflowSaving"
|
||||
@click="saveCurrentWorkflow"
|
||||
/>
|
||||
</template>
|
||||
</PushConnectionTracker>
|
||||
</div>
|
||||
|
@ -82,7 +86,7 @@ import TagsContainer from "@/components/TagsContainer.vue";
|
|||
import PushConnectionTracker from "@/components/PushConnectionTracker.vue";
|
||||
import WorkflowActivator from "@/components/WorkflowActivator.vue";
|
||||
import { workflowHelpers } from "@/components/mixins/workflowHelpers";
|
||||
import SaveWorkflowButton from "@/components/SaveWorkflowButton.vue";
|
||||
import SaveButton from "@/components/SaveButton.vue";
|
||||
import TagsDropdown from "@/components/TagsDropdown.vue";
|
||||
import InlineTextEdit from "@/components/InlineTextEdit.vue";
|
||||
import BreakpointsObserver from "@/components/BreakpointsObserver.vue";
|
||||
|
@ -103,7 +107,7 @@ export default mixins(workflowHelpers).extend({
|
|||
PushConnectionTracker,
|
||||
WorkflowNameShort,
|
||||
WorkflowActivator,
|
||||
SaveWorkflowButton,
|
||||
SaveButton,
|
||||
TagsDropdown,
|
||||
InlineTextEdit,
|
||||
BreakpointsObserver,
|
||||
|
@ -125,6 +129,9 @@ export default mixins(workflowHelpers).extend({
|
|||
isDirty: "getStateIsDirty",
|
||||
currentWorkflowTagIds: "workflowTags",
|
||||
}),
|
||||
isNewWorkflow(): boolean {
|
||||
return !this.$route.params.name;
|
||||
},
|
||||
isWorkflowSaving(): boolean {
|
||||
return this.$store.getters.isActionActive("workflowSaving");
|
||||
},
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
<about :dialogVisible="aboutDialogVisible" @closeDialog="closeAboutDialog"></about>
|
||||
<executions-list :dialogVisible="executionsListDialogVisible" @closeDialog="closeExecutionsListOpenDialog"></executions-list>
|
||||
<credentials-list :dialogVisible="credentialOpenDialogVisible" @closeDialog="closeCredentialOpenDialog"></credentials-list>
|
||||
<credentials-edit :dialogVisible="credentialNewDialogVisible" @closeDialog="closeCredentialNewDialog"></credentials-edit>
|
||||
<workflow-settings :dialogVisible="workflowSettingsDialogVisible" @closeDialog="closeWorkflowSettingsDialog"></workflow-settings>
|
||||
<input type="file" ref="importFile" style="display: none" v-on:change="handleFileImport()">
|
||||
|
||||
|
@ -11,105 +10,105 @@
|
|||
<div id="collapse-change-button" class="clickable" @click="toggleCollapse">
|
||||
<font-awesome-icon icon="angle-right" class="icon" />
|
||||
</div>
|
||||
<el-menu default-active="workflow" @select="handleSelect" :collapse="isCollapsed">
|
||||
<n8n-menu default-active="workflow" @select="handleSelect" :collapse="isCollapsed">
|
||||
|
||||
<el-menu-item index="logo" class="logo-item">
|
||||
<n8n-menu-item index="logo" class="logo-item">
|
||||
<a href="https://n8n.io" target="_blank" class="logo">
|
||||
<img :src="basePath + 'n8n-icon-small.png'" class="icon" alt="n8n.io"/>
|
||||
<span class="logo-text" slot="title">n8n.io</span>
|
||||
</a>
|
||||
</el-menu-item>
|
||||
</n8n-menu-item>
|
||||
|
||||
<MenuItemsIterator :items="sidebarMenuTopItems" :root="true"/>
|
||||
|
||||
<el-submenu index="workflow" title="Workflow">
|
||||
<el-submenu index="workflow" title="Workflow" popperClass="sidebar-popper">
|
||||
<template slot="title">
|
||||
<font-awesome-icon icon="network-wired"/>
|
||||
<span slot="title" class="item-title-root">Workflows</span>
|
||||
</template>
|
||||
|
||||
<el-menu-item index="workflow-new">
|
||||
<n8n-menu-item index="workflow-new">
|
||||
<template slot="title">
|
||||
<font-awesome-icon icon="file"/>
|
||||
<span slot="title" class="item-title">New</span>
|
||||
</template>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="workflow-open">
|
||||
</n8n-menu-item>
|
||||
<n8n-menu-item index="workflow-open">
|
||||
<template slot="title">
|
||||
<font-awesome-icon icon="folder-open"/>
|
||||
<span slot="title" class="item-title">Open</span>
|
||||
</template>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="workflow-save">
|
||||
</n8n-menu-item>
|
||||
<n8n-menu-item index="workflow-save">
|
||||
<template slot="title">
|
||||
<font-awesome-icon icon="save"/>
|
||||
<span slot="title" class="item-title">Save</span>
|
||||
</template>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="workflow-duplicate" :disabled="!currentWorkflow">
|
||||
</n8n-menu-item>
|
||||
<n8n-menu-item index="workflow-duplicate" :disabled="!currentWorkflow">
|
||||
<template slot="title">
|
||||
<font-awesome-icon icon="copy"/>
|
||||
<span slot="title" class="item-title">Duplicate</span>
|
||||
</template>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="workflow-delete" :disabled="!currentWorkflow">
|
||||
</n8n-menu-item>
|
||||
<n8n-menu-item index="workflow-delete" :disabled="!currentWorkflow">
|
||||
<template slot="title">
|
||||
<font-awesome-icon icon="trash"/>
|
||||
<span slot="title" class="item-title">Delete</span>
|
||||
</template>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="workflow-download">
|
||||
</n8n-menu-item>
|
||||
<n8n-menu-item index="workflow-download">
|
||||
<template slot="title">
|
||||
<font-awesome-icon icon="file-download"/>
|
||||
<span slot="title" class="item-title">Download</span>
|
||||
</template>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="workflow-import-url">
|
||||
</n8n-menu-item>
|
||||
<n8n-menu-item index="workflow-import-url">
|
||||
<template slot="title">
|
||||
<font-awesome-icon icon="cloud"/>
|
||||
<span slot="title" class="item-title">Import from URL</span>
|
||||
</template>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="workflow-import-file">
|
||||
</n8n-menu-item>
|
||||
<n8n-menu-item index="workflow-import-file">
|
||||
<template slot="title">
|
||||
<font-awesome-icon icon="hdd"/>
|
||||
<span slot="title" class="item-title">Import from File</span>
|
||||
</template>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="workflow-settings" :disabled="!currentWorkflow">
|
||||
</n8n-menu-item>
|
||||
<n8n-menu-item index="workflow-settings" :disabled="!currentWorkflow">
|
||||
<template slot="title">
|
||||
<font-awesome-icon icon="cog"/>
|
||||
<span slot="title" class="item-title">Settings</span>
|
||||
</template>
|
||||
</el-menu-item>
|
||||
</n8n-menu-item>
|
||||
</el-submenu>
|
||||
|
||||
<el-submenu index="credentials" title="Credentials">
|
||||
<el-submenu index="credentials" title="Credentials" popperClass="sidebar-popper">
|
||||
<template slot="title">
|
||||
<font-awesome-icon icon="key"/>
|
||||
<span slot="title" class="item-title-root">Credentials</span>
|
||||
</template>
|
||||
|
||||
<el-menu-item index="credentials-new">
|
||||
<n8n-menu-item index="credentials-new">
|
||||
<template slot="title">
|
||||
<font-awesome-icon icon="file"/>
|
||||
<span slot="title" class="item-title">New</span>
|
||||
</template>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="credentials-open">
|
||||
</n8n-menu-item>
|
||||
<n8n-menu-item index="credentials-open">
|
||||
<template slot="title">
|
||||
<font-awesome-icon icon="folder-open"/>
|
||||
<span slot="title" class="item-title">Open</span>
|
||||
</template>
|
||||
</el-menu-item>
|
||||
</n8n-menu-item>
|
||||
</el-submenu>
|
||||
|
||||
<el-menu-item index="executions">
|
||||
<n8n-menu-item index="executions">
|
||||
<font-awesome-icon icon="tasks"/>
|
||||
<span slot="title" class="item-title-root">Executions</span>
|
||||
</el-menu-item>
|
||||
</n8n-menu-item>
|
||||
|
||||
<el-submenu index="help" class="help-menu" title="Help">
|
||||
<el-submenu index="help" class="help-menu" title="Help" popperClass="sidebar-popper">
|
||||
<template slot="title">
|
||||
<font-awesome-icon icon="question"/>
|
||||
<span slot="title" class="item-title-root">Help</span>
|
||||
|
@ -117,25 +116,25 @@
|
|||
|
||||
<MenuItemsIterator :items="helpMenuItems" />
|
||||
|
||||
<el-menu-item index="help-about">
|
||||
<n8n-menu-item index="help-about">
|
||||
<template slot="title">
|
||||
<font-awesome-icon class="about-icon" icon="info"/>
|
||||
<span slot="title" class="item-title">About n8n</span>
|
||||
</template>
|
||||
</el-menu-item>
|
||||
</n8n-menu-item>
|
||||
</el-submenu>
|
||||
|
||||
<MenuItemsIterator :items="sidebarMenuBottomItems" :root="true"/>
|
||||
|
||||
<div class="footer-menu-items">
|
||||
<el-menu-item index="updates" class="updates" v-if="hasVersionUpdates" @click="openUpdatesPanel">
|
||||
<n8n-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>
|
||||
</n8n-menu-item>
|
||||
</div>
|
||||
</el-menu>
|
||||
</n8n-menu>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
@ -153,7 +152,6 @@ import {
|
|||
} from '../Interface';
|
||||
|
||||
import About from '@/components/About.vue';
|
||||
import CredentialsEdit from '@/components/CredentialsEdit.vue';
|
||||
import CredentialsList from '@/components/CredentialsList.vue';
|
||||
import ExecutionsList from '@/components/ExecutionsList.vue';
|
||||
import GiftNotificationIcon from './GiftNotificationIcon.vue';
|
||||
|
@ -217,7 +215,6 @@ export default mixins(
|
|||
name: 'MainHeader',
|
||||
components: {
|
||||
About,
|
||||
CredentialsEdit,
|
||||
CredentialsList,
|
||||
ExecutionsList,
|
||||
GiftNotificationIcon,
|
||||
|
@ -229,7 +226,6 @@ export default mixins(
|
|||
aboutDialogVisible: false,
|
||||
// @ts-ignore
|
||||
basePath: this.$store.getters.getBaseUrl,
|
||||
credentialNewDialogVisible: false,
|
||||
credentialOpenDialogVisible: false,
|
||||
executionsListDialogVisible: false,
|
||||
stopExecutionInProgress: false,
|
||||
|
@ -318,9 +314,6 @@ export default mixins(
|
|||
closeCredentialOpenDialog () {
|
||||
this.credentialOpenDialogVisible = false;
|
||||
},
|
||||
closeCredentialNewDialog () {
|
||||
this.credentialNewDialogVisible = false;
|
||||
},
|
||||
openTagManager() {
|
||||
this.$store.dispatch('ui/openTagsManagerModal');
|
||||
},
|
||||
|
@ -414,8 +407,8 @@ export default mixins(
|
|||
// Reset tab title since workflow is deleted.
|
||||
this.$titleReset();
|
||||
this.$showMessage({
|
||||
title: 'Workflow got deleted',
|
||||
message: `The workflow "${this.workflowName}" got deleted!`,
|
||||
title: 'Workflow was deleted',
|
||||
message: `The workflow "${this.workflowName}" was deleted!`,
|
||||
type: 'success',
|
||||
});
|
||||
|
||||
|
@ -477,7 +470,7 @@ export default mixins(
|
|||
} else if (key === 'credentials-open') {
|
||||
this.credentialOpenDialogVisible = true;
|
||||
} else if (key === 'credentials-new') {
|
||||
this.credentialNewDialogVisible = true;
|
||||
this.$store.dispatch('ui/openCredentialsSelectModal');
|
||||
} else if (key === 'execution-open-workflow') {
|
||||
if (this.workflowExecution !== null) {
|
||||
this.openWorkflow(this.workflowExecution.workflowId as string);
|
||||
|
@ -491,6 +484,103 @@ export default mixins(
|
|||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.sidebar-popper{
|
||||
.el-menu-item {
|
||||
font-size: 0.9em;
|
||||
height: 35px;
|
||||
line-height: 35px;
|
||||
color: $--custom-dialog-text-color;
|
||||
--menu-item-hover-fill: #fff0ef;
|
||||
|
||||
.item-title {
|
||||
position: absolute;
|
||||
left: 55px;
|
||||
}
|
||||
|
||||
.svg-inline--fa {
|
||||
position: relative;
|
||||
right: -3px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#side-menu {
|
||||
// Menu
|
||||
.el-menu--vertical,
|
||||
.el-menu {
|
||||
border: none;
|
||||
font-size: 14px;
|
||||
--menu-item-hover-fill: #fff0ef;
|
||||
|
||||
.el-menu--collapse {
|
||||
width: 75px;
|
||||
}
|
||||
|
||||
.el-menu--popup,
|
||||
.el-menu--inline {
|
||||
font-size: 0.9em;
|
||||
li.el-menu-item {
|
||||
height: 35px;
|
||||
line-height: 35px;
|
||||
color: $--custom-dialog-text-color;
|
||||
}
|
||||
}
|
||||
|
||||
.el-menu-item,
|
||||
.el-submenu__title {
|
||||
color: $--color-primary;
|
||||
font-size: 1.2em;
|
||||
.el-submenu__icon-arrow {
|
||||
color: $--color-primary;
|
||||
font-weight: 800;
|
||||
font-size: 1em;
|
||||
}
|
||||
.svg-inline--fa {
|
||||
position: relative;
|
||||
right: -3px;
|
||||
}
|
||||
.item-title {
|
||||
position: absolute;
|
||||
left: 73px;
|
||||
}
|
||||
.item-title-root {
|
||||
position: absolute;
|
||||
left: 60px;
|
||||
top: 1px;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.el-menu-item {
|
||||
a {
|
||||
color: #666;
|
||||
|
||||
&.primary-item {
|
||||
color: $--color-primary;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
}
|
||||
|
||||
&.logo-item {
|
||||
background-color: $--color-primary !important;
|
||||
height: $--header-height;
|
||||
line-height: $--header-height;
|
||||
* {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
|
||||
.icon {
|
||||
position: relative;
|
||||
height: 23px;
|
||||
left: -10px;
|
||||
top: -2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.about-icon {
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
@ -530,29 +620,6 @@ export default mixins(
|
|||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.el-menu-item {
|
||||
a {
|
||||
color: #666;
|
||||
|
||||
&.primary-item {
|
||||
color: $--color-primary;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
}
|
||||
|
||||
&.logo-item {
|
||||
background-color: $--color-primary !important;
|
||||
height: $--header-height;
|
||||
|
||||
.icon {
|
||||
position: relative;
|
||||
height: 23px;
|
||||
left: -10px;
|
||||
top: -2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
a.logo {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
@ -605,7 +672,7 @@ a.logo {
|
|||
}
|
||||
|
||||
.el-menu-item.updates {
|
||||
color: $--sidebar-inactive-color;
|
||||
color: $--sidebar-inactive-color !important;
|
||||
.item-title-root {
|
||||
font-size: 13px;
|
||||
top: 0 !important;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<div>
|
||||
<el-menu-item
|
||||
<n8n-menu-item
|
||||
v-for="item in items"
|
||||
:key="item.id"
|
||||
:index="item.id"
|
||||
|
@ -8,7 +8,7 @@
|
|||
>
|
||||
<font-awesome-icon :icon="item.properties.icon" />
|
||||
<span slot="title" :class="{'item-title-root': root, 'item-title': !root}">{{ item.properties.title }}</span>
|
||||
</el-menu-item>
|
||||
</n8n-menu-item>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -41,4 +41,4 @@ export default Vue.extend({
|
|||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
</script>
|
||||
|
|
|
@ -21,15 +21,20 @@
|
|||
:title="title"
|
||||
:class="{ 'dialog-wrapper': true, [size]: true }"
|
||||
:width="width"
|
||||
:show-close="showClose"
|
||||
:custom-class="getCustomClass()"
|
||||
append-to-body
|
||||
>
|
||||
<template v-slot:title>
|
||||
<slot name="header" />
|
||||
<slot name="header" v-if="!loading" />
|
||||
</template>
|
||||
<div class="modal-content" @keydown.stop @keydown.enter="handleEnter" @keydown.esc="closeDialog">
|
||||
<slot name="content"/>
|
||||
<slot v-if="!loading" name="content"/>
|
||||
<div class="loader" v-else>
|
||||
<n8n-spinner />
|
||||
</div>
|
||||
</div>
|
||||
<el-row class="modal-footer">
|
||||
<el-row v-if="!loading" class="modal-footer">
|
||||
<slot name="footer" :close="closeDialog" />
|
||||
</el-row>
|
||||
</el-dialog>
|
||||
|
@ -41,13 +46,14 @@ import Vue from "vue";
|
|||
|
||||
const sizeMap: {[size: string]: string} = {
|
||||
xl: '80%',
|
||||
lg: '70%',
|
||||
m: '50%',
|
||||
default: '50%',
|
||||
};
|
||||
|
||||
export default Vue.extend({
|
||||
name: "Modal",
|
||||
props: ['name', 'title', 'eventBus', 'size', 'drawer', 'drawerDirection', 'drawerWidth', 'visible'],
|
||||
props: ['name', 'title', 'eventBus', 'size', 'drawer', 'drawerDirection', 'drawerWidth', 'visible', 'showClose', 'loading', 'classic', 'beforeClose', 'customClass'],
|
||||
data() {
|
||||
return {
|
||||
visibleDrawer: this.drawer,
|
||||
|
@ -86,6 +92,17 @@ export default Vue.extend({
|
|||
}
|
||||
},
|
||||
closeDialog(callback?: () => void) {
|
||||
if (this.beforeClose) {
|
||||
this.beforeClose(() => {
|
||||
this.$store.commit('ui/closeTopModal');
|
||||
if (typeof callback === 'function') {
|
||||
callback();
|
||||
}
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.$store.commit('ui/closeTopModal');
|
||||
if (typeof callback === 'function') {
|
||||
callback();
|
||||
|
@ -98,6 +115,15 @@ export default Vue.extend({
|
|||
this.visibleDrawer = true;
|
||||
}, 300); // delayed for closing animation to take effect
|
||||
},
|
||||
getCustomClass() {
|
||||
let classes = this.$props.customClass || '';
|
||||
|
||||
if (this.$props.classic) {
|
||||
classes = `${classes} classic`;
|
||||
}
|
||||
|
||||
return classes;
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
width(): string {
|
||||
|
@ -123,22 +149,31 @@ export default Vue.extend({
|
|||
overflow: hidden;
|
||||
}
|
||||
|
||||
.dialog-wrapper {
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.el-dialog__body {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.dialog-wrapper {
|
||||
&.xl > div, &.md > div {
|
||||
min-width: 620px;
|
||||
}
|
||||
|
||||
&.lg > div {
|
||||
height: 80%;
|
||||
overflow: hidden;
|
||||
|
||||
.modal-content {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
&.sm {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
> div {
|
||||
max-width: 420px;
|
||||
max-width: 460px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -146,4 +181,13 @@ export default Vue.extend({
|
|||
.modal-content > .el-row {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.loader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--color-primary-tint-1);
|
||||
font-size: 30px;
|
||||
height: 80%;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -2,7 +2,13 @@
|
|||
<div
|
||||
v-if="isOpen(name) || keepAlive"
|
||||
>
|
||||
<slot :modalName="name" :active="isActive(name)" :open="isOpen(name)"></slot>
|
||||
<slot
|
||||
:modalName="name"
|
||||
:active="isActive(name)"
|
||||
:open="isOpen(name)"
|
||||
:activeId="getActiveId(name)"
|
||||
:mode="getMode(name)"
|
||||
></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -19,6 +25,12 @@ export default Vue.extend({
|
|||
isOpen(name: string) {
|
||||
return this.$store.getters['ui/isModalOpen'](name);
|
||||
},
|
||||
getMode(name: string) {
|
||||
return this.$store.getters['ui/getModalMode'](name);
|
||||
},
|
||||
getActiveId(name: string) {
|
||||
return this.$store.getters['ui/getModalActiveId'](name);
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -24,6 +24,7 @@
|
|||
/>
|
||||
</template>
|
||||
</ModalRoot>
|
||||
|
||||
<ModalRoot :name="VERSIONS_MODAL_KEY" :keepAlive="true">
|
||||
<template v-slot="{ modalName, open }">
|
||||
<UpdatesPanel
|
||||
|
@ -32,33 +33,57 @@
|
|||
/>
|
||||
</template>
|
||||
</ModalRoot>
|
||||
|
||||
<ModalRoot :name="CREDENTIAL_EDIT_MODAL_KEY">
|
||||
<template v-slot="{ modalName, activeId, mode }">
|
||||
<CredentialEdit
|
||||
:modalName="modalName"
|
||||
:mode="mode"
|
||||
:activeId="activeId"
|
||||
/>
|
||||
</template>
|
||||
</ModalRoot>
|
||||
|
||||
<ModalRoot :name="CREDENTIAL_SELECT_MODAL_KEY">
|
||||
<template v-slot="{ modalName }">
|
||||
<CredentialsSelectModal
|
||||
:modalName="modalName"
|
||||
/>
|
||||
</template>
|
||||
</ModalRoot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from "vue";
|
||||
import { DUPLICATE_MODAL_KEY, TAGS_MANAGER_MODAL_KEY, WORKLOW_OPEN_MODAL_KEY, VERSIONS_MODAL_KEY } from '@/constants';
|
||||
import { DUPLICATE_MODAL_KEY, TAGS_MANAGER_MODAL_KEY, WORKLOW_OPEN_MODAL_KEY, VERSIONS_MODAL_KEY, CREDENTIAL_EDIT_MODAL_KEY, CREDENTIAL_SELECT_MODAL_KEY } from '@/constants';
|
||||
|
||||
import TagsManager from "@/components/TagsManager/TagsManager.vue";
|
||||
import CredentialEdit from "./CredentialEdit/CredentialEdit.vue";
|
||||
import DuplicateWorkflowDialog from "@/components/DuplicateWorkflowDialog.vue";
|
||||
import WorkflowOpen from "@/components/WorkflowOpen.vue";
|
||||
import ModalRoot from "./ModalRoot.vue";
|
||||
import CredentialsSelectModal from "./CredentialsSelectModal.vue";
|
||||
import UpdatesPanel from "./UpdatesPanel.vue";
|
||||
import TagsManager from "@/components/TagsManager/TagsManager.vue";
|
||||
|
||||
export default Vue.extend({
|
||||
name: "Modals",
|
||||
components: {
|
||||
TagsManager,
|
||||
CredentialEdit,
|
||||
DuplicateWorkflowDialog,
|
||||
WorkflowOpen,
|
||||
ModalRoot,
|
||||
CredentialsSelectModal,
|
||||
UpdatesPanel,
|
||||
TagsManager,
|
||||
WorkflowOpen,
|
||||
},
|
||||
data: () => ({
|
||||
DUPLICATE_MODAL_KEY,
|
||||
TAGS_MANAGER_MODAL_KEY,
|
||||
WORKLOW_OPEN_MODAL_KEY,
|
||||
VERSIONS_MODAL_KEY,
|
||||
CREDENTIAL_EDIT_MODAL_KEY,
|
||||
CREDENTIAL_SELECT_MODAL_KEY,
|
||||
}),
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -21,7 +21,7 @@
|
|||
<collection-parameter :parameter="parameter" :values="value" :nodeValues="nodeValues" :path="getPath(index)" :hideDelete="hideDelete" @valueChanged="valueChanged" />
|
||||
</div>
|
||||
<div v-else>
|
||||
<parameter-input class="duplicate-parameter-input-item" :parameter="parameter" :value="value" :displayOptions="true" :path="getPath(index)" @valueChanged="valueChanged" inputSize="small" />
|
||||
<parameter-input class="duplicate-parameter-input-item" :parameter="parameter" :value="value" :displayOptions="true" :path="getPath(index)" @valueChanged="valueChanged" inputSize="small" :isReadOnly="isReadOnly" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -75,6 +75,7 @@ export default mixins(genericHelpers)
|
|||
},
|
||||
},
|
||||
methods: {
|
||||
addTargetBlank,
|
||||
addItem () {
|
||||
const name = this.getPath();
|
||||
let currentValue = get(this.nodeValues, name);
|
||||
|
@ -92,7 +93,6 @@ export default mixins(genericHelpers)
|
|||
|
||||
this.$emit('valueChanged', parameterData);
|
||||
},
|
||||
addTargetBlank,
|
||||
deleteItem (index: number) {
|
||||
const parameterData = {
|
||||
name: this.getPath(index),
|
||||
|
|
|
@ -37,7 +37,7 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<NodeIcon class="node-icon" :nodeType="nodeType" size="60" :shrink="true" :disabled="this.data.disabled"/>
|
||||
<NodeIcon class="node-icon" :nodeType="nodeType" size="60" :circle="true" :shrink="true" :disabled="this.data.disabled"/>
|
||||
</div>
|
||||
<div class="node-description">
|
||||
<div class="node-name" :title="data.name">
|
||||
|
|
|
@ -1,27 +1,30 @@
|
|||
<template>
|
||||
<div v-if="credentialTypesNodeDescriptionDisplayed.length" class="node-credentials">
|
||||
<credentials-edit :dialogVisible="credentialNewDialogVisible" :editCredentials="editCredentials" :setCredentialType="addType" :nodesInit="nodesInit" :node="node" @closeDialog="closeCredentialNewDialog" @credentialsCreated="credentialsCreated" @credentialsUpdated="credentialsUpdated"></credentials-edit>
|
||||
|
||||
<div class="headline">
|
||||
Credentials
|
||||
</div>
|
||||
|
||||
<div v-for="credentialTypeDescription in credentialTypesNodeDescriptionDisplayed" :key="credentialTypeDescription.name" class="credential-data">
|
||||
<el-row v-if="displayCredentials(credentialTypeDescription)" class="credential-parameter-wrapper">
|
||||
|
||||
<el-row class="credential-parameter-wrapper">
|
||||
<el-col :span="10" class="parameter-name">
|
||||
{{credentialTypeNames[credentialTypeDescription.name]}}:
|
||||
</el-col>
|
||||
<el-col :span="12" class="parameter-value" :class="getIssues(credentialTypeDescription.name).length?'has-issues':''">
|
||||
|
||||
<div :style="credentialInputWrapperStyle(credentialTypeDescription.name)">
|
||||
<n8n-select v-model="credentials[credentialTypeDescription.name]" :disabled="isReadOnly" @change="credentialSelected(credentialTypeDescription.name)" placeholder="Select Credential" size="small">
|
||||
<n8n-select :value="selected[credentialTypeDescription.name]" :disabled="isReadOnly" @change="(value) => credentialSelected(credentialTypeDescription.name, value)" placeholder="Select Credential" size="small">
|
||||
<n8n-option
|
||||
v-for="(item, index) in credentialOptions[credentialTypeDescription.name]"
|
||||
:key="item.name + '_' + index"
|
||||
v-for="(item) in credentialOptions[credentialTypeDescription.name]"
|
||||
:key="item.id"
|
||||
:label="item.name"
|
||||
:value="item.name">
|
||||
</n8n-option>
|
||||
<n8n-option
|
||||
:key="NEW_CREDENTIALS_TEXT"
|
||||
:value="NEW_CREDENTIALS_TEXT"
|
||||
:label="NEW_CREDENTIALS_TEXT"
|
||||
>
|
||||
</n8n-option>
|
||||
</n8n-select>
|
||||
</div>
|
||||
|
||||
|
@ -34,7 +37,7 @@
|
|||
|
||||
</el-col>
|
||||
<el-col :span="2" class="parameter-value credential-action">
|
||||
<font-awesome-icon v-if="credentials[credentialTypeDescription.name]" icon="pen" @click="updateCredentials(credentialTypeDescription.name)" class="update-credentials clickable" title="Update Credentials" />
|
||||
<font-awesome-icon v-if="selected[credentialTypeDescription.name] && isCredentialValid(credentialTypeDescription.name)" icon="pen" @click="editCredential(credentialTypeDescription.name)" class="update-credentials clickable" title="Update Credentials" />
|
||||
</el-col>
|
||||
|
||||
</el-row>
|
||||
|
@ -44,12 +47,8 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
|
||||
import { restApi } from '@/components/mixins/restApi';
|
||||
import {
|
||||
ICredentialsCreatedEvent,
|
||||
ICredentialsResponse,
|
||||
INodeUi,
|
||||
INodeUpdatePropertiesInformation,
|
||||
} from '@/Interface';
|
||||
|
@ -59,15 +58,16 @@ import {
|
|||
INodeTypeDescription,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import CredentialsEdit from '@/components/CredentialsEdit.vue';
|
||||
import ParameterInput from '@/components/ParameterInput.vue';
|
||||
|
||||
import { genericHelpers } from '@/components/mixins/genericHelpers';
|
||||
import { nodeHelpers } from '@/components/mixins/nodeHelpers';
|
||||
import { showMessage } from '@/components/mixins/showMessage';
|
||||
|
||||
import { mapGetters } from "vuex";
|
||||
|
||||
import mixins from 'vue-typed-mixins';
|
||||
|
||||
const NEW_CREDENTIALS_TEXT = '- Create New -';
|
||||
|
||||
export default mixins(
|
||||
genericHelpers,
|
||||
nodeHelpers,
|
||||
|
@ -78,11 +78,16 @@ export default mixins(
|
|||
props: [
|
||||
'node', // INodeUi
|
||||
],
|
||||
components: {
|
||||
CredentialsEdit,
|
||||
ParameterInput,
|
||||
data () {
|
||||
return {
|
||||
NEW_CREDENTIALS_TEXT,
|
||||
newCredentialUnsubscribe: null as null | (() => void),
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters('credentials', {
|
||||
credentialOptions: 'allCredentialsByType',
|
||||
}),
|
||||
credentialTypesNode (): string[] {
|
||||
return this.credentialTypesNodeDescription
|
||||
.map((credentialTypeDescription) => credentialTypeDescription.name);
|
||||
|
@ -109,52 +114,16 @@ export default mixins(
|
|||
} = {};
|
||||
let credentialType: ICredentialType | null;
|
||||
for (const credentialTypeName of this.credentialTypesNode) {
|
||||
credentialType = this.$store.getters.credentialType(credentialTypeName);
|
||||
credentialType = this.$store.getters['credentials/getCredentialTypeByName'](credentialTypeName);
|
||||
returnData[credentialTypeName] = credentialType !== null ? credentialType.displayName : credentialTypeName;
|
||||
}
|
||||
return returnData;
|
||||
},
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
addType: undefined as string | undefined,
|
||||
credentialNewDialogVisible: false,
|
||||
credentialOptions: {} as { [key: string]: ICredentialsResponse[]; },
|
||||
credentials: {} as {
|
||||
[key: string]: string | undefined
|
||||
},
|
||||
editCredentials: null as object | null, // Credentials filter
|
||||
newCredentialText: '- Create New -',
|
||||
nodesInit: undefined as string[] | undefined,
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
node () {
|
||||
this.init();
|
||||
selected(): {[type: string]: string} {
|
||||
return this.node.credentials || {};
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
closeCredentialNewDialog () {
|
||||
this.credentialNewDialogVisible = false;
|
||||
},
|
||||
async credentialsCreated (eventData: ICredentialsCreatedEvent) {
|
||||
await this.credentialsUpdated(eventData);
|
||||
},
|
||||
credentialsUpdated (eventData: ICredentialsCreatedEvent) {
|
||||
if (!this.credentialTypesNode.includes(eventData.data.type)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.init();
|
||||
Vue.set(this.credentials, eventData.data.type, eventData.data.name);
|
||||
|
||||
// Makes sure that it does also get set correctly on the node not just the UI
|
||||
this.credentialSelected(eventData.data.type);
|
||||
|
||||
if (eventData.options.closeDialog === true) {
|
||||
this.closeCredentialNewDialog();
|
||||
}
|
||||
},
|
||||
credentialInputWrapperStyle (credentialType: string) {
|
||||
let deductWidth = 0;
|
||||
const styles = {
|
||||
|
@ -170,29 +139,54 @@ export default mixins(
|
|||
|
||||
return styles;
|
||||
},
|
||||
credentialSelected (credentialType: string) {
|
||||
const credential = this.credentials[credentialType];
|
||||
if (credential === this.newCredentialText) {
|
||||
// New credentials should be created
|
||||
this.addType = credentialType;
|
||||
this.editCredentials = null;
|
||||
this.nodesInit = [ (this.node as INodeUi).type ];
|
||||
this.credentialNewDialogVisible = true;
|
||||
|
||||
this.credentials[credentialType] = undefined;
|
||||
listenForNewCredentials(credentialType: string) {
|
||||
this.stopListeningForNewCredentials();
|
||||
|
||||
this.newCredentialUnsubscribe = this.$store.subscribe((mutation, state) => {
|
||||
if (mutation.type === 'credentials/upsertCredential' || mutation.type === 'credentials/enableOAuthCredential'){
|
||||
this.credentialSelected(credentialType, mutation.payload.name);
|
||||
}
|
||||
if (mutation.type === 'credentials/deleteCredential') {
|
||||
this.credentialSelected(credentialType, mutation.payload.name);
|
||||
this.stopListeningForNewCredentials();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
stopListeningForNewCredentials() {
|
||||
if (this.newCredentialUnsubscribe) {
|
||||
this.newCredentialUnsubscribe();
|
||||
}
|
||||
},
|
||||
|
||||
credentialSelected (credentialType: string, credentialName: string) {
|
||||
let selected = undefined;
|
||||
if (credentialName === NEW_CREDENTIALS_TEXT) {
|
||||
this.listenForNewCredentials(credentialType);
|
||||
this.$store.dispatch('ui/openNewCredential', { type: credentialType });
|
||||
}
|
||||
else {
|
||||
selected = credentialName;
|
||||
}
|
||||
|
||||
const node = this.node as INodeUi;
|
||||
const node: INodeUi = this.node;
|
||||
|
||||
const credentials = {
|
||||
...(node.credentials || {}),
|
||||
[credentialType]: selected,
|
||||
};
|
||||
|
||||
const updateInformation: INodeUpdatePropertiesInformation = {
|
||||
name: node.name,
|
||||
name: this.node.name,
|
||||
properties: {
|
||||
credentials: JSON.parse(JSON.stringify(this.credentials)),
|
||||
credentials,
|
||||
},
|
||||
};
|
||||
|
||||
this.$emit('credentialSelected', updateInformation);
|
||||
},
|
||||
|
||||
displayCredentials (credentialTypeDescription: INodeCredentialDescription): boolean {
|
||||
if (credentialTypeDescription.displayOptions === undefined) {
|
||||
// If it is not defined no need to do a proper check
|
||||
|
@ -200,6 +194,7 @@ export default mixins(
|
|||
}
|
||||
return this.displayParameter(this.node.parameters, credentialTypeDescription, '');
|
||||
},
|
||||
|
||||
getIssues (credentialTypeName: string): string[] {
|
||||
const node = this.node as INodeUi;
|
||||
|
||||
|
@ -213,56 +208,25 @@ export default mixins(
|
|||
|
||||
return node.issues.credentials[credentialTypeName];
|
||||
},
|
||||
updateCredentials (credentialType: string): void {
|
||||
const name = this.credentials[credentialType];
|
||||
const credentialData = this.credentialOptions[credentialType].find((optionData: ICredentialsResponse) => optionData.name === name);
|
||||
if (credentialData === undefined) {
|
||||
this.$showMessage({
|
||||
title: 'Credentials not found',
|
||||
message: `The credentials named "${name}" of type "${credentialType}" could not be found!`,
|
||||
type: 'error',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const editCredentials = {
|
||||
id: credentialData.id,
|
||||
name,
|
||||
type: credentialType,
|
||||
};
|
||||
isCredentialValid(credentialType: string): boolean {
|
||||
const name = this.node.credentials[credentialType];
|
||||
const options = this.credentialOptions[credentialType];
|
||||
|
||||
this.editCredentials = editCredentials;
|
||||
this.addType = credentialType;
|
||||
this.credentialNewDialogVisible = true;
|
||||
return options.find((option: ICredentialType) => option.name === name);
|
||||
},
|
||||
|
||||
init () {
|
||||
const node = this.node as INodeUi;
|
||||
editCredential(credentialType: string): void {
|
||||
const name = this.node.credentials[credentialType];
|
||||
const options = this.credentialOptions[credentialType];
|
||||
const selected = options.find((option: ICredentialType) => option.name === name);
|
||||
this.$store.dispatch('ui/openExisitngCredential', { id: selected.id });
|
||||
|
||||
const newOption = {
|
||||
name: this.newCredentialText,
|
||||
};
|
||||
|
||||
let options = [];
|
||||
|
||||
// Get the available credentials for each type
|
||||
for (const credentialType of this.credentialTypesNode) {
|
||||
options = this.$store.getters.credentialsByType(credentialType);
|
||||
options.push(newOption as ICredentialsResponse);
|
||||
Vue.set(this.credentialOptions, credentialType, options);
|
||||
}
|
||||
|
||||
// Set the current node credentials
|
||||
if (node.credentials) {
|
||||
Vue.set(this, 'credentials', JSON.parse(JSON.stringify(node.credentials)));
|
||||
} else {
|
||||
Vue.set(this, 'credentials', {});
|
||||
}
|
||||
this.listenForNewCredentials(credentialType);
|
||||
},
|
||||
|
||||
},
|
||||
mounted () {
|
||||
this.init();
|
||||
beforeDestroy () {
|
||||
this.stopListeningForNewCredentials();
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
@ -317,6 +281,7 @@ export default mixins(
|
|||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: var(--color-text-base);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -27,6 +27,7 @@ export default Vue.extend({
|
|||
'size',
|
||||
'shrink',
|
||||
'disabled',
|
||||
'circle',
|
||||
],
|
||||
computed: {
|
||||
iconStyleData (): object {
|
||||
|
@ -43,7 +44,7 @@ export default Vue.extend({
|
|||
height: size + 'px',
|
||||
'font-size': Math.floor(parseInt(this.size, 10) * 0.6) + 'px',
|
||||
'line-height': size + 'px',
|
||||
'border-radius': Math.ceil(size / 2) + 'px',
|
||||
'border-radius': this.circle ? '50%': '4px',
|
||||
};
|
||||
},
|
||||
isSvgIcon (): boolean {
|
||||
|
|
|
@ -511,24 +511,17 @@ export default mixins(
|
|||
<style lang="scss">
|
||||
|
||||
.node-settings {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
width: 350px;
|
||||
height: 100%;
|
||||
border: none;
|
||||
z-index: 200;
|
||||
font-size: 0.8em;
|
||||
color: #555;
|
||||
border-radius: 2px 0 0 2px;
|
||||
overflow: hidden;
|
||||
min-width: 350px;
|
||||
max-width: 350px;
|
||||
font-size: var(--font-size-s);
|
||||
|
||||
.header-side-menu {
|
||||
padding: 1em 0 1em 1.8em;
|
||||
font-size: 1.35em;
|
||||
font-size: var(--font-size-l);
|
||||
background-color: $--custom-window-sidebar-top;
|
||||
color: #555;
|
||||
|
||||
.node-info {
|
||||
color: #555;
|
||||
display: none;
|
||||
padding-left: 0.5em;
|
||||
font-size: 0.8em;
|
||||
|
@ -546,18 +539,19 @@ export default mixins(
|
|||
}
|
||||
|
||||
.node-parameters-wrapper {
|
||||
height: calc(100% - 110px);
|
||||
height: 100%;
|
||||
font-size: .9em;
|
||||
|
||||
.el-tabs__header {
|
||||
background-color: #fff5f2;
|
||||
line-height: 2em;
|
||||
}
|
||||
|
||||
.el-tabs {
|
||||
height: 100%;
|
||||
.el-tabs__content {
|
||||
height: calc(100% - 17px);
|
||||
overflow-y: auto;
|
||||
height: 100%;
|
||||
padding-bottom: 180px;
|
||||
|
||||
.el-tab-pane {
|
||||
margin: 0 1em;
|
||||
|
|
|
@ -29,9 +29,11 @@
|
|||
:rows="getArgument('rows')"
|
||||
:value="displayValue"
|
||||
:disabled="isReadOnly"
|
||||
@input="onTextInputChange"
|
||||
@change="valueChanged"
|
||||
@keydown.stop
|
||||
@focus="setFocus"
|
||||
@blur="onBlur"
|
||||
:title="displayTitle"
|
||||
:placeholder="isValueExpression?'':parameter.placeholder"
|
||||
>
|
||||
|
@ -48,6 +50,7 @@
|
|||
:value="displayValue"
|
||||
:disabled="isReadOnly"
|
||||
@focus="setFocus"
|
||||
@blur="onBlur"
|
||||
@change="valueChanged"
|
||||
:title="displayTitle"
|
||||
:show-alpha="getArgument('showAlpha')"
|
||||
|
@ -61,6 +64,7 @@
|
|||
@change="valueChanged"
|
||||
@keydown.stop
|
||||
@focus="setFocus"
|
||||
@blur="onBlur"
|
||||
:title="displayTitle"
|
||||
/>
|
||||
</div>
|
||||
|
@ -78,6 +82,7 @@
|
|||
:picker-options="dateTimePickerOptions"
|
||||
@change="valueChanged"
|
||||
@focus="setFocus"
|
||||
@blur="onBlur"
|
||||
@keydown.stop
|
||||
/>
|
||||
|
||||
|
@ -92,7 +97,9 @@
|
|||
:step="getArgument('numberStepSize')"
|
||||
:disabled="isReadOnly"
|
||||
@change="valueChanged"
|
||||
@input="onTextInputChange"
|
||||
@focus="setFocus"
|
||||
@blur="onBlur"
|
||||
@keydown.stop
|
||||
:title="displayTitle"
|
||||
:placeholder="parameter.placeholder"
|
||||
|
@ -107,9 +114,11 @@
|
|||
:loading="remoteParameterOptionsLoading"
|
||||
:disabled="isReadOnly || remoteParameterOptionsLoading"
|
||||
:title="displayTitle"
|
||||
:popper-append-to-body="true"
|
||||
@change="valueChanged"
|
||||
@keydown.stop
|
||||
@focus="setFocus"
|
||||
@blur="onBlur"
|
||||
>
|
||||
<n8n-option
|
||||
v-for="option in parameterOptions"
|
||||
|
@ -136,6 +145,7 @@
|
|||
@change="valueChanged"
|
||||
@keydown.stop
|
||||
@focus="setFocus"
|
||||
@blur="onBlur"
|
||||
:title="displayTitle"
|
||||
>
|
||||
<n8n-option v-for="option in parameterOptions" :value="option.value" :key="option.value" :label="option.name" >
|
||||
|
@ -177,7 +187,6 @@
|
|||
</el-dropdown-menu>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -201,7 +210,6 @@ import ExpressionEdit from '@/components/ExpressionEdit.vue';
|
|||
import PrismEditor from 'vue-prism-editor';
|
||||
import TextEdit from '@/components/TextEdit.vue';
|
||||
import { externalHooks } from '@/components/mixins/externalHooks';
|
||||
import { genericHelpers } from '@/components/mixins/genericHelpers';
|
||||
import { nodeHelpers } from '@/components/mixins/nodeHelpers';
|
||||
import { showMessage } from '@/components/mixins/showMessage';
|
||||
import { workflowHelpers } from '@/components/mixins/workflowHelpers';
|
||||
|
@ -210,7 +218,6 @@ import mixins from 'vue-typed-mixins';
|
|||
|
||||
export default mixins(
|
||||
externalHooks,
|
||||
genericHelpers,
|
||||
nodeHelpers,
|
||||
showMessage,
|
||||
workflowHelpers,
|
||||
|
@ -225,11 +232,14 @@ export default mixins(
|
|||
},
|
||||
props: [
|
||||
'displayOptions', // boolean
|
||||
'inputSize',
|
||||
'isReadOnly',
|
||||
'documentationUrl',
|
||||
'parameter', // NodeProperties
|
||||
'path', // string
|
||||
'value',
|
||||
'isCredential', // boolean
|
||||
'inputSize',
|
||||
'hideIssues', // boolean
|
||||
'errorHighlight',
|
||||
],
|
||||
data () {
|
||||
return {
|
||||
|
@ -431,7 +441,7 @@ export default mixins(
|
|||
return 'text';
|
||||
},
|
||||
getIssues (): string[] {
|
||||
if (this.isCredential === true || this.node === null) {
|
||||
if (this.hideIssues === true || this.node === null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
|
@ -512,7 +522,7 @@ export default mixins(
|
|||
if (this.isValueExpression) {
|
||||
classes.push('expression');
|
||||
}
|
||||
if (this.getIssues.length) {
|
||||
if (this.getIssues.length || this.errorHighlight) {
|
||||
classes.push('has-issues');
|
||||
}
|
||||
return classes;
|
||||
|
@ -602,8 +612,12 @@ export default mixins(
|
|||
openExpressionEdit() {
|
||||
if (this.isValueExpression) {
|
||||
this.expressionEditDialogVisible = true;
|
||||
return;
|
||||
}
|
||||
},
|
||||
onBlur () {
|
||||
this.$emit('blur');
|
||||
},
|
||||
setFocus () {
|
||||
if (this.isValueExpression) {
|
||||
this.expressionEditDialogVisible = true;
|
||||
|
@ -644,6 +658,15 @@ export default mixins(
|
|||
const [r, g, b, a] = valueMatch.splice(1, 4).map(v => Number(v));
|
||||
return "#" + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1) + ((1 << 8) + Math.floor((1-a)*255)).toString(16).slice(1);
|
||||
},
|
||||
onTextInputChange (value: string) {
|
||||
const parameterData = {
|
||||
node: this.node !== null ? this.node.name : this.nodeName,
|
||||
name: this.path,
|
||||
value,
|
||||
};
|
||||
|
||||
this.$emit('textInput', parameterData);
|
||||
},
|
||||
valueChanged (value: string | number | boolean | Date | null) {
|
||||
if (value instanceof Date) {
|
||||
value = value.toISOString();
|
||||
|
@ -840,4 +863,16 @@ export default mixins(
|
|||
align-items: center;
|
||||
}
|
||||
|
||||
.errors {
|
||||
margin-top: var(--spacing-2xs);
|
||||
color: var(--color-danger);
|
||||
font-size: var(--font-size-2xs);
|
||||
font-weight: var(--font-weight-regular);
|
||||
|
||||
a {
|
||||
color: var(--color-danger);
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
|
68
packages/editor-ui/src/components/ParameterInputExpanded.vue
Normal file
68
packages/editor-ui/src/components/ParameterInputExpanded.vue
Normal file
|
@ -0,0 +1,68 @@
|
|||
<template>
|
||||
<n8n-input-label
|
||||
:label="parameter.displayName"
|
||||
:tooltipText="parameter.description"
|
||||
:required="parameter.required"
|
||||
>
|
||||
<parameter-input
|
||||
:parameter="parameter"
|
||||
:value="value"
|
||||
:path="parameter.name"
|
||||
:hideIssues="true"
|
||||
:displayOptions="true"
|
||||
:documentationUrl="documentationUrl"
|
||||
:errorHighlight="showRequiredErrors"
|
||||
|
||||
@blur="onBlur"
|
||||
@textInput="valueChanged"
|
||||
@valueChanged="valueChanged"
|
||||
inputSize="large"
|
||||
/>
|
||||
<div class="errors" v-if="showRequiredErrors">
|
||||
This field is required. <a v-if="documentationUrl" :href="documentationUrl" target="_blank">Open docs</a>
|
||||
</div>
|
||||
</n8n-input-label>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { IUpdateInformation } from '@/Interface';
|
||||
import ParameterInput from './ParameterInput.vue';
|
||||
import Vue from 'vue';
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'ParameterInputExpanded',
|
||||
components: {
|
||||
ParameterInput,
|
||||
},
|
||||
props: {
|
||||
parameter: {
|
||||
},
|
||||
value: {
|
||||
},
|
||||
showValidationWarnings: {
|
||||
type: Boolean,
|
||||
},
|
||||
documentationUrl: {
|
||||
type: String,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
blurred: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
showRequiredErrors(): boolean {
|
||||
return this.$props.parameter.type !== 'boolean' && !this.value && this.$props.parameter.required && (this.blurred || this.showValidationWarnings);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onBlur() {
|
||||
this.blurred = true;
|
||||
},
|
||||
valueChanged(parameterData: IUpdateInformation) {
|
||||
this.$emit('change', parameterData);
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
|
@ -8,7 +8,7 @@
|
|||
</n8n-tooltip>
|
||||
</el-col>
|
||||
<el-col :span="isMultiLineParameter ? 24 : 14" class="parameter-value">
|
||||
<parameter-input :parameter="parameter" :value="value" :displayOptions="displayOptions" :path="path" @valueChanged="valueChanged" inputSize="small" />
|
||||
<parameter-input :parameter="parameter" :value="value" :displayOptions="displayOptions" :path="path" :isReadOnly="isReadOnly" @valueChanged="valueChanged" inputSize="small" />
|
||||
</el-col>
|
||||
</el-row>
|
||||
</template>
|
||||
|
@ -47,6 +47,7 @@ export default Vue
|
|||
},
|
||||
props: [
|
||||
'displayOptions',
|
||||
'isReadOnly',
|
||||
'parameter',
|
||||
'path',
|
||||
'value',
|
||||
|
|
|
@ -70,6 +70,7 @@
|
|||
:value="getParameterValue(nodeValues, parameter.name, path)"
|
||||
:displayOptions="true"
|
||||
:path="getPath(parameter.name)"
|
||||
:isReadOnly="isReadOnly"
|
||||
@valueChanged="valueChanged"
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -639,14 +639,8 @@ export default mixins(
|
|||
|
||||
.run-data-view {
|
||||
position: relative;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
margin-left: 350px;
|
||||
width: calc(100% - 350px);
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 100;
|
||||
color: #555;
|
||||
font-size: 14px;
|
||||
background-color: #f9f9f9;
|
||||
|
||||
.data-display-content {
|
||||
|
@ -657,6 +651,7 @@ export default mixins(
|
|||
right: 0;
|
||||
overflow-y: auto;
|
||||
line-height: 1.5;
|
||||
word-break: normal;
|
||||
|
||||
.binary-data-row {
|
||||
display: inline-flex;
|
||||
|
|
58
packages/editor-ui/src/components/SaveButton.vue
Normal file
58
packages/editor-ui/src/components/SaveButton.vue
Normal file
|
@ -0,0 +1,58 @@
|
|||
<template>
|
||||
<span :class="$style.container">
|
||||
<span :class="$style.saved" v-if="saved">{{ savedLabel }}</span>
|
||||
<n8n-button
|
||||
v-else
|
||||
:label="isSaving ? savingLabel : saveLabel"
|
||||
:loading="isSaving"
|
||||
:disabled="disabled"
|
||||
@click="$emit('click')"
|
||||
/>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
|
||||
export default Vue.extend({
|
||||
name: "SaveButton",
|
||||
props: {
|
||||
saved: {
|
||||
type: Boolean,
|
||||
},
|
||||
isSaving: {
|
||||
type: Boolean,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
},
|
||||
saveLabel: {
|
||||
type: String,
|
||||
default: 'Save',
|
||||
},
|
||||
savingLabel: {
|
||||
type: String,
|
||||
default: 'Saving',
|
||||
},
|
||||
savedLabel: {
|
||||
type: String,
|
||||
default: 'Saved',
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.container {
|
||||
width: 65px;
|
||||
}
|
||||
|
||||
.saved {
|
||||
color: $--custom-font-very-light;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
line-height: 12px;
|
||||
text-align: center;
|
||||
padding: var(--spacing-2xs) var(--spacing-xs);
|
||||
}
|
||||
</style>
|
|
@ -1,51 +0,0 @@
|
|||
<template>
|
||||
<span :class="$style.container">
|
||||
<n8n-button v-if="isDirty || isNewWorkflow" label="Save" :disabled="isWorkflowSaving" @click="save" />
|
||||
<span :class="$style.saved" v-else>Saved</span>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import mixins from "vue-typed-mixins";
|
||||
import { mapGetters } from "vuex";
|
||||
|
||||
import { workflowHelpers } from "@/components/mixins/workflowHelpers";
|
||||
|
||||
export default mixins(workflowHelpers).extend({
|
||||
name: "SaveWorkflowButton",
|
||||
computed: {
|
||||
...mapGetters({
|
||||
isDirty: "getStateIsDirty",
|
||||
}),
|
||||
isWorkflowSaving(): boolean {
|
||||
return this.$store.getters.isActionActive("workflowSaving");
|
||||
},
|
||||
isNewWorkflow(): boolean {
|
||||
return !this.$route.params.name;
|
||||
},
|
||||
isSaved(): boolean {
|
||||
return !this.isWorkflowSaving && !this.isDirty && !this.isNewWorkflow;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
save() {
|
||||
this.saveCurrentWorkflow();
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.container {
|
||||
width: 65px;
|
||||
}
|
||||
|
||||
.saved {
|
||||
color: $--custom-font-very-light;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
line-height: 12px;
|
||||
text-align: center;
|
||||
padding: var(--spacing-2xs) var(--spacing-xs);
|
||||
}
|
||||
</style>
|
|
@ -1,15 +1,59 @@
|
|||
<template functional>
|
||||
<span>
|
||||
{{$options.format(props.date)}}
|
||||
<span :title="$options.methods.convertToHumanReadableDate($props)">
|
||||
{{$options.methods.format(props)}}
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { format } from 'timeago.js';
|
||||
import { format, LocaleFunc, register } from 'timeago.js';
|
||||
import { convertToHumanReadableDate } from './helpers';
|
||||
|
||||
const localeFunc = (num: number, index: number, totalSec: number): [string, string] => {
|
||||
// number: the timeago / timein number;
|
||||
// index: the index of array below;
|
||||
// totalSec: total seconds between date to be formatted and today's date;
|
||||
return [
|
||||
['Just now', 'Right now'],
|
||||
['Just now', 'Right now'], // ['%s seconds ago', 'in %s seconds'],
|
||||
['1 minute ago', 'in 1 minute'],
|
||||
['%s minutes ago', 'in %s minutes'],
|
||||
['1 hour ago', 'in 1 hour'],
|
||||
['%s hours ago', 'in %s hours'],
|
||||
['1 day ago', 'in 1 day'],
|
||||
['%s days ago', 'in %s days'],
|
||||
['1 week ago', 'in 1 week'],
|
||||
['%s weeks ago', 'in %s weeks'],
|
||||
['1 month ago', 'in 1 month'],
|
||||
['%s months ago', 'in %s months'],
|
||||
['1 year ago', 'in 1 year'],
|
||||
['%s years ago', 'in %s years'],
|
||||
][index] as [string, string];
|
||||
};
|
||||
|
||||
register('main', localeFunc as LocaleFunc);
|
||||
|
||||
export default {
|
||||
name: 'UpdatesPanel',
|
||||
props: ['date'],
|
||||
format,
|
||||
props: {
|
||||
date: {
|
||||
type: String,
|
||||
},
|
||||
capitalize: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
format(props: {date: string, capitalize: boolean}) {
|
||||
const text = format(props.date, 'main');
|
||||
|
||||
if (!props.capitalize) {
|
||||
return text.toLowerCase();
|
||||
}
|
||||
|
||||
return text;
|
||||
},
|
||||
convertToHumanReadableDate,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
</script>
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
<Modal
|
||||
:name="modalName"
|
||||
size="xl"
|
||||
:classic="true"
|
||||
>
|
||||
<template v-slot:header>
|
||||
<div class="workflows-header">
|
||||
|
@ -63,6 +64,7 @@ import Modal from '@/components/Modal.vue';
|
|||
import TagsContainer from '@/components/TagsContainer.vue';
|
||||
import TagsDropdown from '@/components/TagsDropdown.vue';
|
||||
import WorkflowActivator from '@/components/WorkflowActivator.vue';
|
||||
import { convertToDisplayDate } from './helpers';
|
||||
|
||||
export default mixins(
|
||||
genericHelpers,
|
||||
|
@ -176,8 +178,8 @@ export default mixins(
|
|||
this.workflows = data;
|
||||
|
||||
this.workflows.forEach((workflowData: IWorkflowShortResponse) => {
|
||||
workflowData.createdAt = this.convertToDisplayDate(workflowData.createdAt as number);
|
||||
workflowData.updatedAt = this.convertToDisplayDate(workflowData.updatedAt as number);
|
||||
workflowData.createdAt = convertToDisplayDate(workflowData.createdAt as number);
|
||||
workflowData.updatedAt = convertToDisplayDate(workflowData.updatedAt as number);
|
||||
});
|
||||
this.isDataLoading = false;
|
||||
},
|
||||
|
@ -215,7 +217,6 @@ export default mixins(
|
|||
flex-grow: 1;
|
||||
|
||||
h1 {
|
||||
font-weight: 600;
|
||||
line-height: 24px;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<span>
|
||||
<el-dialog class="workflow-settings" custom-class="classic" :visible="dialogVisible" append-to-body width="65%" title="Workflow Settings" :before-close="closeDialog">
|
||||
<el-dialog class="workflow-settings" :visible="dialogVisible" append-to-body width="65%" title="Workflow Settings" :before-close="closeDialog">
|
||||
<div v-loading="isLoading">
|
||||
<el-row>
|
||||
<el-col :span="10" class="setting-name">
|
||||
|
|
|
@ -1,5 +1,21 @@
|
|||
import dateformat from 'dateformat';
|
||||
|
||||
const KEYWORDS_TO_FILTER = ['API', 'OAuth1', 'OAuth2'];
|
||||
|
||||
export function addTargetBlank(html: string) {
|
||||
return html.includes('href=')
|
||||
? html.replace(/href=/g, 'target="_blank" href=')
|
||||
: html;
|
||||
}
|
||||
|
||||
export function convertToDisplayDate (epochTime: number) {
|
||||
return dateformat(epochTime, 'yyyy-mm-dd HH:MM:ss');
|
||||
}
|
||||
|
||||
export function convertToHumanReadableDate (epochTime: number) {
|
||||
return dateformat(epochTime, 'd mmmm, yyyy @ HH:MM Z');
|
||||
}
|
||||
|
||||
export function getAppNameFromCredType(name: string) {
|
||||
return name.split(' ').filter((word) => !KEYWORDS_TO_FILTER.includes(word)).join(' ');
|
||||
}
|
||||
|
|
|
@ -1,7 +1,4 @@
|
|||
import dateformat from 'dateformat';
|
||||
|
||||
import { showMessage } from '@/components/mixins/showMessage';
|
||||
import { MessageType } from '@/Interface';
|
||||
import { debounce } from 'lodash';
|
||||
|
||||
import mixins from 'vue-typed-mixins';
|
||||
|
@ -22,9 +19,6 @@ export const genericHelpers = mixins(showMessage).extend({
|
|||
},
|
||||
},
|
||||
methods: {
|
||||
convertToDisplayDate (epochTime: number) {
|
||||
return dateformat(epochTime, 'yyyy-mm-dd HH:MM:ss');
|
||||
},
|
||||
displayTimer (msPassed: number, showMs = false): string {
|
||||
if (msPassed < 60000) {
|
||||
if (showMs === false) {
|
||||
|
@ -91,20 +85,5 @@ export const genericHelpers = mixins(showMessage).extend({
|
|||
// @ts-ignore
|
||||
await this.debouncedFunctions[functionName].apply(this, inputParameters);
|
||||
},
|
||||
|
||||
async confirmMessage (message: string, headline: string, type = 'warning' as MessageType, confirmButtonText = 'OK', cancelButtonText = 'Cancel'): Promise<boolean> {
|
||||
try {
|
||||
await this.$confirm(message, headline, {
|
||||
confirmButtonText,
|
||||
cancelButtonText,
|
||||
type,
|
||||
dangerouslyUseHTMLString: true,
|
||||
});
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
},
|
||||
});
|
||||
|
|
|
@ -204,7 +204,7 @@ export const nodeHelpers = mixins(
|
|||
}
|
||||
|
||||
// Get the display name of the credential type
|
||||
credentialType = this.$store.getters.credentialType(credentialTypeDescription.name);
|
||||
credentialType = this.$store.getters['credentials/getCredentialTypeByName'](credentialTypeDescription.name);
|
||||
if (credentialType === null) {
|
||||
credentialDisplayName = credentialTypeDescription.name;
|
||||
} else {
|
||||
|
@ -219,7 +219,7 @@ export const nodeHelpers = mixins(
|
|||
} else {
|
||||
// If they are set check if the value is valid
|
||||
selectedCredentials = node.credentials[credentialTypeDescription.name];
|
||||
userCredentials = this.$store.getters.credentialsByType(credentialTypeDescription.name);
|
||||
userCredentials = this.$store.getters['credentials/getCredentialsByType'](credentialTypeDescription.name);
|
||||
|
||||
if (userCredentials === null) {
|
||||
userCredentials = [];
|
||||
|
|
|
@ -292,7 +292,7 @@ export const pushConnection = mixins(
|
|||
const pushData = receivedData.data;
|
||||
this.$store.commit('setExecutingNode', pushData.nodeName);
|
||||
} else if (receivedData.type === 'testWebhookDeleted') {
|
||||
// A test-webhook got deleted
|
||||
// A test-webhook was deleted
|
||||
const pushData = receivedData.data;
|
||||
|
||||
if (pushData.workflowId === this.$store.getters.workflowId) {
|
||||
|
|
|
@ -1,11 +1,9 @@
|
|||
import Vue from 'vue';
|
||||
import { parse } from 'flatted';
|
||||
|
||||
import axios, { AxiosRequestConfig, Method } from 'axios';
|
||||
import { Method } from 'axios';
|
||||
import {
|
||||
IActivationError,
|
||||
ICredentialsDecryptedResponse,
|
||||
ICredentialsResponse,
|
||||
IExecutionsCurrentSummaryExtended,
|
||||
IExecutionDeleteFilter,
|
||||
IExecutionPushResponse,
|
||||
|
@ -18,12 +16,9 @@ import {
|
|||
IWorkflowDb,
|
||||
IWorkflowShortResponse,
|
||||
IRestApi,
|
||||
IWorkflowData,
|
||||
IWorkflowDataUpdate,
|
||||
} from '@/Interface';
|
||||
import {
|
||||
ICredentialsDecrypted,
|
||||
ICredentialType,
|
||||
IDataObject,
|
||||
INodeCredentials,
|
||||
INodeParameters,
|
||||
|
@ -153,69 +148,6 @@ export const restApi = Vue.extend({
|
|||
return self.restApi().makeRestApiRequest('GET', `/workflows/from-url`, { url });
|
||||
},
|
||||
|
||||
// Creates a new workflow
|
||||
createNewCredentials: (sendData: ICredentialsDecrypted): Promise<ICredentialsResponse> => {
|
||||
return self.restApi().makeRestApiRequest('POST', `/credentials`, sendData);
|
||||
},
|
||||
|
||||
// Deletes a credentials
|
||||
deleteCredentials: (id: string): Promise<void> => {
|
||||
return self.restApi().makeRestApiRequest('DELETE', `/credentials/${id}`);
|
||||
},
|
||||
|
||||
// Updates existing credentials
|
||||
updateCredentials: (id: string, data: ICredentialsDecrypted): Promise<ICredentialsResponse> => {
|
||||
return self.restApi().makeRestApiRequest('PATCH', `/credentials/${id}`, data);
|
||||
},
|
||||
|
||||
// Returns the credentials with the given id
|
||||
getCredentials: (id: string, includeData?: boolean): Promise<ICredentialsDecryptedResponse | ICredentialsResponse | undefined> => {
|
||||
let sendData;
|
||||
if (includeData) {
|
||||
sendData = {
|
||||
includeData,
|
||||
};
|
||||
}
|
||||
return self.restApi().makeRestApiRequest('GET', `/credentials/${id}`, sendData);
|
||||
},
|
||||
|
||||
// Returns all saved credentials
|
||||
getAllCredentials: (filter?: object): Promise<ICredentialsResponse[]> => {
|
||||
let sendData;
|
||||
if (filter) {
|
||||
sendData = {
|
||||
filter,
|
||||
};
|
||||
}
|
||||
|
||||
return self.restApi().makeRestApiRequest('GET', `/credentials`, sendData);
|
||||
},
|
||||
|
||||
// Returns all credential types
|
||||
getCredentialTypes: (): Promise<ICredentialType[]> => {
|
||||
return self.restApi().makeRestApiRequest('GET', `/credential-types`);
|
||||
},
|
||||
|
||||
// Get OAuth1 Authorization URL using the stored credentials
|
||||
oAuth1CredentialAuthorize: (sendData: ICredentialsResponse): Promise<string> => {
|
||||
return self.restApi().makeRestApiRequest('GET', `/oauth1-credential/auth`, sendData);
|
||||
},
|
||||
|
||||
// Get OAuth2 Authorization URL using the stored credentials
|
||||
oAuth2CredentialAuthorize: (sendData: ICredentialsResponse): Promise<string> => {
|
||||
return self.restApi().makeRestApiRequest('GET', `/oauth2-credential/auth`, sendData);
|
||||
},
|
||||
|
||||
// Verify OAuth2 provider callback and kick off token generation
|
||||
oAuth2Callback: (code: string, state: string): Promise<string> => {
|
||||
const sendData = {
|
||||
'code': code,
|
||||
'state': state,
|
||||
};
|
||||
|
||||
return self.restApi().makeRestApiRequest('POST', `/oauth2-credential/callback`, sendData);
|
||||
},
|
||||
|
||||
// Returns the execution with the given name
|
||||
getExecution: async (id: string): Promise<IExecutionResponse> => {
|
||||
const response = await self.restApi().makeRestApiRequest('GET', `/executions/${id}`);
|
||||
|
|
|
@ -4,6 +4,8 @@ import mixins from 'vue-typed-mixins';
|
|||
|
||||
import { externalHooks } from '@/components/mixins/externalHooks';
|
||||
import { ExecutionError } from 'n8n-workflow';
|
||||
import { ElMessageBoxOptions } from 'element-ui/types/message-box';
|
||||
import { MessageType } from 'element-ui/types/message';
|
||||
|
||||
export const showMessage = mixins(externalHooks).extend({
|
||||
methods: {
|
||||
|
@ -81,6 +83,22 @@ export const showMessage = mixins(externalHooks).extend({
|
|||
});
|
||||
},
|
||||
|
||||
async confirmMessage (message: string, headline: string, type: MessageType | null = 'warning', confirmButtonText = 'OK', cancelButtonText = 'Cancel'): Promise<boolean> {
|
||||
try {
|
||||
const options: ElMessageBoxOptions = {
|
||||
confirmButtonText,
|
||||
cancelButtonText,
|
||||
dangerouslyUseHTMLString: true,
|
||||
...(type && { type }),
|
||||
};
|
||||
|
||||
await this.$confirm(message, headline, options);
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
// @ts-ignore
|
||||
collapsableDetails({ description, node }: Error) {
|
||||
if (!description) return '';
|
||||
|
|
|
@ -13,10 +13,14 @@ export const DUPLICATE_POSTFFIX = ' copy';
|
|||
|
||||
// tags
|
||||
export const MAX_TAG_NAME_LENGTH = 24;
|
||||
|
||||
// modals
|
||||
export const DUPLICATE_MODAL_KEY = 'duplicate';
|
||||
export const TAGS_MANAGER_MODAL_KEY = 'tagsManager';
|
||||
export const WORKLOW_OPEN_MODAL_KEY = 'workflowOpen';
|
||||
export const VERSIONS_MODAL_KEY = 'versions';
|
||||
export const CREDENTIAL_EDIT_MODAL_KEY = 'editCredential';
|
||||
export const CREDENTIAL_SELECT_MODAL_KEY = 'selectCredential';
|
||||
|
||||
// breakpoints
|
||||
export const BREAKPOINT_SM = 768;
|
||||
|
|
177
packages/editor-ui/src/modules/credentials.ts
Normal file
177
packages/editor-ui/src/modules/credentials.ts
Normal file
|
@ -0,0 +1,177 @@
|
|||
import { getCredentialTypes,
|
||||
getCredentialsNewName,
|
||||
getAllCredentials,
|
||||
deleteCredential,
|
||||
getCredentialData,
|
||||
createNewCredential,
|
||||
updateCredential,
|
||||
oAuth2CredentialAuthorize,
|
||||
oAuth1CredentialAuthorize,
|
||||
testCredential,
|
||||
} from '@/api/credentials';
|
||||
import Vue from 'vue';
|
||||
import { ActionContext, Module } from 'vuex';
|
||||
import {
|
||||
ICredentialMap,
|
||||
ICredentialsResponse,
|
||||
ICredentialsState,
|
||||
ICredentialTypeMap,
|
||||
IRootState,
|
||||
} from '../Interface';
|
||||
import {
|
||||
ICredentialType,
|
||||
ICredentialsDecrypted,
|
||||
NodeCredentialTestResult,
|
||||
INodeTypeDescription,
|
||||
} from 'n8n-workflow';
|
||||
import { getAppNameFromCredType } from '@/components/helpers';
|
||||
|
||||
const DEFAULT_CREDENTIAL_NAME = 'Unnamed credential';
|
||||
const DEFAULT_CREDENTIAL_POSTFIX = 'account';
|
||||
const TYPES_WITH_DEFAULT_NAME = ['httpBasicAuth', 'oAuth2Api', 'httpDigestAuth', 'oAuth1Api'];
|
||||
|
||||
const module: Module<ICredentialsState, IRootState> = {
|
||||
namespaced: true,
|
||||
state: {
|
||||
credentialTypes: {},
|
||||
credentials: {},
|
||||
},
|
||||
mutations: {
|
||||
setCredentialTypes: (state: ICredentialsState, credentialTypes: ICredentialType[]) => {
|
||||
state.credentialTypes = credentialTypes.reduce((accu: ICredentialTypeMap, cred: ICredentialType) => {
|
||||
accu[cred.name] = cred;
|
||||
|
||||
return accu;
|
||||
}, {});
|
||||
},
|
||||
setCredentials: (state: ICredentialsState, credentials: ICredentialsResponse[]) => {
|
||||
state.credentials = credentials.reduce((accu: ICredentialMap, cred: ICredentialsResponse) => {
|
||||
if (cred.id) {
|
||||
accu[cred.id] = cred;
|
||||
}
|
||||
|
||||
return accu;
|
||||
}, {});
|
||||
},
|
||||
upsertCredential(state: ICredentialsState, credential: ICredentialsResponse) {
|
||||
if (credential.id) {
|
||||
Vue.set(state.credentials, credential.id, credential);
|
||||
}
|
||||
},
|
||||
deleteCredential(state: ICredentialsState, id: string) {
|
||||
Vue.delete(state.credentials, id);
|
||||
},
|
||||
enableOAuthCredential(state: ICredentialsState, credential: ICredentialsResponse) {
|
||||
// enable oauth event to track change between modals
|
||||
},
|
||||
},
|
||||
getters: {
|
||||
allCredentialTypes(state: ICredentialsState): ICredentialType[] {
|
||||
return Object.values(state.credentialTypes)
|
||||
.sort((a, b) => a.displayName.localeCompare(b.displayName));
|
||||
},
|
||||
allCredentials(state: ICredentialsState): ICredentialsResponse[] {
|
||||
return Object.values(state.credentials)
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
},
|
||||
allCredentialsByType(state: ICredentialsState, getters: any): {[type: string]: ICredentialsResponse[]} { // tslint:disable-line:no-any
|
||||
const credentials = getters.allCredentials as ICredentialsResponse[];
|
||||
const types = getters.allCredentialTypes as ICredentialType[];
|
||||
|
||||
return types.reduce((accu: {[type: string]: ICredentialsResponse[]}, type: ICredentialType) => {
|
||||
accu[type.name] = credentials.filter((cred: ICredentialsResponse) => cred.type === type.name);
|
||||
|
||||
return accu;
|
||||
}, {});
|
||||
},
|
||||
getCredentialTypeByName: (state: ICredentialsState) => {
|
||||
return (type: string) => state.credentialTypes[type];
|
||||
},
|
||||
getCredentialById: (state: ICredentialsState) => {
|
||||
return (id: string) => state.credentials[id];
|
||||
},
|
||||
getCredentialsByType: (state: ICredentialsState, getters: any) => { // tslint:disable-line:no-any
|
||||
return (credentialType: string): ICredentialsResponse[] => {
|
||||
return getters.allCredentials.filter((credentialData: ICredentialsResponse) => credentialData.type === credentialType);
|
||||
};
|
||||
},
|
||||
getNodesWithAccess (state: ICredentialsState, getters: any, rootState: IRootState, rootGetters: any) { // tslint:disable-line:no-any
|
||||
return (credentialTypeName: string) => {
|
||||
const nodeTypes: INodeTypeDescription[] = rootGetters.allNodeTypes;
|
||||
|
||||
return nodeTypes.filter((nodeType: INodeTypeDescription) => {
|
||||
if (!nodeType.credentials) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const credentialTypeDescription of nodeType.credentials) {
|
||||
if (credentialTypeDescription.name === credentialTypeName ) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
};
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
fetchCredentialTypes: async (context: ActionContext<ICredentialsState, IRootState>) => {
|
||||
const credentialTypes = await getCredentialTypes(context.rootGetters.getRestApiContext);
|
||||
context.commit('setCredentialTypes', credentialTypes);
|
||||
},
|
||||
fetchAllCredentials: async (context: ActionContext<ICredentialsState, IRootState>) => {
|
||||
const credentials = await getAllCredentials(context.rootGetters.getRestApiContext);
|
||||
context.commit('setCredentials', credentials);
|
||||
},
|
||||
getCredentialData: async (context: ActionContext<ICredentialsState, IRootState>, { id }: {id: string}) => {
|
||||
return await getCredentialData(context.rootGetters.getRestApiContext, id);
|
||||
},
|
||||
createNewCredential: async (context: ActionContext<ICredentialsState, IRootState>, data: ICredentialsDecrypted) => {
|
||||
const credential = await createNewCredential(context.rootGetters.getRestApiContext, data);
|
||||
context.commit('upsertCredential', credential);
|
||||
|
||||
return credential;
|
||||
},
|
||||
updateCredential: async (context: ActionContext<ICredentialsState, IRootState>, params: {data: ICredentialsDecrypted, id: string}) => {
|
||||
const { id, data } = params;
|
||||
const credential = await updateCredential(context.rootGetters.getRestApiContext, id, data);
|
||||
context.commit('upsertCredential', credential);
|
||||
|
||||
return credential;
|
||||
},
|
||||
deleteCredential: async (context: ActionContext<ICredentialsState, IRootState>, { id }: {id: string}) => {
|
||||
const deleted = await deleteCredential(context.rootGetters.getRestApiContext, id);
|
||||
if (deleted) {
|
||||
context.commit('deleteCredential', id);
|
||||
}
|
||||
},
|
||||
oAuth2Authorize: async (context: ActionContext<ICredentialsState, IRootState>, data: ICredentialsResponse) => {
|
||||
return oAuth2CredentialAuthorize(context.rootGetters.getRestApiContext, data);
|
||||
},
|
||||
oAuth1Authorize: async (context: ActionContext<ICredentialsState, IRootState>, data: ICredentialsResponse) => {
|
||||
return oAuth1CredentialAuthorize(context.rootGetters.getRestApiContext, data);
|
||||
},
|
||||
testCredential: async (context: ActionContext<ICredentialsState, IRootState>, data: ICredentialsDecrypted): Promise<NodeCredentialTestResult> => {
|
||||
return testCredential(context.rootGetters.getRestApiContext, { credentials: data });
|
||||
},
|
||||
getNewCredentialName: async (context: ActionContext<ICredentialsState, IRootState>, params: { credentialTypeName: string }) => {
|
||||
try {
|
||||
const { credentialTypeName } = params;
|
||||
let newName = DEFAULT_CREDENTIAL_NAME;
|
||||
if (!TYPES_WITH_DEFAULT_NAME.includes(credentialTypeName)) {
|
||||
const { displayName } = context.getters.getCredentialTypeByName(credentialTypeName);
|
||||
newName = getAppNameFromCredType(displayName);
|
||||
newName = newName.length > 0 ? `${newName} ${DEFAULT_CREDENTIAL_POSTFIX}` : DEFAULT_CREDENTIAL_NAME;
|
||||
}
|
||||
|
||||
const res = await getCredentialsNewName(context.rootGetters.getRestApiContext, newName);
|
||||
return res.name;
|
||||
} catch (e) {
|
||||
return DEFAULT_CREDENTIAL_NAME;
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default module;
|
|
@ -1,4 +1,4 @@
|
|||
import { DUPLICATE_MODAL_KEY, TAGS_MANAGER_MODAL_KEY, VERSIONS_MODAL_KEY, WORKLOW_OPEN_MODAL_KEY } from '@/constants';
|
||||
import { CREDENTIAL_EDIT_MODAL_KEY, DUPLICATE_MODAL_KEY, TAGS_MANAGER_MODAL_KEY, VERSIONS_MODAL_KEY, WORKLOW_OPEN_MODAL_KEY, CREDENTIAL_SELECT_MODAL_KEY } from '@/constants';
|
||||
import Vue from 'vue';
|
||||
import { ActionContext, Module } from 'vuex';
|
||||
import {
|
||||
|
@ -10,6 +10,11 @@ const module: Module<IUiState, IRootState> = {
|
|||
namespaced: true,
|
||||
state: {
|
||||
modals: {
|
||||
[CREDENTIAL_EDIT_MODAL_KEY]: {
|
||||
open: false,
|
||||
mode: '',
|
||||
activeId: null,
|
||||
},
|
||||
[DUPLICATE_MODAL_KEY]: {
|
||||
open: false,
|
||||
},
|
||||
|
@ -22,6 +27,9 @@ const module: Module<IUiState, IRootState> = {
|
|||
[VERSIONS_MODAL_KEY]: {
|
||||
open: false,
|
||||
},
|
||||
[CREDENTIAL_SELECT_MODAL_KEY]: {
|
||||
open: false,
|
||||
},
|
||||
},
|
||||
modalStack: [],
|
||||
sidebarMenuCollapsed: true,
|
||||
|
@ -37,9 +45,23 @@ const module: Module<IUiState, IRootState> = {
|
|||
isModalActive: (state: IUiState) => {
|
||||
return (name: string) => state.modalStack.length > 0 && name === state.modalStack[0];
|
||||
},
|
||||
getModalActiveId: (state: IUiState) => {
|
||||
return (name: string) => state.modals[name].activeId;
|
||||
},
|
||||
getModalMode: (state: IUiState) => {
|
||||
return (name: string) => state.modals[name].mode;
|
||||
},
|
||||
sidebarMenuCollapsed: (state: IUiState): boolean => state.sidebarMenuCollapsed,
|
||||
},
|
||||
mutations: {
|
||||
setMode: (state: IUiState, params: {name: string, mode: string}) => {
|
||||
const { name, mode } = params;
|
||||
Vue.set(state.modals[name], 'mode', mode);
|
||||
},
|
||||
setActiveId: (state: IUiState, params: {name: string, id: string}) => {
|
||||
const { name, id } = params;
|
||||
Vue.set(state.modals[name], 'activeId', id);
|
||||
},
|
||||
openModal: (state: IUiState, name: string) => {
|
||||
Vue.set(state.modals[name], 'open', true);
|
||||
state.modalStack = [name].concat(state.modalStack);
|
||||
|
@ -47,6 +69,12 @@ const module: Module<IUiState, IRootState> = {
|
|||
closeTopModal: (state: IUiState) => {
|
||||
const name = state.modalStack[0];
|
||||
Vue.set(state.modals[name], 'open', false);
|
||||
if (state.modals.mode) {
|
||||
Vue.set(state.modals[name], 'mode', '');
|
||||
}
|
||||
if (state.modals.activeId) {
|
||||
Vue.set(state.modals[name], 'activeId', '');
|
||||
}
|
||||
|
||||
state.modalStack = state.modalStack.slice(1);
|
||||
},
|
||||
|
@ -67,7 +95,20 @@ const module: Module<IUiState, IRootState> = {
|
|||
openUpdatesPanel: async (context: ActionContext<IUiState, IRootState>) => {
|
||||
context.commit('openModal', VERSIONS_MODAL_KEY);
|
||||
},
|
||||
openExisitngCredential: async (context: ActionContext<IUiState, IRootState>, { id }: {id: string}) => {
|
||||
context.commit('setActiveId', {name: CREDENTIAL_EDIT_MODAL_KEY, id});
|
||||
context.commit('setMode', {name: CREDENTIAL_EDIT_MODAL_KEY, mode: 'edit'});
|
||||
context.commit('openModal', CREDENTIAL_EDIT_MODAL_KEY);
|
||||
},
|
||||
openNewCredential: async (context: ActionContext<IUiState, IRootState>, { type }: {type: string}) => {
|
||||
context.commit('setActiveId', {name: CREDENTIAL_EDIT_MODAL_KEY, id: type});
|
||||
context.commit('setMode', {name: CREDENTIAL_EDIT_MODAL_KEY, mode: 'new'});
|
||||
context.commit('openModal', CREDENTIAL_EDIT_MODAL_KEY);
|
||||
},
|
||||
openCredentialsSelectModal: async (context: ActionContext<IUiState, IRootState>) => {
|
||||
context.commit('openModal', CREDENTIAL_SELECT_MODAL_KEY);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default module;
|
||||
export default module;
|
||||
|
|
|
@ -21,17 +21,10 @@ body {
|
|||
font-weight: 400;
|
||||
}
|
||||
|
||||
|
||||
// Dialog
|
||||
.v-modal {
|
||||
opacity: .85;
|
||||
background-color: lighten($--custom-table-background-main, 55% );
|
||||
}
|
||||
|
||||
.el-dialog {
|
||||
background-color: $--custom-dialog-background;
|
||||
-webkit-box-shadow: none;
|
||||
box-shadow: none;
|
||||
border: var(--border-base);
|
||||
box-shadow: 0px 6px 16px rgb(68 28 23 / 6%);
|
||||
border-radius: 8px;
|
||||
|
||||
@media (max-height: 1050px) {
|
||||
margin: 4em auto !important;
|
||||
|
@ -40,42 +33,42 @@ body {
|
|||
@media (max-height: 930px) {
|
||||
margin: 1em auto !important;
|
||||
}
|
||||
&.classic {
|
||||
.el-dialog__header {
|
||||
padding: 15px 20px;
|
||||
}
|
||||
|
||||
.el-dialog__header {
|
||||
padding: 15px 20px;
|
||||
.el-dialog__title {
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
.el-dialog__headerbtn {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: -50px;
|
||||
color: #fff;
|
||||
background-color: $--custom-table-background-main;
|
||||
border-radius: 0 18px 18px 0;
|
||||
z-index: 110;
|
||||
font-size: 1.7em;
|
||||
text-align: center;
|
||||
line-height: 30px;
|
||||
height: 50px;
|
||||
width: 50px;
|
||||
.el-dialog__close {
|
||||
.el-dialog__headerbtn {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: -50px;
|
||||
color: #fff;
|
||||
font-weight: 400;
|
||||
background-color: $--custom-table-background-main;
|
||||
border-radius: 0 18px 18px 0;
|
||||
z-index: 110;
|
||||
font-size: 1.7em;
|
||||
text-align: center;
|
||||
line-height: 30px;
|
||||
height: 50px;
|
||||
width: 50px;
|
||||
.el-dialog__close {
|
||||
color: #fff;
|
||||
font-weight: 400;
|
||||
}
|
||||
.el-dialog__close:hover {
|
||||
transform: scale(1.2);
|
||||
}
|
||||
}
|
||||
.el-dialog__close:hover {
|
||||
transform: scale(1.2);
|
||||
.el-dialog__body {
|
||||
color: $--custom-dialog-text-color;
|
||||
padding: 0 20px 20px 20px;
|
||||
}
|
||||
.el-dialog__title {
|
||||
color: $--custom-dialog-text-color;
|
||||
}
|
||||
}
|
||||
.el-dialog__body {
|
||||
color: $--custom-dialog-text-color;
|
||||
padding: 0 20px 20px 20px;
|
||||
}
|
||||
.el-dialog__title {
|
||||
color: $--custom-dialog-text-color;
|
||||
}
|
||||
}
|
||||
|
||||
.el-message-box {
|
||||
background-color: $--custom-dialog-background;
|
||||
border: none;
|
||||
|
@ -90,71 +83,11 @@ body {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
// Menu
|
||||
.el-menu--vertical,
|
||||
.el-menu {
|
||||
border: none;
|
||||
font-size: 14px;
|
||||
|
||||
.el-menu--collapse {
|
||||
width: 75px;
|
||||
}
|
||||
|
||||
.el-menu--popup,
|
||||
.el-menu--inline {
|
||||
font-size: 0.9em;
|
||||
li.el-menu-item {
|
||||
height: 35px;
|
||||
line-height: 35px;
|
||||
color: $--custom-dialog-text-color;
|
||||
}
|
||||
}
|
||||
|
||||
.el-menu-item,
|
||||
.el-submenu__title {
|
||||
color: $--color-primary;
|
||||
font-size: 1.2em;
|
||||
.el-submenu__icon-arrow {
|
||||
color: $--color-primary;
|
||||
font-weight: 800;
|
||||
font-size: 1em;
|
||||
}
|
||||
.svg-inline--fa {
|
||||
position: relative;
|
||||
right: -3px;
|
||||
}
|
||||
.item-title {
|
||||
position: absolute;
|
||||
left: 73px;
|
||||
}
|
||||
.item-title-root {
|
||||
position: absolute;
|
||||
left: 60px;
|
||||
top: 1px;
|
||||
}
|
||||
|
||||
&:hover, &:focus {
|
||||
background-color: #fff0ef;
|
||||
}
|
||||
}
|
||||
}
|
||||
.el-menu--vertical {
|
||||
.el-menu-item {
|
||||
.item-title {
|
||||
position: absolute;
|
||||
left: 55px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Notification Message
|
||||
.el-message p {
|
||||
line-height: 1.5em;
|
||||
}
|
||||
|
||||
|
||||
// Table
|
||||
.el-table {
|
||||
thead th {
|
||||
|
|
|
@ -10,9 +10,7 @@ import Dialog from 'element-ui/lib/dialog';
|
|||
import Dropdown from 'element-ui/lib/dropdown';
|
||||
import DropdownMenu from 'element-ui/lib/dropdown-menu';
|
||||
import DropdownItem from 'element-ui/lib/dropdown-item';
|
||||
import Menu from 'element-ui/lib/menu';
|
||||
import Submenu from 'element-ui/lib/submenu';
|
||||
import MenuItem from 'element-ui/lib/menu-item';
|
||||
import Radio from 'element-ui/lib/radio';
|
||||
import RadioGroup from 'element-ui/lib/radio-group';
|
||||
import RadioButton from 'element-ui/lib/radio-button';
|
||||
|
@ -49,10 +47,14 @@ import locale from 'element-ui/lib/locale';
|
|||
import {
|
||||
N8nIconButton,
|
||||
N8nButton,
|
||||
N8nInfoTip,
|
||||
N8nInput,
|
||||
N8nInputLabel,
|
||||
N8nInputNumber,
|
||||
N8nMenu,
|
||||
N8nMenuItem,
|
||||
N8nSelect,
|
||||
N8nSpinner,
|
||||
N8nOption,
|
||||
} from 'n8n-design-system';
|
||||
import { ElMessageBoxOptions } from "element-ui/types/message-box";
|
||||
|
@ -62,10 +64,14 @@ Vue.use(Fragment.Plugin);
|
|||
// n8n design system
|
||||
Vue.use(N8nButton);
|
||||
Vue.use(N8nIconButton);
|
||||
Vue.use(N8nInfoTip);
|
||||
Vue.use(N8nInput);
|
||||
Vue.use(N8nInputLabel);
|
||||
Vue.use(N8nInputNumber);
|
||||
Vue.use(N8nMenu);
|
||||
Vue.use(N8nMenuItem);
|
||||
Vue.use(N8nSelect);
|
||||
Vue.use(N8nSpinner);
|
||||
Vue.use(N8nOption);
|
||||
|
||||
// element io
|
||||
|
@ -76,9 +82,7 @@ Vue.use(Drawer);
|
|||
Vue.use(Dropdown);
|
||||
Vue.use(DropdownMenu);
|
||||
Vue.use(DropdownItem);
|
||||
Vue.use(Menu);
|
||||
Vue.use(Submenu);
|
||||
Vue.use(MenuItem);
|
||||
Vue.use(Radio);
|
||||
Vue.use(RadioGroup);
|
||||
Vue.use(RadioButton);
|
||||
|
@ -131,6 +135,8 @@ Vue.prototype.$confirm = async (message: string, configOrTitle: string | ElMessa
|
|||
roundButton: true,
|
||||
cancelButtonClass: 'btn--cancel',
|
||||
confirmButtonClass: 'btn--confirm',
|
||||
showClose: false,
|
||||
closeOnClickModal: false,
|
||||
};
|
||||
|
||||
if (typeof configOrTitle === 'string') {
|
|
@ -13,6 +13,7 @@ import {
|
|||
faBug,
|
||||
faCalendar,
|
||||
faCheck,
|
||||
faCheckCircle,
|
||||
faChevronDown,
|
||||
faChevronUp,
|
||||
faCode,
|
||||
|
@ -97,6 +98,7 @@ library.add(faBook);
|
|||
library.add(faBug);
|
||||
library.add(faCalendar);
|
||||
library.add(faCheck);
|
||||
library.add(faCheckCircle);
|
||||
library.add(faChevronDown);
|
||||
library.add(faChevronUp);
|
||||
library.add(faCode);
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
import './icons';
|
||||
import './directives';
|
||||
import './compontents';
|
||||
import './components';
|
||||
|
|
|
@ -20,12 +20,6 @@ export default new Router({
|
|||
sidebar: MainSidebar,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/oauth2/callback',
|
||||
name: 'oAuth2Callback',
|
||||
components: {
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/workflow',
|
||||
name: 'NodeViewNew',
|
||||
|
|
|
@ -33,6 +33,7 @@ import {
|
|||
IRestApiContext,
|
||||
} from './Interface';
|
||||
|
||||
import credentials from './modules/credentials';
|
||||
import tags from './modules/tags';
|
||||
import ui from './modules/ui';
|
||||
import workflows from './modules/workflows';
|
||||
|
@ -47,8 +48,6 @@ const state: IRootState = {
|
|||
activeNode: null,
|
||||
// @ts-ignore
|
||||
baseUrl: process.env.VUE_APP_URL_BASE_API ? process.env.VUE_APP_URL_BASE_API : (window.BASE_PATH === '/%BASE_PATH%/' ? '/' : window.BASE_PATH),
|
||||
credentials: null,
|
||||
credentialTypes: null,
|
||||
endpointWebhook: 'webhook',
|
||||
endpointWebhookTest: 'webhook-test',
|
||||
executionId: null,
|
||||
|
@ -91,10 +90,11 @@ const state: IRootState = {
|
|||
};
|
||||
|
||||
const modules = {
|
||||
credentials,
|
||||
tags,
|
||||
ui,
|
||||
workflows,
|
||||
versions,
|
||||
ui,
|
||||
};
|
||||
|
||||
export const store = new Vuex.Store({
|
||||
|
@ -309,43 +309,6 @@ export const store = new Vuex.Store({
|
|||
}
|
||||
},
|
||||
|
||||
// Credentials
|
||||
addCredentials (state, credentialData: ICredentialsResponse) {
|
||||
if (state.credentials !== null) {
|
||||
state.credentials.push(credentialData);
|
||||
}
|
||||
},
|
||||
removeCredentials (state, credentialData: ICredentialsResponse) {
|
||||
if (state.credentials === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (let i = 0; i < state.credentials.length; i++) {
|
||||
if (state.credentials[i].id === credentialData.id) {
|
||||
state.credentials.splice(i, 1);
|
||||
return;
|
||||
}
|
||||
}
|
||||
},
|
||||
updateCredentials (state, credentialData: ICredentialsResponse) {
|
||||
if (state.credentials === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (let i = 0; i < state.credentials.length; i++) {
|
||||
if (state.credentials[i].id === credentialData.id) {
|
||||
state.credentials[i] = credentialData;
|
||||
return;
|
||||
}
|
||||
}
|
||||
},
|
||||
setCredentials (state, credentials: ICredentialsResponse[]) {
|
||||
Vue.set(state, 'credentials', credentials);
|
||||
},
|
||||
setCredentialTypes (state, credentialTypes: ICredentialType[]) {
|
||||
Vue.set(state, 'credentialTypes', credentialTypes);
|
||||
},
|
||||
|
||||
renameNodeSelectedAndExecution (state, nameData) {
|
||||
state.stateIsDirty = true;
|
||||
// If node has any WorkflowResultData rename also that one that the data
|
||||
|
@ -788,32 +751,6 @@ export const store = new Vuex.Store({
|
|||
}
|
||||
return false;
|
||||
},
|
||||
allCredentialTypes: (state): ICredentialType[] | null => {
|
||||
return state.credentialTypes;
|
||||
},
|
||||
allCredentials: (state): ICredentialsResponse[] | null => {
|
||||
return state.credentials;
|
||||
},
|
||||
credentialsByType: (state) => (credentialType: string): ICredentialsResponse[] | null => {
|
||||
if (state.credentials === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return state.credentials.filter((credentialData) => credentialData.type === credentialType);
|
||||
},
|
||||
credentialType: (state) => (credentialType: string): ICredentialType | null => {
|
||||
if (state.credentialTypes === null) {
|
||||
return null;
|
||||
}
|
||||
const foundType = state.credentialTypes.find(credentialData => {
|
||||
return credentialData.name === credentialType;
|
||||
});
|
||||
|
||||
if (foundType === undefined) {
|
||||
return null;
|
||||
}
|
||||
return foundType;
|
||||
},
|
||||
allNodeTypes: (state): INodeTypeDescription[] => {
|
||||
return state.nodeTypes;
|
||||
},
|
||||
|
|
|
@ -2201,12 +2201,10 @@ export default mixins(
|
|||
this.$store.commit('setNodeTypes', nodeTypes);
|
||||
},
|
||||
async loadCredentialTypes (): Promise<void> {
|
||||
const credentialTypes = await this.restApi().getCredentialTypes();
|
||||
this.$store.commit('setCredentialTypes', credentialTypes);
|
||||
await this.$store.dispatch('credentials/fetchCredentialTypes');
|
||||
},
|
||||
async loadCredentials (): Promise<void> {
|
||||
const credentials = await this.restApi().getAllCredentials();
|
||||
this.$store.commit('setCredentials', credentials);
|
||||
await this.$store.dispatch('credentials/fetchAllCredentials');
|
||||
},
|
||||
async loadNodesProperties(nodeNames: string[]): Promise<void> {
|
||||
const allNodes = this.$store.getters.allNodeTypes;
|
||||
|
|
1
packages/nodes-base/credentials/AWS.svg
Normal file
1
packages/nodes-base/credentials/AWS.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 333334 199332" shape-rendering="geometricPrecision" text-rendering="geometricPrecision" image-rendering="optimizeQuality" fill-rule="evenodd" clip-rule="evenodd"><defs><style>.fil1{fill:#f90}</style></defs><g id="Layer_x0020_1"><g id="amazon-web-services-2.svg"><path d="M93937 72393c0 4102 443 7428 1219 9867 887 2439 1996 5100 3548 7982 554 887 776 1774 776 2550 0 1109-665 2217-2106 3326l-6985 4656c-998 665-1995 998-2882 998-1109 0-2217-554-3326-1552-1552-1663-2882-3437-3991-5211-1109-1885-2217-3991-3437-6541-8648 10200-19512 15299-32594 15299-9312 0-16740-2661-22172-7982-5432-5322-8204-12417-8204-21286 0-9424 3326-17073 10089-22838s15743-8647 27161-8647c3769 0 7650 332 11752 887 4102 554 8315 1441 12749 2439v-8093c0-8426-1774-14301-5211-17738-3548-3437-9534-5100-18071-5100-3880 0-7871 443-11973 1441s-8093 2217-11973 3769c-1774 776-3104 1219-3880 1441s-1330 332-1774 332c-1552 0-2328-1109-2328-3437v-5432c0-1774 222-3104 776-3880s1552-1552 3104-2328c3880-1996 8537-3659 13969-4989C43606 885 49370 220 55468 220c13193 0 22838 2993 29046 8980 6098 5987 9202 15077 9202 27272v35920h222zM48926 89244c3659 0 7428-665 11419-1995s7539-3769 10532-7095c1774-2106 3104-4435 3770-7095 665-2661 1108-5876 1108-9645v-4656c-3215-776-6652-1441-10199-1885-3548-443-6984-665-10421-665-7428 0-12860 1441-16519 4435-3659 2993-5432 7206-5432 12749 0 5211 1330 9091 4102 11751 2661 2772 6541 4102 11641 4102zm89023 11973c-1996 0-3326-332-4213-1109-887-665-1663-2217-2328-4324l-26053-85697c-665-2217-998-3658-998-4434 0-1774 887-2772 2661-2772h10865c2106 0 3548 333 4324 1109 887 665 1552 2217 2217 4324l18625 73391 17295-73391c554-2217 1219-3659 2106-4324s2439-1109 4435-1109h8869c2106 0 3548 333 4435 1109 887 665 1663 2217 2106 4324l17516 74278 19180-74278c665-2217 1441-3659 2217-4324 887-665 2328-1109 4324-1109h10310c1774 0 2772 887 2772 2772 0 554-111 1109-222 1774s-333 1552-776 2772l-26718 85697c-665 2217-1441 3658-2328 4324-887 665-2328 1109-4213 1109h-9534c-2107 0-3548-333-4435-1109s-1663-2217-2106-4435l-17184-71507-17073 71396c-554 2217-1220 3658-2107 4434s-2439 1109-4434 1109h-9534zm142459 2993c-5765 0-11530-665-17073-1995s-9867-2772-12749-4435c-1774-998-2993-2106-3437-3104-443-998-665-2106-665-3104v-5654c0-2328 887-3437 2550-3437 665 0 1330 111 1995 333s1663 665 2772 1109c3769 1663 7871 2993 12195 3880 4435 887 8758 1330 13193 1330 6984 0 12417-1220 16186-3659s5765-5987 5765-10532c0-3104-998-5654-2993-7760-1996-2107-5765-3991-11197-5765l-16075-4989c-8093-2550-14080-6319-17738-11308-3658-4878-5543-10310-5543-16075 0-4656 998-8758 2993-12306s4656-6652 7982-9091c3326-2550 7095-4434 11530-5765S279190-2 284068-2c2439 0 4989 111 7428 443 2550 333 4878 776 7206 1219 2217 554 4324 1109 6319 1774s3548 1330 4656 1996c1552 887 2661 1774 3326 2771 665 887 998 2107 998 3659v5211c0 2328-887 3548-2550 3548-887 0-2328-444-4213-1331-6319-2882-13415-4324-21286-4324-6319 0-11308 998-14745 3104s-5211 5321-5211 9867c0 3104 1109 5765 3326 7871s6319 4213 12195 6097l15743 4989c7982 2550 13747 6098 17184 10643s5100 9756 5100 15521c0 4767-998 9091-2882 12860-1996 3770-4656 7095-8093 9756-3437 2771-7539 4767-12306 6208-4989 1552-10199 2328-15854 2328z" fill="#252f3e"/><path class="fil1" d="M301362 158091c-36474 26940-89467 41241-135031 41241-63858 0-121395-23614-164854-62859-3437-3104-332-7317 3770-4878 47006 27272 104988 43791 164964 43791 40465 0 84921-8426 125830-25721 6097-2772 11308 3991 5321 8426z"/><path class="fil1" d="M316550 140796c-4656-5987-30820-2883-42682-1441-3548 443-4102-2661-887-4989 20842-14634 55099-10421 59090-5543 3991 4989-1109 39246-20620 55653-2993 2550-5876 1220-4545-2106 4435-10976 14301-35698 9645-41574z"/></g></g></svg>
|
After Width: | Height: | Size: 3.6 KiB |
|
@ -8,6 +8,7 @@ export class Aws implements ICredentialType {
|
|||
name = 'aws';
|
||||
displayName = 'AWS';
|
||||
documentationUrl = 'aws';
|
||||
icon = 'file:AWS.svg';
|
||||
properties: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Region',
|
||||
|
|
1
packages/nodes-base/credentials/Google.svg
Normal file
1
packages/nodes-base/credentials/Google.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 48 48"><defs><path id="a" d="M44.5 20H24v8.5h11.8C34.7 33.9 30.1 37 24 37c-7.2 0-13-5.8-13-13s5.8-13 13-13c3.1 0 5.9 1.1 8.1 2.9l6.4-6.4C34.6 4.1 29.6 2 24 2 11.8 2 2 11.8 2 24s9.8 22 22 22c11 0 21-8 21-22 0-1.3-.2-2.7-.5-4z"/></defs><clipPath id="b"><use xlink:href="#a" overflow="visible"/></clipPath><path clip-path="url(#b)" fill="#FBBC05" d="M0 37V11l17 13z"/><path clip-path="url(#b)" fill="#EA4335" d="M0 11l17 13 7-6.1L48 14V0H0z"/><path clip-path="url(#b)" fill="#34A853" d="M0 37l30-23 7.9 1L48 0v48H0z"/><path clip-path="url(#b)" fill="#4285F4" d="M48 48L17 24l-4-3 35-10z"/></svg>
|
After Width: | Height: | Size: 688 B |
|
@ -8,6 +8,7 @@ export class GoogleApi implements ICredentialType {
|
|||
name = 'googleApi';
|
||||
displayName = 'Google API';
|
||||
documentationUrl = 'google';
|
||||
icon = 'file:Google.svg';
|
||||
properties: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Service Account Email',
|
||||
|
@ -15,6 +16,7 @@ export class GoogleApi implements ICredentialType {
|
|||
type: 'string',
|
||||
default: '',
|
||||
description: 'The Google Service account similar to user-808@project.iam.gserviceaccount.com.',
|
||||
required: true,
|
||||
|
||||
},
|
||||
{
|
||||
|
@ -23,6 +25,7 @@ export class GoogleApi implements ICredentialType {
|
|||
type: 'string',
|
||||
default: '',
|
||||
description: 'Use the multiline editor. Make sure there are exactly 3 lines.<br />-----BEGIN PRIVATE KEY-----<br />KEY IN A SINGLE LINE<br />-----END PRIVATE KEY-----',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
displayName: ' Impersonate a User',
|
||||
|
|
|
@ -10,6 +10,7 @@ export class GoogleOAuth2Api implements ICredentialType {
|
|||
];
|
||||
displayName = 'Google OAuth2 API';
|
||||
documentationUrl = 'google';
|
||||
icon = 'file:Google.svg';
|
||||
properties: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Authorization URL',
|
||||
|
|
|
@ -8,6 +8,7 @@ export class HttpBasicAuth implements ICredentialType {
|
|||
name = 'httpBasicAuth';
|
||||
displayName = 'Basic Auth';
|
||||
documentationUrl = 'httpRequest';
|
||||
icon = 'node:n8n-nodes-base.httpRequest';
|
||||
properties: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'User',
|
||||
|
|
|
@ -8,6 +8,7 @@ export class HttpDigestAuth implements ICredentialType {
|
|||
name = 'httpDigestAuth';
|
||||
displayName = 'Digest Auth';
|
||||
documentationUrl = 'httpRequest';
|
||||
icon = 'node:n8n-nodes-base.httpRequest';
|
||||
properties: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'User',
|
||||
|
|
|
@ -8,6 +8,7 @@ export class HttpHeaderAuth implements ICredentialType {
|
|||
name = 'httpHeaderAuth';
|
||||
displayName = 'Header Auth';
|
||||
documentationUrl = 'httpRequest';
|
||||
icon = 'node:n8n-nodes-base.httpRequest';
|
||||
properties: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Name',
|
||||
|
|
1
packages/nodes-base/credentials/Microsoft.svg
Normal file
1
packages/nodes-base/credentials/Microsoft.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 23 23"><path fill="#f3f3f3" d="M0 0h23v23H0z"/><path fill="#f35325" d="M1 1h10v10H1z"/><path fill="#81bc06" d="M12 1h10v10H12z"/><path fill="#05a6f0" d="M1 12h10v10H1z"/><path fill="#ffba08" d="M12 12h10v10H12z"/></svg>
|
After Width: | Height: | Size: 272 B |
|
@ -8,6 +8,7 @@ export class MicrosoftOAuth2Api implements ICredentialType {
|
|||
extends = [
|
||||
'oAuth2Api',
|
||||
];
|
||||
icon = 'file:Microsoft.svg';
|
||||
displayName = 'Microsoft OAuth2 API';
|
||||
documentationUrl = 'microsoft';
|
||||
properties: INodeProperties[] = [
|
||||
|
|
|
@ -14,6 +14,7 @@ export class SlackApi implements ICredentialType {
|
|||
name: 'accessToken',
|
||||
type: 'string',
|
||||
default: '',
|
||||
required: true,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@ import {
|
|||
|
||||
export class SshPassword implements ICredentialType {
|
||||
name = 'sshPassword';
|
||||
displayName = 'SSH';
|
||||
displayName = 'SSH Password';
|
||||
properties: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Host',
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue