Guides

Building Your Own UI

While Outpost offers a Tenant User Portal, you may want to build your own UI for users to manage their destinations and view their events.

The portal is built using the Outpost API with JWT authentication. You can leverage the same API to build your own UI.

Within this guide, we will use the User Portal as a reference implementation for a simple UI. You can find the full source code for the User Portal here.

In this guide, we will assume you are using React (client-side) to build your own UI, but the same principles can be applied to any other framework.

Authentication

To perform API calls on behalf of your tenants, you can either generate a JWT token, which can be used client-side to make Outpost API calls, or you can proxy any API requests to the Outpost API through your own API. When proxying through your own API, you can ensure the API call is made for the currently authenticated tenant using the API tenant_id parameter.

Proxying through your own API can be useful if you want to limit access to some configuration or functionality of Outpost.

Generating a JWT Token (Optional)

You can generate a JWT token by using the Tenant JWT Token API.

curl --location 'localhost:3333/api/v1/<TENANT_ID>/token' \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer <API_KEY>' \
bash

Fetching Destination Type Schema

The destination type schema can be fetched using the Destination Types Schema API. It can be used to render destination information such as the destination type icon and label. Additionally, the schema includes the destination type configuration fields, which can be used to render the destination configuration UI.

Listing Configured Destinations

Destinations are listed using the List Destinations API. Destinations can be listed by type and topic. Since each destination type has different configuration, the target field can be used to display a recognizable label for the destination, such as the Webhook URL, the SQS queue URL, or Hookdeck Source Name associated with the destination. Each destination type will return a sensible target value to display.

// React example to fetch and render a list of destinations const [destinations, setDestinations] = useState([]); const [destination_types, setDestinationTypes] = useState([]); const fetchDestinations = async () => { // Get the tenant destinations const response = await fetch(`${API_URL}/api/v1/destinations`, { headers: { Authorization: `Bearer ${token}`, }, }); const destinations = await response.json(); setDestinations(destinations); }; const fetchDestinationTypes = async () => { // Get the destination types schemas const response = await fetch(`${API_URL}/api/v1/destination-types`, { headers: { Authorization: `Bearer ${token}`, }, }); const destination_types = await response.json(); setDestinationTypes(destination_types); }; useEffect(() => { fetchDestinations(); fetchDestinationTypes(); }, []); if (!destination_types || !destinations) { return <div>Loading...</div>; } const destination_type_map = destination_types.reduce((acc, type) => { acc[type.id] = type; return acc; }, {}); return ( <ul> {destinations.map((destination) => ( <li key={destination.id}> <span dangerouslySetInnerHTML={{ __html: destination_type_map[destination.type].svg, }} /> <h2>{destination_type_map[destination.type].label}</h2> {destination.target_url ? ( <a href={destination.target_url} target="_blank" rel="noopener noreferrer" > {destination.target_url} </a> ) : ( <p>{destination.target}</p> )} </li> ))} </ul> );
tsx

You can find the source code of the DestinationList.tsx component of the User Portal here: DestinationList.tsx

Creating a Destination

To create a destination, the form will require three steps: one to choose the destination type, one to select the topics (optional), and one to configure the destination.

Choosing the Destination Type

The list of available destination types is rendered from the list of destination types fetched from the API.

