Skip to main content

Routing and dashboard structure

info

Routing is an experimental new feature available starting with v0.30.0 and is currently in private beta. Please contact us if you would like us to enable it for your organization.

Names and functionality of the Routing API are likely to change.

While actions are the bread and butter of building with Interval, powerful internal tools may call for more sophisticated UIs and structure, such as dashboards, list and resource views, and hierachical navigation.

Interval's Routing API enables these more powerful features within your Interval app.

With the Routing API you can:

  • Define top-level navigation within your organization's Interval dashboard
  • Group related actions to provide better structure and easier navigation
  • Create pages and list views with custom content for your team to be used as a launching point for other actions
  • Make some customizations for layout and UI within the Interval dashboard

The Routing API introduces a second type of screen, called a page. While actions are designed for collecting input and displaying output (analogous to PUT/POST/DELETE in HTTP), pages are just for displaying content (analogous to GET in HTTP). For example, you might create a Users screen in your app, where the root Users screen that shows a list of users is a page, and the Create user and Update user forms are actions.

Together, pages and actions - along with linking APIs for navigating between the two - are the primary building blocks for developing apps with Interval.

Defining pages

To create a page, create a Page instance and pass it in when you instantiate your Interval class, alongside any existing actions. This will create a top-level navigation link for the page and provide a space for nesting related actions within the page.

Page objects can themselves contain other pages, allowing you to construct arbitrary path-like hierarchies.

caution

With the introduction of the Routing API, the actions param in the Interval class has been renamed to routes. You can still define actions here as usual, in addition to pages.

// Note the experimental import path
import Interval, { Page, io } from "@interval/sdk/dist/experimental";

const interval = new Interval({
apiKey: "<YOUR API KEY>",
// Note: `actions` has been renamed to `routes`, contains both actions and pages
routes: {
hello_world: async () => {
const name = await io.input.text("Your name");
return `Hello, ${name}`;
},
users: new Page({
name: "Users",
routes: {
create: {
name: "Create user",
handler: async () => {
// Usual action code, just nested within a route
},
},
update: {
name: "Edit user",
handler: async () => {
// Usual action code, just nested within a route
},
},
},
}),
},
});

interval.listen();
interval.com

A Page's constructor accepts an object of the following shape:

  • name: The page's name. Required.
  • routes: An object containing the actions and sub-pages nested within the page, just like Interval's top-level routes property (formerly actions). Optional.

In addition to being defined on the Interval or Page constructors, routes can be added via the routes.add() or add() methods which accept the page's slug as first argument and a Page object as second argument.

Routes can be removed by passing the group slug to Interval's routes.remove() or Page's remove() methods:

interval.routes.add("hello", hello);

interval.listen();

// in response to some event
hello.remove("hello");

The routes.add() and routes.remove() methods can be used to dynamically add or remove routes — before and after calling Interval's listen() method.

Actions can be dynamically added or removed from an action group using the .add() and .remove() methods, just like the methods on interval.routes.

tip

Action slugs do not need to be unique across different routes.

Rendering pages

Within a Page, you may also define an asynchronous handler() function which allows for customizing the page's layout. This function should return a Layout class to define the content of the page.

Pages with a handler function may also define nested sub-actions via routes as in the examples above. These will appear in a secondary nav to make room for the page content.

Currently the only supported layout is Layout.Basic, which supports the following properties in its class constructor:

  • title: The page's title and primary heading. Type string | Promise<string> | (() => string | Promise<string>). Required.
  • description: The page's description. Type string | Promise<string> | (() => string | Promise<string>). Optional.
  • metadata: An array of key/value pairs of metadata to display on the page. labels must be static strings, values are primitive values, Promises of primitive values, or a function returning those. Optional.
  • menuItems: An array of objects defining links to other actions and populate in the page as a menu of buttons.
  • children: An array of io.display components to render as the page's primary body contents, above any actions or sub-groups available in the group. These should not be awaited. Optional.
// Note the experimental import path
import Interval, { Page, Layout, io } from "@interval/sdk/dist/experimental";

const interval = new Interval({
apiKey: "<YOUR API KEY>",
routes: {
hello_world: async () => {
const name = await io.input.text("Your name");
return `Hello, ${name}`;
},
users: new Page({
name: "Users",
routes: {
create: {
name: "Create user",
handler: async () => {
// Usual action code, just nested within a route
},
},
},
handler: async () => {
return new Layout.Basic({
title: "Beta users",
description: "These users have signed up for the beta.",
metadata: [
{
label: "Total users",
value: 2,
},
{
label: "Active users",
value: 1,
},
],
menuItems: [
{
label: "Create user",
action: "users/create",
},
{
label: "View metrics",
action: "hello_world",
},
],
children: [
io.display.table("Users", {
data: [
{ name: "Alice", email: "alice@example.com" },
{ name: "Bartholomew", email: "bart@example.com" },
],
}),
],
});
},
}),
},
});

interval.listen();

Loading routes from the filesystem

Instead of manually defining routes in the Interval constructor, they can also be loaded directly from the filesystem using the routesDirectory property.

Interval will recursively walk directories and detect files with default exports of Actions or Pages and add them to your routes based on their files' paths.

A Page will automatically be created for each directory, and files inside that export Actions (or Pages) will be added to that Page. If a directory contains an index.ts (or index.js) file, its default export should be a Page, which will customize the automatically generated Page for the directory and can contain a handler and inline routes which will be added to the file-based routes detected.

tip

Relative path resolution is based on your Node.js process's current working directory. In order to load starting from a path relative to the Interval constructor, be sure to provide an explicit path, for instance by using the __dirname variable.

For instance, these two examples are equivalent:

// in `src/interval.ts`
import Interval from "@interval/sdk/dist/experimental";
import path from "path";

const interval = new Interval({
apiKey: "<YOUR API KEY>",
routesDirectory: path.resolve(__dirname, "routes"),
});

interval.listen();

// in `src/routes/hello_world.ts`
import { Action, io } from "@interval/sdk/dist/experimental";

export default new Action(async () => {
const name = await io.input.text("Your name");
return `Hello, ${name}`;
});

// in `src/routes/users/index.ts`
import { Page, Layout, io } from "@interval/sdk/dist/experimental";

export default new Page({
name: "Users",
handler: async () => {
return new Layout.Basic({
title: "Beta users",
description: "These users have signed up for the beta.",
metadata: [
{
label: "Total users",
value: 2,
},
{
label: "Active users",
value: 1,
},
],
menuItems: [
{
label: "Create user",
action: "users/create",
},
{
label: "View metrics",
action: "hello_world",
},
],
children: [
io.display.table("Users", {
data: [
{ name: "Alice", email: "alice@example.com" },
{ name: "Bartholomew", email: "bart@example.com" },
],
}),
],
});
},
routes: {
create: {
name: "Create user",
handler: async () => {
// Usual action code, just nested within a route
},
},
},
});

// in `src/routes/users/edit.ts`
import { Action, io } from "@interval/sdk/dist/experimental";

export default new Action({
name: "Edit user",
handler: async () => {
// Usual action code, just nested within a route
},
});
info

Standalone object or function exports are not supported for file-based actions, remember to use the new Action class wrapper.

Did this section clearly explain what you wanted to learn?