mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-20 11:22:15 +00:00
🔀 Merge branch 'master' into fixed-collection-ux-improvements
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -43,10 +43,10 @@
|
||||
<el-table-column label="" width="30">
|
||||
<!-- eslint-disable-next-line vue/no-unused-vars -->
|
||||
<template slot="header" slot-scope="scope">
|
||||
<el-checkbox :indeterminate="isIndeterminate" v-model="checkAll" @change="handleCheckAllChange">Check all</el-checkbox>
|
||||
<el-checkbox :indeterminate="isIndeterminate" v-model="checkAll" @change="handleCheckAllChange" label=" "></el-checkbox>
|
||||
</template>
|
||||
<template slot-scope="scope">
|
||||
<el-checkbox v-if="scope.row.stoppedAt !== undefined && scope.row.id" :value="selectedItems[scope.row.id.toString()] || checkAll" @change="handleCheckboxChanged(scope.row.id)" >Check all</el-checkbox>
|
||||
<el-checkbox v-if="scope.row.stoppedAt !== undefined && scope.row.id" :value="selectedItems[scope.row.id.toString()] || checkAll" @change="handleCheckboxChanged(scope.row.id)" label=" "></el-checkbox>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column property="startedAt" label="Started At / ID" width="205">
|
||||
@@ -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>
|
||||
|
||||
@@ -26,10 +26,9 @@
|
||||
<n8n-select size="mini" v-model="maxDisplayItems" @click.stop>
|
||||
<n8n-option v-for="option in maxDisplayItemsOptions" :label="option" :value="option" :key="option" />
|
||||
</n8n-select>
|
||||
</span> /
|
||||
</span>/
|
||||
<strong>{{ dataCount }}</strong>
|
||||
</div>
|
||||
|
||||
<n8n-tooltip
|
||||
v-if="runMetadata"
|
||||
placement="right"
|
||||
@@ -41,7 +40,7 @@
|
||||
<font-awesome-icon icon="info-circle" class="primary-color" />
|
||||
</n8n-tooltip>
|
||||
<span v-if="maxOutputIndex > 0">
|
||||
| Output:
|
||||
| Output:
|
||||
</span>
|
||||
<span class="opts" v-if="maxOutputIndex > 0" >
|
||||
<n8n-select size="mini" v-model="outputIndex" @click.stop>
|
||||
@@ -51,7 +50,7 @@
|
||||
</span>
|
||||
|
||||
<span v-if="maxRunIndex > 0">
|
||||
| Data of Execution:
|
||||
| Data of Execution:
|
||||
</span>
|
||||
<span class="opts">
|
||||
<n8n-select v-if="maxRunIndex > 0" size="mini" v-model="runIndex" @click.stop>
|
||||
@@ -270,6 +269,9 @@ export default mixins(
|
||||
MAX_DISPLAY_ITEMS_AUTO_ALL,
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.init();
|
||||
},
|
||||
computed: {
|
||||
hasNodeRun(): boolean {
|
||||
return Boolean(this.node && this.workflowRunData && this.workflowRunData.hasOwnProperty(this.node.name));
|
||||
@@ -423,6 +425,18 @@ export default mixins(
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
init() {
|
||||
// Reset the selected output index every time another node gets selected
|
||||
this.outputIndex = 0;
|
||||
this.maxDisplayItems = 25;
|
||||
this.refreshDataSize();
|
||||
if (this.displayMode === 'Binary') {
|
||||
this.closeBinaryDataDisplay();
|
||||
if (this.binaryData.length === 0) {
|
||||
this.displayMode = 'Table';
|
||||
}
|
||||
}
|
||||
},
|
||||
closeBinaryDataDisplay () {
|
||||
this.binaryDataDisplayVisible = false;
|
||||
this.binaryDataDisplayData = null;
|
||||
@@ -607,17 +621,8 @@ export default mixins(
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
node (newNode, oldNode) {
|
||||
// Reset the selected output index every time another node gets selected
|
||||
this.outputIndex = 0;
|
||||
this.maxDisplayItems = 25;
|
||||
this.refreshDataSize();
|
||||
if (this.displayMode === 'Binary') {
|
||||
this.closeBinaryDataDisplay();
|
||||
if (this.binaryData.length === 0) {
|
||||
this.displayMode = 'Table';
|
||||
}
|
||||
}
|
||||
node() {
|
||||
this.init();
|
||||
},
|
||||
jsonData () {
|
||||
this.refreshDataSize();
|
||||
@@ -630,8 +635,6 @@ export default mixins(
|
||||
this.runIndex = Math.min(this.runIndex, this.maxRunIndex);
|
||||
},
|
||||
},
|
||||
mounted () {
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -639,14 +642,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 +654,7 @@ export default mixins(
|
||||
right: 0;
|
||||
overflow-y: auto;
|
||||
line-height: 1.5;
|
||||
word-break: normal;
|
||||
|
||||
.binary-data-row {
|
||||
display: inline-flex;
|
||||
@@ -795,6 +793,10 @@ export default mixins(
|
||||
.title-text {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
||||
> * {
|
||||
margin-right: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.title-data-display-selector {
|
||||
|
||||
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;
|
||||
|
||||
Reference in New Issue
Block a user