mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 18:12:04 +00:00
feat(editor): Simplify NDV by moving authentication details to credentials modal (#5067)
* ⚡ Removing authentication parameter from NDV * ⚡ Added auth type selector to credentials modal * 🔨 Extracting reusable logic to util functions * ⚡ Updating credentials position, adding label for radio buttons * ⚡ Using first node credentials for nodes with single auth options and hiding auth selector UI in that case * ⚡ Fixing credentials modal when opened from credentials page * ⚡ Showing all available credentials in NDV credentials dropdown * ⚡ Updating node credentials dropdown component to show credentials description. Disabling `Credentials of type not found` error in node * ⚡ Moving auth related fields from NDV to credentials modal. Added support for multiple auth fileds * ⚡ Moving NDV fields that authentication depends on to credentials modal * ⚡ Keeping old auth/credentials UI in NDV for HTTP Request and Webhook nodes. Pre-populating credential type for HTTP request node when selected from 'app action' menu * 💄 Use old label and field position for nodes that use old credentials UI in NDV * ⚡ Implementing more generic way to find node's auth fileds * 📚 Adding comments on parameter hiding logic * ⚡ Fixing node auth options logic for multiple auth fields * 👕 Fixing lint errors * 💄 Addressing design review comments * ⚡ Not selecting first auth option when opening new credential dialog * ⚡ Using default credentials name and icon if authentication type is not selected * ⚡ Updating credential data when auth type is changed * ⚡ Setting new credentials type for HTTP Request and Webhook nodes * ⚡ Setting nodes with access when changing auth type * 👕 Fixing lint error * ⚡ Updating active node auth type from credentials modal * ⚡ Syncronizing credentials modal and dropdown * 👕 Fixing linter error * ⚡ Handling credential dropdown UI for multiple credentials * 👕 Removing unused imports * ⚡ Handling auth selection when default auth type is the first option * ⚡ Updating credentials change listening logic * ⚡ Resetting credential data when deleting a credential, disabling 'Details' and 'Sharing' tabs if auth type is not selected * 🐛 Skipping credentials type check when showing mixed credentials in the dropdown and switching credentials type * ⚡ Showing credential modal tabs for saved credentials * ⚡ Preventing renaming credentials when no auth type is selected * 🐛 Fixing credentials modal when opened from credentials page * ⚡ Keeping auth radio buttons selected when switching tabs * ✅ Adding initial batch of credentials NDV tests * ⚡ Updating node auth filed value when new credential type is selected * ⚡ Using all available credential types for current node to sync credential dropdown with modal * ⚡ Sorting mixed credentials by date, simplifying credential dropdown option logic * 🔨 Extracting some reusable logic to utils * ⚡ Improving required vs optional credentials detection and using it to show auth radio buttons * 👕 Fixing lint errors * ✅ Adding more credentials tests * ⚡ Filtering credential options based on authentication type * 🔨 Refactoring credentials and auth utils * ⚡ Updated handling of auth options in credentials modal to work with new logic * 🔨 Getting the terminology in line * 📚 Removing leftover comment * ⚡ Updating node auth filed detection logic to account for different edge-cases * ⚡ Adding Wait node as an exception for new UI * ⚡ Updating NDV display when auth type changes * ⚡ Updating default credentials name when auth type changes * ⚡ Hiding auth settings after credentials are saved * ⚡ Always showing credentials modal menu tabs * ⚡ Improving main auth field detection logic so it doesn't account for authentication fields which can have `none` value * ⚡ Restoring accidentally deleted not existing credential issue logic * ⚡ Updating other nodes when deleted credentials have been updated * ⚡ Using filtered auth type list to show or hide radio buttons section in credentials modal * 👕 Addressing lint error * 👌 Addressing PR review feedback * 👕 Fixing lint issues * ⚡ Updating main auth filed detection logic so it checks full dependency path to determine if the field is required or optional * 👌 Addressing the rest of PR feedback * ✅ Updating credential tests * ⚡ Resetting credential data on authentication type change * ⚡ Created AuthTypeSelector component * 👌 Addressing PR comments * ⚡ Not resetting overwritten credential properties when changing auth type * ⚡ Hiding auth selector section if there are no options to show
This commit is contained in:
committed by
GitHub
parent
874c735d0a
commit
b321c5e4ec
@@ -8,17 +8,12 @@
|
||||
:key="credentialTypeDescription.name"
|
||||
>
|
||||
<n8n-input-label
|
||||
:label="
|
||||
$locale.baseText('nodeCredentials.credentialFor', {
|
||||
interpolate: {
|
||||
credentialType: credentialTypeNames[credentialTypeDescription.name],
|
||||
},
|
||||
})
|
||||
"
|
||||
:label="getCredentialsFieldLabel(credentialTypeDescription)"
|
||||
:bold="false"
|
||||
:set="(issues = getIssues(credentialTypeDescription.name))"
|
||||
size="small"
|
||||
color="text-dark"
|
||||
data-test-id="credentials-label"
|
||||
>
|
||||
<div v-if="readonly || isReadOnly">
|
||||
<n8n-input
|
||||
@@ -27,20 +22,37 @@
|
||||
size="small"
|
||||
/>
|
||||
</div>
|
||||
<div v-else :class="issues.length && !hideIssues ? $style.hasIssues : $style.input">
|
||||
<div
|
||||
v-else
|
||||
:class="issues.length && !hideIssues ? $style.hasIssues : $style.input"
|
||||
data-test-id="node-credentials-select"
|
||||
>
|
||||
<n8n-select
|
||||
:value="getSelectedId(credentialTypeDescription.name)"
|
||||
@change="(value) => onCredentialSelected(credentialTypeDescription.name, value)"
|
||||
@change="
|
||||
(value) =>
|
||||
onCredentialSelected(
|
||||
credentialTypeDescription.name,
|
||||
value,
|
||||
showMixedCredentials(credentialTypeDescription),
|
||||
)
|
||||
"
|
||||
@blur="$emit('blur', 'credentials')"
|
||||
:placeholder="getSelectPlaceholder(credentialTypeDescription.name, issues)"
|
||||
size="small"
|
||||
>
|
||||
<n8n-option
|
||||
v-for="item in getCredentialOptions(credentialTypeDescription.name)"
|
||||
v-for="item in getCredentialOptions(
|
||||
getAllRelatedCredentialTypes(credentialTypeDescription),
|
||||
)"
|
||||
:key="item.id"
|
||||
:label="item.name"
|
||||
:value="item.id"
|
||||
>
|
||||
<div :class="[$style.credentialOption, 'mt-2xs', 'mb-2xs']">
|
||||
<n8n-text bold>{{ item.name }}</n8n-text>
|
||||
<n8n-text size="small">{{ item.typeDisplayName }}</n8n-text>
|
||||
</div>
|
||||
</n8n-option>
|
||||
<n8n-option
|
||||
:key="NEW_CREDENTIALS_TEXT"
|
||||
@@ -68,6 +80,7 @@
|
||||
selected[credentialTypeDescription.name] &&
|
||||
isCredentialExisting(credentialTypeDescription.name)
|
||||
"
|
||||
data-test-id="credential-edit-button"
|
||||
>
|
||||
<font-awesome-icon
|
||||
icon="pen"
|
||||
@@ -91,7 +104,14 @@ import {
|
||||
INodeUpdatePropertiesInformation,
|
||||
IUser,
|
||||
} from '@/Interface';
|
||||
import { ICredentialType, INodeCredentialDescription, INodeCredentialsDetails } from 'n8n-workflow';
|
||||
import {
|
||||
ICredentialType,
|
||||
INodeCredentialDescription,
|
||||
INodeCredentialsDetails,
|
||||
INodeParameters,
|
||||
INodeProperties,
|
||||
INodeTypeDescription,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import { genericHelpers } from '@/mixins/genericHelpers';
|
||||
import { nodeHelpers } from '@/mixins/nodeHelpers';
|
||||
@@ -100,13 +120,26 @@ import { showMessage } from '@/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';
|
||||
import { useNDVStore } from '@/stores/ndv';
|
||||
import { KEEP_AUTH_IN_NDV_FOR_NODES } from '@/constants';
|
||||
import {
|
||||
getAuthTypeForNodeCredential,
|
||||
getMainAuthField,
|
||||
getNodeCredentialForSelectedAuthType,
|
||||
getAllNodeCredentialForAuthType,
|
||||
updateNodeAuthType,
|
||||
isRequiredCredential,
|
||||
} from '@/utils';
|
||||
|
||||
interface CredentialDropdownOption extends ICredentialsResponse {
|
||||
typeDisplayName: string;
|
||||
}
|
||||
|
||||
export default mixins(genericHelpers, nodeHelpers, restApi, showMessage).extend({
|
||||
name: 'NodeCredentials',
|
||||
@@ -122,6 +155,10 @@ export default mixins(genericHelpers, nodeHelpers, restApi, showMessage).extend(
|
||||
overrideCredType: {
|
||||
type: String,
|
||||
},
|
||||
showAll: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
hideIssues: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
@@ -134,15 +171,101 @@ export default mixins(genericHelpers, nodeHelpers, restApi, showMessage).extend(
|
||||
return {
|
||||
NEW_CREDENTIALS_TEXT: `- ${this.$locale.baseText('nodeCredentials.createNew')} -`,
|
||||
subscribedToCredentialType: '',
|
||||
listeningForAuthChange: false,
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.listenForCredentialUpdates();
|
||||
// Listen for credentials store changes so credential selection can be updated if creds are changed from the modal
|
||||
this.credentialsStore.$onAction(({ name, after, store, args }) => {
|
||||
const listeningForActions = ['createNewCredential', 'updateCredential', 'deleteCredential'];
|
||||
const credentialType = this.subscribedToCredentialType;
|
||||
if (!credentialType) {
|
||||
return;
|
||||
}
|
||||
|
||||
after(async (result) => {
|
||||
if (!listeningForActions.includes(name)) {
|
||||
return;
|
||||
}
|
||||
const current = this.selected[credentialType];
|
||||
let credentialsOfType: ICredentialsResponse[] = [];
|
||||
if (this.showAll) {
|
||||
if (this.node) {
|
||||
credentialsOfType = [
|
||||
...(this.credentialsStore.allUsableCredentialsForNode(this.node) || []),
|
||||
];
|
||||
}
|
||||
} else {
|
||||
credentialsOfType = [
|
||||
...(this.credentialsStore.allUsableCredentialsByType[credentialType] || []),
|
||||
];
|
||||
}
|
||||
switch (name) {
|
||||
// new credential was added
|
||||
case 'createNewCredential':
|
||||
if (result) {
|
||||
this.onCredentialSelected(credentialType, (result as ICredentialsResponse).id);
|
||||
}
|
||||
break;
|
||||
case 'updateCredential':
|
||||
const updatedCredential = result as ICredentialsResponse;
|
||||
// credential name was changed, update it
|
||||
if (updatedCredential.name !== current.name) {
|
||||
this.onCredentialSelected(credentialType, current.id);
|
||||
}
|
||||
break;
|
||||
case 'deleteCredential':
|
||||
// all credentials were deleted
|
||||
if (credentialsOfType.length === 0) {
|
||||
this.clearSelectedCredential(credentialType);
|
||||
} else {
|
||||
const id = args[0].id;
|
||||
// credential was deleted, select last one added to replace with
|
||||
if (current.id === id) {
|
||||
this.onCredentialSelected(
|
||||
credentialType,
|
||||
credentialsOfType[credentialsOfType.length - 1].id,
|
||||
);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
watch: {
|
||||
'node.parameters': {
|
||||
immediate: true,
|
||||
deep: true,
|
||||
handler(newValue: INodeParameters, oldValue: INodeParameters) {
|
||||
// When active node parameters change, check if authentication type has been changed
|
||||
// and set `subscribedToCredentialType` to corresponding credential type
|
||||
const isActive = this.node.name === this.ndvStore.activeNode?.name;
|
||||
const nodeType = this.nodeTypesStore.getNodeType(this.node.type, this.node.typeVersion);
|
||||
// Only do this for active node and if it's listening for auth change
|
||||
if (isActive && nodeType && this.listeningForAuthChange) {
|
||||
if (this.mainNodeAuthField && oldValue && newValue) {
|
||||
const newAuth = newValue[this.mainNodeAuthField.name];
|
||||
|
||||
if (newAuth) {
|
||||
const credentialType = getNodeCredentialForSelectedAuthType(
|
||||
nodeType,
|
||||
newAuth.toString(),
|
||||
);
|
||||
if (credentialType) {
|
||||
this.subscribedToCredentialType = credentialType.name;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapStores(
|
||||
useCredentialsStore,
|
||||
useNodeTypesStore,
|
||||
useNDVStore,
|
||||
useUIStore,
|
||||
useUsersStore,
|
||||
useWorkflowsStore,
|
||||
@@ -189,11 +312,39 @@ export default mixins(genericHelpers, nodeHelpers, restApi, showMessage).extend(
|
||||
selected(): { [type: string]: INodeCredentialsDetails } {
|
||||
return this.node.credentials || {};
|
||||
},
|
||||
nodeType(): INodeTypeDescription | null {
|
||||
return this.nodeTypesStore.getNodeType(this.node.type, this.node.typeVersion);
|
||||
},
|
||||
mainNodeAuthField(): INodeProperties | null {
|
||||
return getMainAuthField(this.nodeType);
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
getCredentialOptions(type: string): ICredentialsResponse[] {
|
||||
return this.credentialsStore.allUsableCredentialsByType[type];
|
||||
getAllRelatedCredentialTypes(credentialType: INodeCredentialDescription): string[] {
|
||||
const isRequiredCredential = this.showMixedCredentials(credentialType);
|
||||
if (isRequiredCredential) {
|
||||
if (this.mainNodeAuthField) {
|
||||
const credentials = getAllNodeCredentialForAuthType(
|
||||
this.nodeType,
|
||||
this.mainNodeAuthField.name,
|
||||
);
|
||||
return credentials.map((cred) => cred.name);
|
||||
}
|
||||
}
|
||||
return [credentialType.name];
|
||||
},
|
||||
getCredentialOptions(types: string[]): CredentialDropdownOption[] {
|
||||
let options: CredentialDropdownOption[] = [];
|
||||
types.forEach((type) => {
|
||||
options = options.concat(
|
||||
this.credentialsStore.allUsableCredentialsByType[type].map((option: any) => ({
|
||||
...option,
|
||||
typeDisplayName: this.credentialsStore.getCredentialTypeByName(type).displayName,
|
||||
})),
|
||||
);
|
||||
});
|
||||
return options;
|
||||
},
|
||||
getSelectedId(type: string) {
|
||||
if (this.isCredentialExisting(type)) {
|
||||
@@ -226,70 +377,6 @@ export default mixins(genericHelpers, nodeHelpers, restApi, showMessage).extend(
|
||||
|
||||
return styles;
|
||||
},
|
||||
// Listen for credentials store changes so credential selection can be updated if creds are changed from the modal
|
||||
listenForCredentialUpdates() {
|
||||
const getCounts = () => {
|
||||
return Object.keys(this.credentialsStore.allUsableCredentialsByType).reduce(
|
||||
(counts: { [key: string]: number }, key: string) => {
|
||||
counts[key] = this.credentialsStore.allUsableCredentialsByType[key].length;
|
||||
return counts;
|
||||
},
|
||||
{},
|
||||
);
|
||||
};
|
||||
|
||||
let previousCredentialCounts = getCounts();
|
||||
const onCredentialMutation = () => {
|
||||
// This data pro stores credential type that the component is currently interested in
|
||||
const credentialType = this.subscribedToCredentialType;
|
||||
if (!credentialType) {
|
||||
return;
|
||||
}
|
||||
|
||||
let credentialsOfType = [
|
||||
...(this.credentialsStore.allUsableCredentialsByType[credentialType] || []),
|
||||
];
|
||||
// all credentials were deleted
|
||||
if (credentialsOfType.length === 0) {
|
||||
this.clearSelectedCredential(credentialType);
|
||||
return;
|
||||
}
|
||||
|
||||
credentialsOfType = credentialsOfType.sort((a, b) => (a.id < b.id ? -1 : 1));
|
||||
const previousCredsOfType = previousCredentialCounts[credentialType] || 0;
|
||||
const current = this.selected[credentialType];
|
||||
|
||||
// new credential was added
|
||||
if (credentialsOfType.length > previousCredsOfType || !current) {
|
||||
this.onCredentialSelected(
|
||||
credentialType,
|
||||
credentialsOfType[credentialsOfType.length - 1].id,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const matchingCredential = credentialsOfType.find((cred) => cred.id === current.id);
|
||||
// credential was deleted, select last one added to replace with
|
||||
if (!matchingCredential) {
|
||||
this.onCredentialSelected(
|
||||
credentialType,
|
||||
credentialsOfType[credentialsOfType.length - 1].id,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// credential was updated
|
||||
if (matchingCredential.name !== current.name) {
|
||||
// credential name was changed, update it
|
||||
this.onCredentialSelected(credentialType, current.id);
|
||||
}
|
||||
};
|
||||
|
||||
this.credentialsStore.$subscribe((mutation, state) => {
|
||||
onCredentialMutation();
|
||||
previousCredentialCounts = getCounts();
|
||||
});
|
||||
},
|
||||
|
||||
clearSelectedCredential(credentialType: string) {
|
||||
const node: INodeUi = this.node;
|
||||
@@ -310,12 +397,19 @@ export default mixins(genericHelpers, nodeHelpers, restApi, showMessage).extend(
|
||||
this.$emit('credentialSelected', updateInformation);
|
||||
},
|
||||
|
||||
onCredentialSelected(credentialType: string, credentialId: string | null | undefined) {
|
||||
onCredentialSelected(
|
||||
credentialType: string,
|
||||
credentialId: string | null | undefined,
|
||||
requiredCredentials = false,
|
||||
) {
|
||||
if (credentialId === this.NEW_CREDENTIALS_TEXT) {
|
||||
// If new credential dialog is open, start listening for auth type change which should happen in the modal
|
||||
// this will be handled in this component's watcher which will set subscribed credential accordingly
|
||||
this.listeningForAuthChange = true;
|
||||
this.subscribedToCredentialType = credentialType;
|
||||
}
|
||||
if (!credentialId || credentialId === this.NEW_CREDENTIALS_TEXT) {
|
||||
this.uiStore.openNewCredential(credentialType);
|
||||
this.uiStore.openNewCredential(credentialType, requiredCredentials);
|
||||
this.$telemetry.track('User opened Credential modal', {
|
||||
credential_type: credentialType,
|
||||
source: 'node',
|
||||
@@ -334,9 +428,10 @@ export default mixins(genericHelpers, nodeHelpers, restApi, showMessage).extend(
|
||||
});
|
||||
|
||||
const selectedCredentials = this.credentialsStore.getCredentialById(credentialId);
|
||||
const selectedCredentialsType = this.showAll ? selectedCredentials.type : credentialType;
|
||||
const oldCredentials =
|
||||
this.node.credentials && this.node.credentials[credentialType]
|
||||
? this.node.credentials[credentialType]
|
||||
this.node.credentials && this.node.credentials[selectedCredentialsType]
|
||||
? this.node.credentials[selectedCredentialsType]
|
||||
: {};
|
||||
|
||||
const selected = { id: selectedCredentials.id, name: selectedCredentials.name };
|
||||
@@ -345,13 +440,16 @@ export default mixins(genericHelpers, nodeHelpers, restApi, showMessage).extend(
|
||||
if (
|
||||
oldCredentials.id === null ||
|
||||
(oldCredentials.id &&
|
||||
!this.credentialsStore.getCredentialByIdAndType(oldCredentials.id, credentialType))
|
||||
!this.credentialsStore.getCredentialByIdAndType(
|
||||
oldCredentials.id,
|
||||
selectedCredentialsType,
|
||||
))
|
||||
) {
|
||||
// update all nodes in the workflow with the same old/invalid credentials
|
||||
this.workflowsStore.replaceInvalidWorkflowCredentials({
|
||||
credentials: selected,
|
||||
invalid: oldCredentials,
|
||||
type: credentialType,
|
||||
type: selectedCredentialsType,
|
||||
});
|
||||
this.updateNodesCredentialsIssues();
|
||||
this.$showMessage({
|
||||
@@ -366,11 +464,27 @@ export default mixins(genericHelpers, nodeHelpers, restApi, showMessage).extend(
|
||||
});
|
||||
}
|
||||
|
||||
// If credential is selected from mixed credential dropdown, update node's auth filed based on selected credential
|
||||
if (this.showAll && this.mainNodeAuthField) {
|
||||
const nodeCredentialDescription = this.nodeType?.credentials?.find(
|
||||
(cred) => cred.name === selectedCredentialsType,
|
||||
);
|
||||
const authOption = getAuthTypeForNodeCredential(this.nodeType, nodeCredentialDescription);
|
||||
if (authOption) {
|
||||
updateNodeAuthType(this.node, authOption.value);
|
||||
const parameterData = {
|
||||
name: `parameters.${this.mainNodeAuthField.name}`,
|
||||
value: authOption.value,
|
||||
};
|
||||
this.$emit('valueChanged', parameterData);
|
||||
}
|
||||
}
|
||||
|
||||
const node: INodeUi = this.node;
|
||||
|
||||
const credentials = {
|
||||
...(node.credentials || {}),
|
||||
[credentialType]: selected,
|
||||
[selectedCredentialsType]: selected,
|
||||
};
|
||||
|
||||
const updateInformation: INodeUpdatePropertiesInformation = {
|
||||
@@ -401,7 +515,6 @@ export default mixins(genericHelpers, nodeHelpers, restApi, showMessage).extend(
|
||||
if (!node.issues.credentials.hasOwnProperty(credentialTypeName)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return node.issues.credentials[credentialTypeName];
|
||||
},
|
||||
|
||||
@@ -414,7 +527,7 @@ export default mixins(genericHelpers, nodeHelpers, restApi, showMessage).extend(
|
||||
return false;
|
||||
}
|
||||
const { id } = this.node.credentials[credentialType];
|
||||
const options = this.getCredentialOptions(credentialType);
|
||||
const options = this.getCredentialOptions([credentialType]);
|
||||
|
||||
return !!options.find((option: ICredentialsResponse) => option.id === id);
|
||||
},
|
||||
@@ -431,6 +544,24 @@ export default mixins(genericHelpers, nodeHelpers, restApi, showMessage).extend(
|
||||
});
|
||||
this.subscribedToCredentialType = credentialType;
|
||||
},
|
||||
showMixedCredentials(credentialType: INodeCredentialDescription): boolean {
|
||||
const nodeType = this.nodeTypesStore.getNodeType(this.node.type, this.node.typeVersion);
|
||||
const isRequired = isRequiredCredential(nodeType, credentialType);
|
||||
|
||||
return !KEEP_AUTH_IN_NDV_FOR_NODES.includes(this.node.type || '') && isRequired;
|
||||
},
|
||||
getCredentialsFieldLabel(credentialType: INodeCredentialDescription): string {
|
||||
const credentialTypeName = this.credentialTypeNames[credentialType.name];
|
||||
|
||||
if (!this.showMixedCredentials(credentialType)) {
|
||||
return this.$locale.baseText('nodeCredentials.credentialFor', {
|
||||
interpolate: {
|
||||
credentialType: credentialTypeName,
|
||||
},
|
||||
});
|
||||
}
|
||||
return this.$locale.baseText('nodeCredentials.credentialsLabel');
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@@ -470,4 +601,9 @@ export default mixins(genericHelpers, nodeHelpers, restApi, showMessage).extend(
|
||||
composes: input;
|
||||
--input-border-color: var(--color-danger);
|
||||
}
|
||||
|
||||
.credentialOption {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user