const [destination_types, setDestinationTypes] = useState([]); const fetchDestinationTypes = async () => { // Get the destination types schemas const response = await fetch(`${API_URL}/api/v1/destination-types`, { headers: { Authorization: `Bearer ${token}`, }, }); const destination_types = await response.json(); setDestinationTypes(destination_types); }; useEffect(() => { fetchDestinationTypes(); }, []); const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => { e.preventDefault(); const formData = new FormData(e.target as HTMLFormElement); const destination_type = formData.get("type"); goToNextStep(destination_type); }; if (!destination_types) { return <div>Loading...</div>; } return ( <div> <h1>Choose a destination type</h1> <form onSubmit={handleSubmit}> {destinations?.map((destination) => ( <label key={destination.type}> <input type="radio" name="type" value={destination.type} required defaultChecked={ defaultValue ? defaultValue.type === destination.type : undefined } /> <p> <span dangerouslySetInnerHTML={{ __html: destination.icon }} /> {destination.label} </p> <p>{destination.description}</p> </label> ))} </form> </div> );
tsx

You can find the source code of the CreateDestination.tsx component of the User Portal here: CreateDestination.tsx

Selecting Topics

Available topics are returned from the List Topics API. You can display the list of topics as a list of checkboxes to capture the user input.

const [topics, setTopics] = useState([]); const fetchTopics = async () => { const response = await fetch(`${API_URL}/api/v1/topics`, { headers: { Authorization: `Bearer ${token}`, }, }); const topics = await response.json(); setTopics(topics); }; useEffect(() => { fetchTopics(); }, []); if (!topics) { return <div>Loading...</div>; } return ( <div> <h1>Select topics</h1> <form onSubmit={handleSubmit}> {topics.map((topic) => ( <label key={topic.id}> <input type="checkbox" name="topics" value={topic.id} /> {topic.name} </label> ))} </div> );
tsx

You can find the source code of the TopicPicker.tsx component of the User Portal here: TopicPicker.tsx

Configuring the Destination

Using the destination type schema for the selected destination type, you can render a form to create and manage destinations configuration. The configuration fields are found in the configuration_fields and credentials_fields arrays of the destination type schema.

To render your form, you should render all fields from both arrays. Note that some of the credentials_fields will be obfuscated once the destination is created, and in order to edit the input, the value must be cleared first.

The input field schema is as follows:

type InputField = { type: "text" | "checkbox"; // Only text and checkbox fields are supported required: boolean; // If true, the field will be required description?: string; // Field description, to use as a tooltip sensitive?: boolean; // If true, the field will be obfuscated once the destination is created and should be treated as a password input default?: string; // Default value for the field minlength?: number; // Minimum length for the field maxlength?: number; // Maximum length for the field pattern?: string; // Regex validation pattern, to use with the input's pattern attribute };
ts

Remote Setup URL

Some destination type schemas have a remote_setup_url property that contains a URL to a page where the destination can be configured. Destinations that support remote URLs have a simplified setup flow that doesn't require instructions. For example, with the Hookdeck destination, the user is taken through a setup flow managed by Hookdeck to configure the destination.

The URL is optional but provides a better user experience than following sometimes lengthy instructions to configure the destination.

Instructions

Each destination type schema has an instructions property that contains instructions to configure the destination as a markdown string. These instructions should be displayed to the user to help them configure the destination, as for some destination types, such as AWS, the necessary configuration can be complex and require multiple steps by the user within AWS.

Example of a destination configuration form:

const DestinationConfigForm = ({ destination_type, }: { destination_type: string; }) => { const [destination_types, setDestinationTypes] = useState([]); //... Fetch the destination type schema if (!destination_types) { return <div>Loading...</div>; } const type_schema = destination_types.find( (type) => type.id === destination_type ); return ( <> {destination_type_schema.remote_setup_url ? ( <a href={destination_type_schema.remote_setup_url} target="_blank" rel="noopener noreferrer" > Setup in {destination_type_schema.label} </a> ) : ( <button onClick={showInstructionsModal}> {" "} // Modal not implemented just for example {showInstructions ? "Hide instructions" : "Show instructions"} </button> )} <form onSubmit={handleSubmit}> {[...type_schema.config_fields, ...type_schema.credential_fields].map( (field) => ( <div key={field.key}> <label htmlFor={field.key}> {field.label} {field.required && <span>\*</span>} </label> {field.type === "text" && ( <> <input type={ "sensitive" in field && field.sensitive ? "password" : "text" } placeholder={""} id={field.key} name={field.key} defaultValue={field.default} required={field.required} minLength={field.minlength} maxLength={field.maxlength} pattern={field.pattern} /> </> )} {field.type === "checkbox" && ( <input type="checkbox" id={field.key} name={field.key} defaultChecked={false} disabled={field.disabled} required={field.required} /> )} {field.description && <p>{field.description}</p>} </div> ) )} </form> </> ); };
tsx

You can find the source code of the DestinationConfigForm.tsx component of the User Portal here: DestinationConfigForm.tsx

Listing Events

Events are listed using the List Events API. You can use the topic parameter to filter the events by topic or the destination_id parameter to filter the events by destination.

const [events, setEvents] = useState([]); const fetchEvents = async () => { const response = await fetch(`${API_URL}/api/v1/events`, { headers: { Authorization: `Bearer ${token}`, }, }); }; useEffect(() => { fetchEvents(); }, []); if (!events) { return <div>Loading...</div>; } return ( <div> <h1>Events</h1> <ul> {events.map((event) => ( <li key={event.id}> <h2>{event.id}</h2> <p>{event.created_at}</p> <p>{event.payload}</p> </li> ))} </ul> </div> );
tsx

For each event, you can retrieve all its associated delivery attempts using the List Event Deliveries Attempts API.

You can find the source code of the Events.tsx component of the User Portal here: Events.tsx