Collecting User Configuration

When the user installs an Integration, a series of steps take place to collect the necessary authorization information to allow the Integration to connect to any third-party systems it needs. Optionally, the developer can add steps to that flow to collect any additional parameters the Integration needs from the user.

700700

For example, the user may need to specify the project they are interested to work with in the third-party system, or they may need to select which fields of a given object they are interested in. This configuration information gets stored in that user's Install and is available to the integration code at runtime.

This guide walks you through the process of adding a configuration screen to your Integration's install flow.

Designing the configuration screen

Fusebit's configuration screens are based on the commonly-used JSONForms library, please refer to it's documentation as necessary.

The first step is to define the screen's UI Schema. Create a new file in your Integration and name it uiSchema.json. This example defines a single control with it's properties bound to team-name.

{
  "type": "VerticalLayout",
  "elements": [
    {
      "type": "HorizontalLayout",
      "elements": [
        {
          "type": "Control",
          "scope": "#/properties/team-name"
        }
      ]
    }
  ]
}

Create a separate JSON Schema file named schema.json, where you can define the data backing the control:

{
  "type": "object",
  "properties": {
    "team-name": {
      "type": "string",
      "enum": ["Engineering", "Growth"]
    }
  }
}

Adding an endpoint in the Integration

In the integration.js file, add the following two endpoints, which will render the form and collect the data submitted by the user.

const schema = require('./schema.json');
const uiSchema = require('./uiSchema.json');

router.get('/api/form', async (ctx) => {  
  const [form, contentType] = integration.response.createJsonForm({
    schema,
    uiSchema,
    dialogTitle: 'Choose Your Team',
    submitUrl: 'form/submitted',
    state: {
      session: ctx.query.session,
    },
  });
  ctx.body = form;
  ctx.header['Content-Type'] = contentType;
});

router.post('/api/form/submitted', async (ctx) => {
  const pl = JSON.parse(ctx.req.body.payload);
  await superagent
    .put(`${ctx.state.params.baseUrl}/session/${pl.state.session}`)
    .set('Authorization', `Bearer ${ctx.state.params.functionAccessToken}`)
    .send({ output: pl.payload });
  return ctx.redirect(`${ctx.state.params.baseUrl}/session/${pl.state.session}/callback`);
});

In the components section of fusebit.json add the following section for the form. Set any Connectors as dependencies using the dependsOn property.

{
  "components": [
    {
      "name": "linearConnector"
      // Removed for brevity
    },
    {
      "name": "form",
      "path": "/api/form",
      "skip": false,
      "entityId": "{ this integration name }",
      "dependsOn": ["linearConnector"],
      "entityType": "integration"
    }
  ]
  // Removed for brevity
}

👍

Check that the entityId is correct

Make sure that the entityId in the form component is the entityId of the Integration you are adding it to, and not the Connector that is also present.

Accessing the configuration value

After the Integration is installed, you can access any configuration values as shown below:

router.post('/api/tenant/:tenantId/test', integration.middleware.authorizeUser('install:get'), async (ctx) => {
  const installs = await integration.tenant.getTenantInstalls(ctx, ctx.params.tenantId)

  if (installs.length === 0) {
    ctx.throw(404, `Cannot find an Integration Install associated with tenant ${ctx.params.tenantId}`);
  }

  if (installs.length > 1) {
    ctx.throw(400, `Too many Integration Installs found with tenant ${ctx.params.tenantId}`);
  }

  const teamName = installs[0].data.form['team-name'];

  // Rest of the integration logic, using the teamName config value
  
});

Supplying custom defaults

Many configuration options will have sensible default values which can be supplied prior to the configuration of the form. Sometimes these values are unique defaults for a specific Tenant's configuration form, and other times they are secrets used by a Connector - such as the Client Credential Flow Connector.

Sessions support inputs on a per-component basis supplied as part of the initial POST to the session endpoint. Suppose your integration has these components:

{
  "components": [
    {
      "name": "clientCredentialFlowConnector"
      // Removed for brevity
    },
    {
      "name": "form",
      "path": "/api/form",
      "skip": false,
      "entityId": "{ this integration name }",
      "dependsOn": ["linearConnector"],
      "entityType": "integration"
    }
  ]
  // Removed for brevity
}

When creating a session, default input's could be supplied to the session endpoint in this fashion:

{
  "redirectUrl": "http://example.com/redirect",
  "components": ["input", "form"],
  "input": {
    "clientCredentialFlowConnector": {
      "client_id": "AAAA",
      "client_secret": "BBBB"
    },
    "form": {
      "option": "A Sensible Default"
    }
  }
}

Note that the above example also demonstrates the components parameter in the session object, which allows for a session to specify an explicit sequence or smaller sample of the available components on an integration.

A form in an integration would then be able to access that value by looking in the input section of the session object:

router.get('/api/form', async (ctx) => {
  // Retrieve the current session
  const session = superagent.get(`${ctx.state.params.baseUrl}/session/${ctx.query.session}`)
    .set('Authorization', `Bearer ${ctx.state.params.functionAccessToken}`);
  
  const [form, contentType] = integration.response.createJsonForm({
    ...
    state: {
      option: session.body.input.option,
    },
        ...
  });
});

Each component gets a unique session, with the input for that component isolated from all of the other components.

Using data from previous components

If a form or other element needs access to parameters specified in a previous step - for example, to query for an endpoint and then use that endpoint in a subsequent authorization step - the component in the integration configuration can specify a dependsOn parameter:

{
  "components": [
    {
      "name": "conn",
      "entityType": "connector",
      "entityId": "my-connector",
      "provider": "@fusebit-int/oauth-provider"
    },
    {
      "name": "form",
      "entityType": "integration",
      "entityId": "my-integration",
      "dependsOn": [
        "conn"
      ],
      "path": "/api/my-form"
    }
  ]
}

This parameter allows the form component, during the execution of the /api/my-form endpoint, to have access to the session id of the related session:

{
  "id": "sid-12345",
  "dependsOn": {
    "conn": {
      "parentEntityType": "connector",
      "parentEntityId": "my-connector",
      "entityId": "sid-9876"
    }
  }
}

The entityId can be used to retrieve an access token from the connector's /api/session/{entityId}/token endpoint, allowing the form to have access to the customer account. This is often used to populate a list of servers, channels, or other artifacts.

Now, with the dependsOn configured, to access 3rd party SDKs during the install process:

router.get('/api/form', async (ctx) => {  
  const linearSdk = await integration.service.getSdk(ctx, 'linearConnector', ctx.query.session);
  const teams = await linearSdk.teams();
  let teamNames = [];
  teams.nodes.forEach((team) => teamNames.push(team.name));
  schema.properties['team-name'].enum = teamNames;
  const [form, contentType] = integration.response.createJsonForm({
    schema,
    uiSchema,
    dialogTitle: 'Choose Your Team',
    submitUrl: 'form/submitted',
    state: {
      session: ctx.query.session,
    },
  });
  ctx.body = form;
  ctx.header['Content-Type'] = contentType;
});
{
  "type": "object",
  "properties": {
    "team-name": {
      "type": "string",
      "enum": []
    }
  }
}

This uses the Linear SDK to pull teams from Linear to dynamically populate the enums within team-name instead of hard coding it as shown previously.


Did this page help you?