Code-Native Config Wizard
Just like low-code integrations, code-native integrations include a config wizard. The config wizard can include things like OAuth 2.0 connections, API key connections, dynamically-sourced UI elements (data sources), and other advanced configuration wizard steps.
A config wizard consists of multiple pages.
Each page has a title, which is derived from the key
of the configPage object, and a tagline
as well as a set of elements
(individual config variables).
For example, a config wizard might contain a page for a Slack OAuth 2.0 connection, a page where the user selects a channel from a dynamically-populated dropdown menu, and a page where a user enters two static string inputs:
import { configPage, configVar } from "@prismatic-io/spectral";
import { slackConnectionConfigVar } from "./connections";
import { slackSelectChannelDataSource } from "./dataSources";
export const configPages = {
Connections: configPage({
tagline: "Authenticate with Slack",
elements: {
"Slack OAuth Connection": slackConnectionConfigVar,
},
}),
"Slack Config": configPage({
tagline: "Select a Slack channel from a dropdown menu",
elements: {
"Select Slack Channel": slackSelectChannelDataSource,
},
}),
"Other Config": configPage({
elements: {
"Acme API Endpoint": configVar({
stableKey: "acme-api-endpoint",
dataType: "string",
description: "The endpoint to fetch TODO items from Acme",
defaultValue:
"https://my-json-server.typicode.com/prismatic-io/placeholder-data/todo",
}),
"Webhook Config Endpoint": configVar({
stableKey: "webhook-config-endpoint",
dataType: "string",
description:
"The endpoint to call when deploying or deleting an instance",
}),
},
}),
};
Config variable visibility in code-native
Each config variable can have a permissionAndVisibilityType
property with one of three values:
customer
is the default value. Customer users can view and edit the config variable, and it will always appear in the config wizard.embedded
makes it so the config variable does not show up in the config wizard, but your app is able to set it programmatically through the embedded SDK. This is helpful if you want to set an API key for a user during the configuration process, but not allow the user to see or edit the value that is set.organization
makes it so the config variable is not visible to your customer, and is not able to be set programmatically by your app. Config variables marked organization must have a default value, or else your team members will need to set the value on behalf of your customer.
Additionally, visibleToOrgDeployer
determines if an organization user will see this config variable in the config wizard UI.
While organization team members always have programmatic access to instances' config variables and their values, this helps to visually conceal some config variable values like generated metadata from data sources, etc.
Defaults to true
.
configVar({
stableKey: "debug",
dataType: "boolean",
description: "Enable debug logging",
defaultValue: "false",
permissionAndVisibilityType: "customer",
visibleToOrgDeployer: true,
});
Connections in code-native integrations
Connection definitions in CNI are very similar to custom component connection definitions, but you use the connectionConfigVar
function instead of the connection
function.
For example, a Slack OAuth 2.0 connection might look like this:
export const slackConnectionConfigVar = connectionConfigVar({
stableKey: "slack-oauth-connection",
dataType: "connection",
oauth2Type: OAuth2Type.AuthorizationCode,
inputs: {
authorizeUrl: {
label: "Authorize URL",
placeholder: "Authorize URL",
type: "string",
default: "https://slack.com/oauth/v2/authorize",
required: true,
shown: false,
comments: "The OAuth 2.0 Authorization URL for the API",
},
tokenUrl: {
label: "Token URL",
placeholder: "Token URL",
type: "string",
default: "https://slack.com/api/oauth.v2.access",
required: true,
shown: false,
comments: "The OAuth 2.0 Token URL for the API",
},
revokeUrl: {
label: "Revoke URL",
placeholder: "Revoke URL",
type: "string",
required: true,
shown: false,
comments: "The OAuth 2.0 Revocation URL for Slack",
default: "https://slack.com/api/auth.revoke",
},
scopes: {
label: "Scopes",
placeholder: "Scopes",
type: "string",
required: false,
shown: false,
comments: "Space separated OAuth 2.0 permission scopes for the API",
default: "chat:write chat:write.public channels:read",
},
clientId: {
label: "Client ID",
placeholder: "Client ID",
type: "string",
required: true,
shown: false,
comments: "Client Identifier of your app for the API",
default: SLACK_CLIENT_ID,
},
clientSecret: {
label: "Client Secret",
placeholder: "Client Secret",
type: "password",
required: true,
shown: false,
comments: "Client Secret of your app for the API",
default: SLACK_CLIENT_SECRET,
},
signingSecret: {
label: "Signing Secret",
type: "password",
required: true,
shown: false,
default: SLACK_SIGNING_SECRET,
},
},
});
The above OAuth 2.0 connection would be rendered in a config wizard as a card with a connect button that a user can click to authenticate with Slack.
Connection inputs yield objects.
To access one of the fields in the connection definition (like the signingSecret
input) in a trigger, or onExecution
function, you can access it using the configVars
object in the context
parameter.
OAuth
// Access a field defined in the connection definition
context.configVars["Slack OAuth Connection"].fields.signingSecret;
// Access the access token from the OAuth 2.0 flow
context.configVars["Slack OAuth Connection"].token?.access_token;
Data sources is code-native integrations
Data sources are defined in CNI similar to how they are defined in custom components, but you use the dataSourceConfigVar
function rather than the dataSource
function.
For example, if you would like to add a picklist dropdown menu to your config wizard that displays a list of Slack channels, you could define a data source like this:
import {
Connection,
Element,
dataSourceConfigVar,
} from "@prismatic-io/spectral";
import { createSlackClient } from "./slackClient";
import { AxiosResponse } from "axios";
interface Channel {
id: string;
name: string;
}
interface ListChannelsResponse {
ok: boolean;
channels: Channel[];
response_metadata?: {
next_cursor: string;
};
}
export const slackSelectChannelDataSource = dataSourceConfigVar({
stableKey: "slack-channel-selection",
dataSourceType: "picklist",
perform: async (context) => {
const client = createSlackClient(
context.configVars["Slack OAuth Connection"] as Connection,
);
let channels: Channel[] = [];
let cursor = null;
let counter = 1;
// Loop over pages of conversations, fetching up to 10,000 channels
// If we loop more than 10 times, we risk hitting Slack API limits,
// and returning over 10,000 channels can cause the UI to hang
do {
const response: AxiosResponse<ListChannelsResponse> = await client.get(
"conversations.list",
{
params: {
exclude_archived: true,
types: "public_channel",
cursor,
limit: 1000,
},
},
);
if (!response.data.ok) {
throw new Error(
`Error when fetching data from Slack: ${response.data}`,
);
}
channels = [...channels, ...response.data.channels];
cursor = response.data.response_metadata?.next_cursor;
counter += 1;
} while (cursor && counter < 10);
// Map conversations to key/label objects, sorted by name
const objects = channels
.sort((a, b) => (a.name < b.name ? -1 : 1))
.map<Element>((channel) => ({
key: channel.id,
label: channel.name,
}));
return { result: objects };
},
});
The above data source would be rendered in a config wizard as a picklist dropdown menu that displays a list of Slack channels.
You can build advanced UI elements, like field mappers, into your config wizard using JSON Forms data sources.
Other config variable types in code-native integrations
Other types of config variables that Prismatic supports (picklists, text inputs, schedules, etc) can be added to CNI integrations, as well.
These are generally defined inline alongside other elements
in a configPage
:
export const configPages = {
// ...
"Other Config": configPage({
"Acme API Endpoint": configVar({
stableKey: "1F886045-27E7-452B-9B44-776863F6A862",
dataType: "string",
description: "The endpoint to fetch TODO items from Acme",
defaultValue:
"https://my-json-server.typicode.com/prismatic-io/placeholder-data/todo",
}),
}),
};
Additional config wizard helper text in code-native integrations
In addition to config variables, you can add helpful text and images to guide your customers as they work through your config wizard.
To add HTML to the config wizard (which can include links, images, etc), include a string element
to a configPage
definition:
export const configPages = {
Connections: configPage({
elements: {
helpertext1: "<h2>Asana Instructions</h2>",
helpertext2:
"To generate an Asana API Key, visit the " +
'<a href="https://app.asana.com/0/my-apps" target="_blank">developer portal</a> ' +
'and select "Create new token".',
"Asana API Key": connectionConfigVar({
stableKey: "f0eab60f-545b-4b46-bebf-04d3aca6b63c",
dataType: "connection",
inputs: {
// ...
},
}),
},
}),
};
User-level config wizards in code-native integrations
If your integration relies on user-level config, you can add a user-level config wizard similar to how you create the integration's config wizard.
Within configPages.ts
create a userLevelConfigPages
object that has the same shape as configPages
:
export const userLevelConfigPages = {
Options: configPage({
elements: {
"My ULC Config Variables": configVar({
dataType: "string",
stableKey: "my-ulc-config-var",
description: "Enter a widget value",
}),
},
}),
};
Then, in index.ts
import the userLevelConfigPages
object.
Provide the object as an export of your project (so TypeScript can infer types via .spectral/index.ts
), and include it in your integration()
definition:
import { integration } from "@prismatic-io/spectral";
import flows from "./flows";
import { configPages, userLevelConfigPages } from "./configPages";
import { componentRegistry } from "./componentRegistry";
export { configPages, userLevelConfigPages } from "./configPages";
export { componentRegistry } from "./componentRegistry";
export default integration({
name: "ulc-example",
description: "My user-level config example integration",
iconPath: "icon.png",
flows,
configPages,
userLevelConfigPages,
componentRegistry,
});
Config variable stable keys
Config variables each have a user-supplied stableKey
property.
These keys are used to uniquely identify the config variable in the Prismatic API, and help guard against inadvertent changes to the name of the config variable.
Without a stable key, if a config variable's name can be changed the Prismatic API will treat it as a new config variable and existing values assigned to the config variable will be lost.
With a stable key, the Prismatic API will be able to map the old config variable to the renamed one, and retain config variable values.
Stable keys can be any user-supplied string. You can choose a random UUID, or a string that describes the flow or config variable.