Files
Automata/packages/editor-ui/src/components/NodeCredentials.vue
Milorad FIlipović bae3098e4e refactor(editor): Finish pinia migration, remove all vuex dependancies (#4533)
*  Added pinia support. Migrated community nodes module.
*  Added ui pinia store, moved some data from root store to it, updated modals to work with pinia stores
*  Added ui pinia store and migrated a part of the root store
*  Migrated `settings` store to pinia
*  Removing vuex store refs from router
*  Migrated `users` module to pinia store
*  Fixing errors after sync with master
*  One more error after merge
*  Created `workflows` pinia store. Moved large part of root store to it. Started updating references.
*  Finished migrating workflows store to pinia
*  Renaming some getters and actions to make more sense
*  Finished migrating the root store to pinia
*  Migrated ndv store to pinia
*  Renaming main panel dimensions getter so it doesn't clash with data prop name
* ✔️ Fixing lint errors
*  Migrated `templates` store to pinia
*  Migrated the `nodeTypes`store
*  Removed unused pieces of code and oold vuex modules
*  Adding vuex calls to pinia store, fixing wrong references
* 💄 Removing leftover $store refs
*  Added legacy getters and mutations to store to support webhooks
*  Added missing front-end hooks, updated vuex state subscriptions to pinia
* ✔️ Fixing linting errors
*  Removing vue composition api plugin
*  Fixing main sidebar state when loading node view
* 🐛 Fixing an error when activating workflows
* 🐛 Fixing isses with workflow settings and executions auto-refresh
* 🐛 Removing duplicate listeners which cause import error
* 🐛 Fixing route authentication
*  Updating freshly pulled $store refs
*  Adding deleted const
*  Updating store references in ee features. Reseting NodeView credentials update flag when resetting workspace
*  Adding return type to email submission modal
*  Making NodeView only react to paste event when active
* 🐛 Fixing signup view errors
*  Started migrating the `credentials` module to pinia
* 👌 Addressing PR review comments
*  Migrated permissions module to pinia
*  Migrated `nodeCreator`, `tags` and `versions` modules to pinia
*  Implemented webhooks pinia store
*  Removing all leftover vuex files and references
*  Removing final vuex refs
*  Updating expected credentialId type
*  Removing node credentials subscription code, reducing node click debounce timeout
* 🐛 Fixing pushing nodes downstream when inserting new node
* ✔️ Fixing a lint error in new type guard
*  Updating helper reference
* ✔️ Removing unnecessary awaits
*  fix(editor): remove unnecessary imports from NDV
*  Merging mapStores blocks in NodeView
*  fix(editor): make sure JS Plumb not loaded earlier than needed
*  Updating type guard nad credentials subscriptions
*  Updating type guard so it doesn't use `any` type

Co-authored-by: Csaba Tuncsik <csaba@n8n.io>
2022-11-09 10:01:50 +01:00

386 lines
12 KiB
Vue

<template>
<div v-if="credentialTypesNodeDescriptionDisplayed.length" :class="['node-credentials', $style.container]">
<div v-for="credentialTypeDescription in credentialTypesNodeDescriptionDisplayed" :key="credentialTypeDescription.name">
<n8n-input-label
:label="$locale.baseText(
'nodeCredentials.credentialFor',
{
interpolate: {
credentialType: credentialTypeNames[credentialTypeDescription.name]
}
}
)"
:bold="false"
:set="issues = getIssues(credentialTypeDescription.name)"
size="small"
color="text-dark"
>
<div v-if="isReadOnly">
<n8n-input
:value="selected && selected[credentialTypeDescription.name] && selected[credentialTypeDescription.name].name"
disabled
size="small"
/>
</div>
<div
v-else
:class="issues.length ? $style.hasIssues : $style.input"
>
<n8n-select :value="getSelectedId(credentialTypeDescription.name)" @change="(value) => onCredentialSelected(credentialTypeDescription.name, value)" :placeholder="$locale.baseText('nodeCredentials.selectCredential')" size="small">
<n8n-option
v-for="(item) in getCredentialOptions(credentialTypeDescription.name)"
:key="item.id"
:label="item.name"
:value="item.id">
</n8n-option>
<n8n-option
:key="NEW_CREDENTIALS_TEXT"
:value="NEW_CREDENTIALS_TEXT"
:label="NEW_CREDENTIALS_TEXT"
>
</n8n-option>
</n8n-select>
<div :class="$style.warning" v-if="issues.length">
<n8n-tooltip placement="top" >
<titled-list slot="content" :title="`${$locale.baseText('nodeCredentials.issues')}:`" :items="issues" />
<font-awesome-icon icon="exclamation-triangle" />
</n8n-tooltip>
</div>
<div :class="$style.edit" v-if="selected[credentialTypeDescription.name] && isCredentialExisting(credentialTypeDescription.name)">
<font-awesome-icon icon="pen" @click="editCredential(credentialTypeDescription.name)" class="clickable" :title="$locale.baseText('nodeCredentials.updateCredential')" />
</div>
</div>
</n8n-input-label>
</div>
</div>
</template>
<script lang="ts">
import { restApi } from '@/components/mixins/restApi';
import {
ICredentialsResponse,
INodeUi,
INodeUpdatePropertiesInformation,
IUser,
} from '@/Interface';
import {
ICredentialType,
INodeCredentialDescription,
INodeCredentialsDetails,
} from 'n8n-workflow';
import { genericHelpers } from '@/components/mixins/genericHelpers';
import { nodeHelpers } from '@/components/mixins/nodeHelpers';
import { showMessage } from '@/components/mixins/showMessage';
import TitledList from '@/components/TitledList.vue';
import mixins from 'vue-typed-mixins';
import {getCredentialPermissions} from "@/permissions";
import { mapStores } from 'pinia';
import { useUIStore } from '@/stores/ui';
import { useUsersStore } from '@/stores/users';
import { useWorkflowsStore } from '@/stores/workflows';
import { useNodeTypesStore } from '@/stores/nodeTypes';
import { useCredentialsStore } from '@/stores/credentials';
export default mixins(
genericHelpers,
nodeHelpers,
restApi,
showMessage,
).extend({
name: 'NodeCredentials',
props: [
'node', // INodeUi
'overrideCredType', // cred type
],
components: {
TitledList,
},
data () {
return {
NEW_CREDENTIALS_TEXT: `- ${this.$locale.baseText('nodeCredentials.createNew')} -`,
subscribedToCredentialType: '',
};
},
mounted() {
this.listenForNewCredentials();
},
computed: {
...mapStores(
useCredentialsStore,
useNodeTypesStore,
useUIStore,
useUsersStore,
useWorkflowsStore,
),
allCredentialsByType(): {[type: string]: ICredentialsResponse[]} {
return this.credentialsStore.allCredentialsByType;
},
currentUser (): IUser {
return this.usersStore.currentUser || {} as IUser;
},
credentialTypesNode (): string[] {
return this.credentialTypesNodeDescription
.map((credentialTypeDescription) => credentialTypeDescription.name);
},
credentialTypesNodeDescriptionDisplayed (): INodeCredentialDescription[] {
return this.credentialTypesNodeDescription
.filter((credentialTypeDescription) => {
return this.displayCredentials(credentialTypeDescription);
});
},
credentialTypesNodeDescription (): INodeCredentialDescription[] {
const node = this.node as INodeUi;
const credType = this.credentialsStore.getCredentialTypeByName(this.overrideCredType);
if (credType) return [credType];
const activeNodeType = this.nodeTypesStore.getNodeType(node.type, node.typeVersion);
if (activeNodeType && activeNodeType.credentials) {
return activeNodeType.credentials;
}
return [];
},
credentialTypeNames () {
const returnData: {
[key: string]: string;
} = {};
let credentialType: ICredentialType | null;
for (const credentialTypeName of this.credentialTypesNode) {
credentialType = this.credentialsStore.getCredentialTypeByName(credentialTypeName);
returnData[credentialTypeName] = credentialType !== null ? credentialType.displayName : credentialTypeName;
}
return returnData;
},
selected(): {[type: string]: INodeCredentialsDetails} {
return this.node.credentials || {};
},
},
methods: {
getCredentialOptions(type: string): ICredentialsResponse[] {
return (this.allCredentialsByType as Record<string, ICredentialsResponse[]>)[type].filter((credential) => {
const permissions = getCredentialPermissions(this.currentUser, credential);
return permissions.use;
});
},
getSelectedId(type: string) {
if (this.isCredentialExisting(type)) {
return this.selected[type].id;
}
return undefined;
},
credentialInputWrapperStyle (credentialType: string) {
let deductWidth = 0;
const styles = {
width: '100%',
};
if (this.getIssues(credentialType).length) {
deductWidth += 20;
}
if (deductWidth !== 0) {
styles.width = `calc(100% - ${deductWidth}px)`;
}
return styles;
},
// TODO: Investigate if this can be solved using only the store data (storing selected flag in credentials objects, ...)
listenForNewCredentials() {
// Listen for credentials store changes so credential selection can be updated if creds are changed from the modal
this.credentialsStore.$subscribe((mutation, state) => {
// This data pro stores credential type that the component is currently interested in
const credentialType = this.subscribedToCredentialType;
const credentialsOfType = this.credentialsStore.allCredentialsByType[credentialType].sort((a, b) => (a.id < b.id ? -1 : 1));
if (credentialsOfType.length > 0) {
// If nothing has been selected previously, select the first one (newly added)
if (!this.selected[credentialType]) {
this.onCredentialSelected(credentialType, credentialsOfType[0].id);
} else {
// Else, check id currently selected cred has been updated
const newSelected = credentialsOfType.find(cred => cred.id === this.selected[credentialType].id);
// If it has changed, select it
if (newSelected && newSelected.name !== this.selected[credentialType].name) {
this.onCredentialSelected(credentialType, newSelected.id);
} else { // Else select the last cred with that type since selected has been deleted or a new one has been added
this.onCredentialSelected(credentialType, credentialsOfType[credentialsOfType.length - 1].id);
}
}
}
this.subscribedToCredentialType = '';
});
},
clearSelectedCredential(credentialType: string) {
const node: INodeUi = this.node;
const credentials = {
...(node.credentials || {}),
};
delete credentials[credentialType];
const updateInformation: INodeUpdatePropertiesInformation = {
name: this.node.name,
properties: {
credentials,
},
};
this.$emit('credentialSelected', updateInformation);
},
onCredentialSelected (credentialType: string, credentialId: string | null | undefined) {
if (credentialId === this.NEW_CREDENTIALS_TEXT) {
// this.listenForNewCredentials(credentialType);
this.subscribedToCredentialType = credentialType;
}
if (!credentialId || credentialId === this.NEW_CREDENTIALS_TEXT) {
this.uiStore.openNewCredential(credentialType);
this.$telemetry.track('User opened Credential modal', { credential_type: credentialType, source: 'node', new_credential: true, workflow_id: this.workflowsStore.workflowId });
return;
}
this.$telemetry.track(
'User selected credential from node modal',
{
credential_type: credentialType,
node_type: this.node.type,
...(this.hasProxyAuth(this.node) ? { is_service_specific: true } : {}),
workflow_id: this.workflowsStore.workflowId,
credential_id: credentialId,
},
);
const selectedCredentials = this.credentialsStore.getCredentialById(credentialId);
const oldCredentials = this.node.credentials && this.node.credentials[credentialType] ? this.node.credentials[credentialType] : {};
const selected = { id: selectedCredentials.id, name: selectedCredentials.name };
// if credentials has been string or neither id matched nor name matched uniquely
if (oldCredentials.id === null || (oldCredentials.id && !this.credentialsStore.getCredentialByIdAndType(oldCredentials.id, credentialType))) {
// update all nodes in the workflow with the same old/invalid credentials
this.workflowsStore.replaceInvalidWorkflowCredentials({
credentials: selected,
invalid: oldCredentials,
type: credentialType,
});
this.updateNodesCredentialsIssues();
this.$showMessage({
title: this.$locale.baseText('nodeCredentials.showMessage.title'),
message: this.$locale.baseText(
'nodeCredentials.showMessage.message',
{
interpolate: {
oldCredentialName: oldCredentials.name,
newCredentialName: selected.name,
},
},
),
type: 'success',
});
}
const node: INodeUi = this.node;
const credentials = {
...(node.credentials || {}),
[credentialType]: selected,
};
const updateInformation: INodeUpdatePropertiesInformation = {
name: this.node.name,
properties: {
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
return true;
}
return this.displayParameter(this.node.parameters, credentialTypeDescription, '', this.node);
},
getIssues (credentialTypeName: string): string[] {
const node = this.node as INodeUi;
if (node.issues === undefined || node.issues.credentials === undefined) {
return [];
}
if (!node.issues.credentials.hasOwnProperty(credentialTypeName)) {
return [];
}
return node.issues.credentials[credentialTypeName];
},
isCredentialExisting(credentialType: string): boolean {
if (!this.node.credentials || !this.node.credentials[credentialType] || !this.node.credentials[credentialType].id) {
return false;
}
const { id } = this.node.credentials[credentialType];
const options = this.getCredentialOptions(credentialType);
return !!options.find((option: ICredentialsResponse) => option.id === id);
},
editCredential(credentialType: string): void {
const { id } = this.node.credentials[credentialType];
this.uiStore.openExistingCredential(id);
this.$telemetry.track('User opened Credential modal', { credential_type: credentialType, source: 'node', new_credential: false, workflow_id: this.workflowsStore.workflowId });
this.subscribedToCredentialType = credentialType;
},
},
});
</script>
<style lang="scss" module>
.container {
margin-top: var(--spacing-xs);
& > div:not(:first-child) {
margin-top: var(--spacing-xs);
}
}
.warning {
min-width: 20px;
margin-left: 5px;
color: #ff8080;
font-size: var(--font-size-s);
}
.edit {
display: flex;
justify-content: center;
align-items: center;
color: var(--color-text-base);
min-width: 20px;
margin-left: 5px;
font-size: var(--font-size-s);
}
.input {
display: flex;
align-items: center;
}
.hasIssues {
composes: input;
--input-border-color: var(--color-danger);
}
</style>