feat(editor): Select credentials in template setup if theres only one (#7879)

Automatically select credentials in the template credential setup if
there's only one available.

NOTE! This feature is still behind a feature flag. To test, set:

```js
localStorage.setItem('template-credentials-setup', 'true')
```


https://github.com/n8n-io/n8n/assets/10324676/edc1f586-214f-4c37-b7ec-dd1786433dc1
This commit is contained in:
Tomi Turtiainen 2023-11-30 15:46:14 +02:00 committed by GitHub
parent e0b7f89035
commit fe3417a615
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 466 additions and 0 deletions

View file

@ -817,10 +817,20 @@ export interface ITemplatesWorkflow {
};
}
export interface ITemplatesWorkflowInfo {
nodeCount: number;
nodeTypes: {
[key: string]: {
count: number;
};
};
}
export interface ITemplatesWorkflowResponse extends ITemplatesWorkflow, IWorkflowTemplate {
description: string | null;
image: ITemplatesImage[];
categories: ITemplatesCategory[];
workflowInfo: ITemplatesWorkflowInfo;
}
/**
@ -1010,6 +1020,7 @@ export interface IVersionNode {
}
export interface ITemplatesNode extends IVersionNode {
id: number;
categories?: ITemplatesCategory[];
}

View file

@ -1,10 +1,16 @@
import { useTemplatesStore } from '@/stores/templates.store';
import type { IWorkflowTemplateNodeWithCredentials } from '@/utils/templates/templateTransforms';
import type { CredentialUsages } from '@/views/SetupWorkflowFromTemplateView/setupTemplate.store';
import {
getAppCredentials,
getAppsRequiringCredentials,
groupNodeCredentialsByName,
useSetupTemplateStore,
} from '@/views/SetupWorkflowFromTemplateView/setupTemplate.store';
import { setActivePinia } from 'pinia';
import * as testData from './setupTemplate.store.testData';
import { createTestingPinia } from '@pinia/testing';
import { useCredentialsStore } from '@/stores/credentials.store';
const objToMap = <T>(obj: Record<string, T>) => {
return new Map<string, T>(Object.entries(obj));
@ -145,4 +151,111 @@ describe('SetupWorkflowFromTemplateView store', () => {
]);
});
});
describe('setInitialCredentialsSelection', () => {
beforeEach(() => {
setActivePinia(
createTestingPinia({
stubActions: false,
}),
);
});
it("selects no credentials when there isn't any available", () => {
// Setup
const credentialsStore = useCredentialsStore();
credentialsStore.setCredentialTypes([testData.credentialTypeTelegram]);
const templatesStore = useTemplatesStore();
templatesStore.addWorkflows([testData.fullShopifyTelegramTwitterTemplate]);
const setupTemplateStore = useSetupTemplateStore();
setupTemplateStore.setTemplateId(testData.fullShopifyTelegramTwitterTemplate.id.toString());
// Execute
setupTemplateStore.setInitialCredentialSelection();
expect(setupTemplateStore.selectedCredentialIdByName).toEqual({});
});
it("selects credential when there's only one", () => {
// Setup
const credentialsStore = useCredentialsStore();
credentialsStore.setCredentialTypes([testData.credentialTypeTelegram]);
credentialsStore.setCredentials([testData.credentialsTelegram1]);
const templatesStore = useTemplatesStore();
templatesStore.addWorkflows([testData.fullShopifyTelegramTwitterTemplate]);
const setupTemplateStore = useSetupTemplateStore();
setupTemplateStore.setTemplateId(testData.fullShopifyTelegramTwitterTemplate.id.toString());
// Execute
setupTemplateStore.setInitialCredentialSelection();
expect(setupTemplateStore.selectedCredentialIdByName).toEqual({
telegram_habot: 'YaSKdvEcT1TSFrrr1',
});
});
it('selects no credentials when there are more than 1 available', () => {
// Setup
const credentialsStore = useCredentialsStore();
credentialsStore.setCredentialTypes([testData.credentialTypeTelegram]);
credentialsStore.setCredentials([
testData.credentialsTelegram1,
testData.credentialsTelegram2,
]);
const templatesStore = useTemplatesStore();
templatesStore.addWorkflows([testData.fullShopifyTelegramTwitterTemplate]);
const setupTemplateStore = useSetupTemplateStore();
setupTemplateStore.setTemplateId(testData.fullShopifyTelegramTwitterTemplate.id.toString());
// Execute
setupTemplateStore.setInitialCredentialSelection();
expect(setupTemplateStore.selectedCredentialIdByName).toEqual({});
});
test.each([
['httpBasicAuth'],
['httpCustomAuth'],
['httpDigestAuth'],
['httpHeaderAuth'],
['oAuth1Api'],
['oAuth2Api'],
['httpQueryAuth'],
])('does not auto-select credentials for %s', (credentialType) => {
// Setup
const credentialsStore = useCredentialsStore();
credentialsStore.setCredentialTypes([testData.newCredentialType(credentialType)]);
credentialsStore.setCredentials([
testData.newCredential({
name: `${credentialType}Credential`,
type: credentialType,
}),
]);
const templatesStore = useTemplatesStore();
const workflow = testData.newFullOneNodeTemplate({
name: 'Test',
type: 'n8n-nodes-base.httpRequest',
typeVersion: 1,
credentials: {
[credentialType]: 'Test',
},
parameters: {},
position: [250, 300],
});
templatesStore.addWorkflows([workflow]);
const setupTemplateStore = useSetupTemplateStore();
setupTemplateStore.setTemplateId(workflow.id.toString());
// Execute
setupTemplateStore.setInitialCredentialSelection();
expect(setupTemplateStore.credentialUsages.length).toBe(1);
expect(setupTemplateStore.selectedCredentialIdByName).toEqual({});
});
});
});

View file

@ -0,0 +1,309 @@
import { faker } from '@faker-js/faker/locale/en';
import type {
ICredentialsResponse,
ITemplatesWorkflowFull,
IWorkflowTemplateNode,
} from '@/Interface';
import type { ICredentialType } from 'n8n-workflow';
export const newFullOneNodeTemplate = (node: IWorkflowTemplateNode): ITemplatesWorkflowFull => ({
full: true,
id: faker.number.int(),
name: faker.commerce.productName(),
totalViews: 1,
createdAt: '2021-08-24T10:40:50.007Z',
description: faker.lorem.paragraph(),
workflow: {
nodes: [node],
connections: {},
},
nodes: [
{
defaults: {},
displayName: faker.commerce.productName(),
icon: 'file:telegram.svg',
iconData: {
fileBuffer: '',
type: 'file',
},
id: faker.number.int(),
name: node.type,
},
],
image: [],
categories: [],
user: {
username: faker.internet.userName(),
},
workflowInfo: {
nodeCount: 1,
nodeTypes: {
[node.type]: {
count: 1,
},
},
},
});
export const fullShopifyTelegramTwitterTemplate: ITemplatesWorkflowFull = {
full: true,
id: 1205,
name: 'Promote new Shopify products on Twitter and Telegram',
totalViews: 485,
createdAt: '2021-08-24T10:40:50.007Z',
description:
'This workflow automatically promotes your new Shopify products on Twitter and Telegram. This workflow is also featured in the blog post [*6 e-commerce workflows to power up your Shopify store*](https://n8n.io/blog/no-code-ecommerce-workflow-automations/#promote-your-new-products-on-social-media).\n\n## Prerequisites\n\n- A Shopify account and [credentials](https://docs.n8n.io/integrations/credentials/shopify/)\n- A Twitter account and [credentials](https://docs.n8n.io/integrations/credentials/twitter/)\n- A Telegram account and [credentials](https://docs.n8n.io/integrations/credentials/telegram/) for the channel you want to send messages to.\n\n## Nodes\n\n- [Shopify Trigger node](https://docs.n8n.io/integrations/trigger-nodes/n8n-nodes-base.shopifytrigger/) triggers the workflow when you create a new product in Shopify.\n- [Twitter node](https://docs.n8n.io/integrations/nodes/n8n-nodes-base.twitter/) posts a tweet with the text "Hey there, my design is now on a new product! Visit my {shop name} to get this cool {product title} (and check out more {product type})".\n- [Telegram node](https://docs.n8n.io/integrations/nodes/n8n-nodes-base.telegram/) posts a message with the same text as above in a Telegram channel.',
workflow: {
nodes: [
{
name: 'Twitter',
type: 'n8n-nodes-base.twitter',
position: [720, -220],
parameters: {
text: '=Hey there, my design is now on a new product ✨\nVisit my {{$json["vendor"]}} shop to get this cool{{$json["title"]}} (and check out more {{$json["product_type"]}}) 🛍️',
additionalFields: {},
},
credentials: {
twitterOAuth1Api: 'twitter',
},
typeVersion: 1,
},
{
name: 'Telegram',
type: 'n8n-nodes-base.telegram',
position: [720, -20],
parameters: {
text: '=Hey there, my design is now on a new product!\nVisit my {{$json["vendor"]}} shop to get this cool{{$json["title"]}} (and check out more {{$json["product_type"]}})',
chatId: '123456',
additionalFields: {},
},
credentials: {
telegramApi: 'telegram_habot',
},
typeVersion: 1,
},
{
name: 'product created',
type: 'n8n-nodes-base.shopifyTrigger',
position: [540, -110],
webhookId: '2a7e0e50-8f09-4a2b-bf54-a849a6ac4fe0',
parameters: {
topic: 'products/create',
},
credentials: {
shopifyApi: 'shopify_nodeqa',
},
typeVersion: 1,
},
],
connections: {
'product created': {
main: [
[
{
node: 'Twitter',
type: 'main',
index: 0,
},
{
node: 'Telegram',
type: 'main',
index: 0,
},
],
],
},
},
},
workflowInfo: {
nodeCount: 3,
nodeTypes: {
'n8n-nodes-base.twitter': {
count: 1,
},
'n8n-nodes-base.telegram': {
count: 1,
},
'n8n-nodes-base.shopifyTrigger': {
count: 1,
},
},
},
user: {
username: 'lorenanda',
},
nodes: [
{
id: 49,
icon: 'file:telegram.svg',
name: 'n8n-nodes-base.telegram',
defaults: {
name: 'Telegram',
},
iconData: {
type: 'file',
fileBuffer:
'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2aWV3Qm94PSIwIDAgNjYgNjYiIGZpbGw9IiNmZmYiIGZpbGwtcnVsZT0iZXZlbm9kZCIgc3Ryb2tlPSIjMDAwIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjx1c2UgeGxpbms6aHJlZj0iI2EiIHg9Ii41IiB5PSIuNSIvPjxzeW1ib2wgaWQ9ImEiIG92ZXJmbG93PSJ2aXNpYmxlIj48ZyBzdHJva2U9Im5vbmUiIGZpbGwtcnVsZT0ibm9uemVybyI+PHBhdGggZD0iTTAgMzJjMCAxNy42NzMgMTQuMzI3IDMyIDMyIDMyczMyLTE0LjMyNyAzMi0zMlM0OS42NzMgMCAzMiAwIDAgMTQuMzI3IDAgMzIiIGZpbGw9IiMzN2FlZTIiLz48cGF0aCBkPSJNMjEuNjYxIDM0LjMzOGwzLjc5NyAxMC41MDhzLjQ3NS45ODMuOTgzLjk4MyA4LjA2OC03Ljg2NCA4LjA2OC03Ljg2NGw4LjQwNy0xNi4yMzctMjEuMTE5IDkuODk4eiIgZmlsbD0iI2M4ZGFlYSIvPjxwYXRoIGQ9Ik0yNi42OTUgMzcuMDM0bC0uNzI5IDcuNzQ2cy0uMzA1IDIuMzczIDIuMDY4IDBsNC42NDQtNC4yMDMiIGZpbGw9IiNhOWM2ZDgiLz48cGF0aCBkPSJNMjEuNzMgMzQuNzEybC03LjgwOS0yLjU0NXMtLjkzMi0uMzc4LS42MzMtMS4yMzdjLjA2Mi0uMTc3LjE4Ni0uMzI4LjU1OS0uNTg4IDEuNzMxLTEuMjA2IDMyLjAyOC0xMi4wOTYgMzIuMDI4LTEyLjA5NnMuODU2LS4yODggMS4zNjEtLjA5N2MuMjMxLjA4OC4zNzguMTg3LjUwMy41NDguMDQ1LjEzMi4wNzEuNDExLjA2OC42ODktLjAwMy4yMDEtLjAyNy4zODYtLjA0NS42NzgtLjE4NCAyLjk3OC01LjcwNiAyNS4xOTgtNS43MDYgMjUuMTk4cy0uMzMgMS4zLTEuNTE0IDEuMzQ1Yy0uNDMyLjAxNi0uOTU2LS4wNzEtMS41ODItLjYxLTIuMzIzLTEuOTk4LTEwLjM1Mi03LjM5NC0xMi4xMjYtOC41OGEuMzQuMzQgMCAwMS0uMTQ2LS4yMzljLS4wMjUtLjEyNS4xMDgtLjI4LjEwOC0uMjhzMTMuOTgtMTIuNDI3IDE0LjM1Mi0xMy43MzFjLjAyOS0uMTAxLS4wNzktLjE1MS0uMjI2LS4xMDctLjkyOS4zNDItMTcuMDI1IDEwLjUwNi0xOC44MDEgMTEuNjI5LS4xMDQuMDY2LS4zOTUuMDIzLS4zOTUuMDIzIi8+PC9nPjwvc3ltYm9sPjwvc3ZnPg==',
},
categories: [
{
id: 6,
name: 'Communication',
},
],
displayName: 'Telegram',
typeVersion: 1,
},
{
id: 107,
icon: 'file:shopify.svg',
name: 'n8n-nodes-base.shopifyTrigger',
defaults: {
name: 'Shopify Trigger',
},
iconData: {
type: 'file',
fileBuffer:
'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2aWV3Qm94PSIwIDAgNTggNjYiIGZpbGw9IiNmZmYiIGZpbGwtcnVsZT0iZXZlbm9kZCIgc3Ryb2tlPSIjMDAwIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjx1c2UgeGxpbms6aHJlZj0iI2EiIHg9Ii41IiB5PSIuNSIvPjxzeW1ib2wgaWQ9ImEiIG92ZXJmbG93PSJ2aXNpYmxlIj48ZyBzdHJva2U9Im5vbmUiIGZpbGwtcnVsZT0ibm9uemVybyI+PHBhdGggZD0iTTQ5LjI1NSAxMi40ODRhLjYzMy42MzMgMCAwMC0uNTY0LS41MjdjLS4yMjUtLjAzNy01LjE3LS4zNzYtNS4xNy0uMzc2bC0zLjc3LTMuNzdjLS4zNC0uMzc2LTEuMDkyLS4yNjYtMS4zNzYtLjE4OC0uMDM3IDAtLjc1Mi4yMjUtMS45MjIuNjA1LTEuMTM3LTMuMy0zLjE1LTYuMzA2LTYuNjk2LTYuMzA2aC0uMzAzQzI4LjQzOC42MDUgMjcuMTk0IDAgMjYuMTQ0IDBjLTguMjU2LjAzNy0xMi4yIDEwLjMzMy0xMy40MzQgMTUuNTk0bC01Ljc3IDEuNzdjLTEuNzcuNTY0LTEuODM1LjYwNS0yLjA3MyAyLjI5M0wwIDU3LjE3NSAzNi40NjggNjRsMTkuNzYzLTQuMjZjMC0uMDM3LTYuOTQtNDYuODk3LTYuOTc2LTQ3LjI1NXpNMzQuNDMxIDguODZjLS45MTcuMzAzLTEuOTYzLjYwNS0zLjEuOTQ1di0uNjhhMTUuMDMgMTUuMDMgMCAwMC0uNzUyLTQuOTk5YzEuODQ4LjI4NCAzLjEgMi4zNTcgMy44NDMgNC43MzN6bS02LjA2OC00LjI5OGMuNjAzIDEuNzc4Ljg4MyAzLjY1LjgyNiA1LjUyN3YuMzRsLTYuMzc1IDEuOTYzYzEuMjQ4LTQuNjYgMy41NS02Ljk2MiA1LjU1LTcuODN6bS0yLjQ1LTIuMjkzYTEuOTQgMS45NCAwIDAxMS4wNTUuMzM5Yy0yLjY2IDEuMjM4LTUuNDcyIDQuMzY2LTYuNjc4IDEwLjYyN2wtNS4wNDUgMS41NDZDMTYuNjY4IDEwLjAzIDE5Ljk4OCAyLjI2IDI1LjkxIDIuMjZ6IiBmaWxsPSIjOTViZjQ3Ii8+PHBhdGggZD0iTTQ4LjY5MSAxMS45NTdjLS4yMjUtLjAzNy01LjE3LS4zNzYtNS4xNy0uMzc2bC0zLjc3LTMuNzdhLjc1My43NTMgMCAwMC0uNTI3LS4yMjVMMzYuNDcyIDY0bDE5Ljc2My00LjI2LTYuOTgtNDcuMjE4YS42OC42OCAwIDAwLS41NjQtLjU2NHoiIGZpbGw9IiM1ZThlM2UiLz48cGF0aCBkPSJNMjkuNzU4IDIyLjlsLTIuNDU0IDcuMjQyYTExLjM2IDExLjM2IDAgMDAtNC43NTItMS4xMzNjLTMuODQ4IDAtNC4wMzYgMi40MTItNC4wMzYgMy4wMTggMCAzLjI5OCA4LjYzNiA0LjU2NCA4LjYzNiAxMi4zMzMgMCA2LjEtMy44ODUgMTAuMDMtOS4xIDEwLjAzLTYuMjYgMC05LjQ2Ny0zLjg4NS05LjQ2Ny0zLjg4NWwxLjY2NS01LjU1czMuMjggMi44MyA2LjA3MyAyLjgzYTIuNDcgMi40NyAwIDAwMi41NjQtMi40OWMwLTQuMzQtNy4xLTQuNTI3LTcuMS0xMS42MTggMC01Ljk2MiA0LjI5OC0xMS43NyAxMi45MzQtMTEuNzcgMy4zOTQuMDUgNS4wMTggMSA1LjAxOCAxeiIvPjwvZz48L3N5bWJvbD48L3N2Zz4=',
},
categories: [
{
id: 2,
name: 'Sales',
},
],
displayName: 'Shopify Trigger',
typeVersion: 1,
},
{
id: 325,
icon: 'file:x.svg',
name: 'n8n-nodes-base.twitter',
defaults: {
name: 'X',
},
iconData: {
type: 'file',
fileBuffer:
'data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgMjQgMjQiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTE4LjI0NCAyLjI1aDMuMzA4bC03LjIyNyA4LjI2IDguNTAyIDExLjI0SDE2LjE3bC01LjIxNC02LjgxN0w0Ljk5IDIxLjc1SDEuNjhsNy43My04LjgzNUwxLjI1NCAyLjI1SDguMDhsNC43MTMgNi4yMzF6bS0xLjE2MSAxNy41MmgxLjgzM0w3LjA4NCA0LjEyNkg1LjExN3oiPjwvcGF0aD48L3N2Zz4K',
},
categories: [
{
id: 1,
name: 'Marketing & Content',
},
],
displayName: 'X (Formerly Twitter)',
typeVersion: 2,
},
],
categories: [
{
id: 2,
name: 'Sales',
},
{
id: 19,
name: 'Marketing & Growth',
},
],
image: [
{
id: 527,
url: 'https://n8niostorageaccount.blob.core.windows.net/n8nio-strapi-blobs-prod/assets/89a078b208fe4c6181902608b1cd1332.png',
},
],
};
export const newCredentialType = (name: string): ICredentialType => ({
name,
displayName: name,
documentationUrl: name,
properties: [],
});
export const newCredential = (
opts: Pick<ICredentialsResponse, 'type'> & Partial<ICredentialsResponse>,
): ICredentialsResponse => ({
createdAt: faker.date.past().toISOString(),
updatedAt: faker.date.past().toISOString(),
id: faker.string.alphanumeric({ length: 16 }),
name: faker.commerce.productName(),
nodesAccess: [],
...opts,
});
export const credentialTypeTelegram: ICredentialType = {
name: 'telegramApi',
displayName: 'Telegram API',
documentationUrl: 'telegram',
properties: [
{
displayName: 'Access Token',
name: 'accessToken',
type: 'string',
typeOptions: {
password: true,
},
default: '',
description:
'Chat with the <a href="https://telegram.me/botfather">bot father</a> to obtain the access token',
},
],
test: {
request: {
baseURL: '=https://api.telegram.org/bot{{$credentials.accessToken}}',
url: '/getMe',
},
},
};
export const credentialsTelegram1: ICredentialsResponse = {
createdAt: '2023-11-23T14:26:07.969Z',
updatedAt: '2023-11-23T14:26:07.964Z',
id: 'YaSKdvEcT1TSFrrr1',
name: 'Telegram account',
type: 'telegramApi',
nodesAccess: [
{
nodeType: 'n8n-nodes-base.telegram',
date: new Date('2023-11-23T14:26:07.962Z'),
},
{
nodeType: 'n8n-nodes-base.telegramTrigger',
date: new Date('2023-11-23T14:26:07.962Z'),
},
],
ownedBy: {
id: '713ef3e7-9e65-4b0a-893c-8a653cbb2c4f',
email: 'user@n8n.io',
firstName: 'Player',
lastName: 'One',
},
sharedWith: [],
};
export const credentialsTelegram2: ICredentialsResponse = {
createdAt: '2023-11-23T14:26:07.969Z',
updatedAt: '2023-11-23T14:26:07.964Z',
id: 'YaSKdvEcT1TSFrrr2',
name: 'Telegram account',
type: 'telegramApi',
nodesAccess: [
{
nodeType: 'n8n-nodes-base.telegram',
date: new Date('2023-11-23T14:26:07.962Z'),
},
{
nodeType: 'n8n-nodes-base.telegramTrigger',
date: new Date('2023-11-23T14:26:07.962Z'),
},
],
ownedBy: {
id: '713ef3e7-9e65-4b0a-893c-8a653cbb2c4f',
email: 'user@n8n.io',
firstName: 'Player',
lastName: 'One',
},
sharedWith: [],
};

View file

@ -232,6 +232,34 @@ export const useSetupTemplateStore = defineStore('setupTemplate', () => {
templateId.value = id;
};
const ignoredAutoFillCredentialTypes = new Set([
'httpBasicAuth',
'httpCustomAuth',
'httpDigestAuth',
'httpHeaderAuth',
'oAuth1Api',
'oAuth2Api',
'httpQueryAuth',
]);
/**
* Selects initial credentials for the template. Credentials
* need to be loaded before this.
*/
const setInitialCredentialSelection = () => {
for (const credUsage of credentialUsages.value) {
if (ignoredAutoFillCredentialTypes.has(credUsage.credentialType)) {
continue;
}
const availableCreds = credentialsStore.getCredentialsByType(credUsage.credentialType);
if (availableCreds.length === 1) {
selectedCredentialIdByName.value[credUsage.credentialName] = availableCreds[0].id;
}
}
};
/**
* Loads the template if it hasn't been loaded yet.
*/
@ -241,6 +269,8 @@ export const useSetupTemplateStore = defineStore('setupTemplate', () => {
}
await templatesStore.fetchTemplateById(templateId.value);
setInitialCredentialSelection();
};
/**
@ -257,6 +287,8 @@ export const useSetupTemplateStore = defineStore('setupTemplate', () => {
nodeTypesStore.loadNodeTypesIfNotLoaded(),
loadTemplateIfNeeded(),
]);
setInitialCredentialSelection();
} finally {
isLoading.value = false;
}
@ -341,6 +373,7 @@ export const useSetupTemplateStore = defineStore('setupTemplate', () => {
skipSetup,
init,
loadTemplateIfNeeded,
setInitialCredentialSelection,
setTemplateId,
setSelectedCredentialId,
unsetSelectedCredential,