Skip to main content

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:

Example config pages definition
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.

A debug config variable that is only visible to org team members
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:

Example connection definition
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.

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

Accessing connection inputs in a trigger or onExecution function
// 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:

Example data source definition
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.

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:

Example config variable definition
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",
}),
}),
};
A page in the config wizard with two static string inputs

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:

Include helper text in the config wizard
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: {
// ...
},
}),
},
}),
};
A page in the config wizard with additional helper text

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:

User-level config wizard
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:

Including user-level config in your component
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.