Skip to main content

Pages

While actions are the bread and butter of building with Interval, Interval's Pages API enables more sophisticated UIs and structure, such as dashboards, list and resource views, and hierarchical navigation.

info

The Pages API is available on SDK versions v0.35.0 and higher.

Pages are still in beta. The API names and functionality may change.

With the Pages API you can:

  • Define top-level navigation within your organization's Interval dashboard.
  • Group related actions to provide better structure and easier navigation.
  • Create screens with text, data, and list views to be used as a launching point for other actions.

The Pages 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.

The Page 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. Optional.
  • unlisted: An optional boolean property that hides the page from auto-generated navigation if set to true. Defaults to false.
caution

With the introduction of the Pages 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.

import Interval, { Page, io } from "@interval/sdk";

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
},
},
},
}),
},
});

interval.listen();

Page objects can themselves contain other pages, allowing you to construct arbitrary path-like hierarchies. In the above code example, these four paths are generated:

  • hello_world
  • users
  • users/create

You'll use these paths when linking between actions.

tip

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

In addition to being defined on the Interval or Page constructors, routes can be added or removed dynamically via the add() and remove() methods. This works the same as adding and removing actions, and the add() and remove() methods are also available on Page to manage sub-pages or nested actions within a page.

Customizing page layout

The default appearance of a Page is a grid or list view of the actions defined within it, similar to the root Dashboard screen:

interval.com

For more control over output - such as displaying text, metadata, and list views - you can define an asynchronous handler() function for the Page which allows for customizing the page's layout.

A Page's handler is conceptually similar to an Action handler, but it can't await input; it must return an instance of the Layout class.

Layout supports the following properties in its class constructor:

  • title: The page's title and primary heading. Type string | Promise<string> | (() => string | Promise<string>). Optional. If not defined, it will use the name property from its parent Page.
  • description: The page's description. Type string | Promise<string> | (() => string | Promise<string>). 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 body. These should not be awaited. Optional.

In the following example, the default actions list is replaced with a list of users:

import Interval, { Page, Layout, io } from "@interval/sdk";
import { fetchUsers } from "../db/users";

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 () => {
const betaUsers = await fetchUsers();

return new Layout({
title: "Beta users",
description: "These users have signed up for the beta.",
menuItems: [
{
label: "Create user",
action: "users/create",
},
],
children: [
io.display.metadata("", {
layout: "card",
data: [
{
label: "Total users",
value: betaUsers.length,
},
{
label: "Active users",
value: betaUsers.filter(u => u.isActive).length,
},
],
}),
io.display.table("Users", {
data: betaUsers,
}),
],
});
},
}),
},
});

interval.listen();

Loading routes from the filesystem

caution

This feature is currently only available in the TypeScript/JavaScript SDK. If you're interested in filesystem routes with Python, let us know!

As the number of actions and pages in your app grows, you'll likely want to move them out of the Interval constructor into separate files. Loading actions and pages from the filesystem can be done automatically with the routesDirectory property:

import Interval from "@interval/sdk";
import path from "path";

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

When routesDirectory is defined, 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.

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.

A Page is automatically created for each directory, and files inside the directory that export an Action or Page will be nested within that Page. The directory's auto-generated Page can be customized by an index.ts (or index.js) file, which should export a Page as its default export.

Using the Users example above:

  • index.ts defines the Interval constructor and configures routesDirectory
  • users/index.ts exports a Page as its default export
  • users/create.ts exports an Action as its default export

Therefore, these two examples are equivalent:

// in `src/interval.ts`
import Interval from "@interval/sdk";
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";

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";

export default new Page({
name: "Users",
handler: async () => {
return new Layout({
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: {
edit: {
name: "Edit user",
handler: async () => {
// Usual action code, just nested within a route
},
},
},
});

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

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

Standalone object or function exports are not supported for file-based actions; always use the Action and Page class wrappers.

Was this section useful?