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.
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
Pages are defined using Page
class instances. Pages will create a hierarchy entry for the page and provide a space for nesting related actions or other pages beneath it in the hierarchy.
The Page
constructor accepts an object of the following shape:
name
: The page's name. Required.handler
: The page's layout handler. Optional.unlisted
: An optional boolean property that hides the page from auto-generated navigation if set totrue
. Defaults tofalse
.routes
: An object containing the actions and sub-pages nested within the page, just likeInterval
's top-levelroutes
property. Optional.
- TypeScript
- JavaScript
tip
See Writing actions for more information about how routes are defined, and Loading routes from the filesystem for more information about how that relates to Pages specifically.
import { Page } from "@interval/sdk";
export default new Page({
name: "Users",
routes: {
create: {
name: "Create user",
handler: async () => {
// Usual action code, just nested within a route
},
},
},
});
tip
See Writing actions for more information about how routes are defined, and Loading routes from the filesystem for more information about how that relates to Pages specifically.
import { Page } from "@interval/sdk";
export default new Page({
name: "Users",
routes: {
create: {
name: "Create user",
handler: async () => {
// Usual action code, just nested within a route
},
},
},
});
In addition to being defined by the filesystem or in 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:
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. Typestring | Promise<string> | (() => string | Promise<string>)
. Optional. If not defined, it will use thename
property from its parentPage
.description
: The page's description. Typestring | 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 ofio.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:
- TypeScript
- JavaScript
import { Page, Layout, io } from "@interval/sdk";
import { fetchUsers } from "../db/users";
export default 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",
route: "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,
}),
],
});
},
});
import { Page, Layout, io } from "@interval/sdk";
import { fetchUsers } from "../db/users";
export default 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",
route: "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,
}),
],
});
},
});
Loading routes from the filesystem
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({
endpoint: "wss://<YOUR INTERVAL SERVER URL>/websocket",
apiKey: "<YOUR API KEY>", // get an API key from the Keys page in your Interval dashboard
routesDirectory: path.resolve(__dirname, "routes"),
});
When routesDirectory
is defined, Interval will recursively walk directories
and detect files with default exports of Action
s or Page
s 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.
If necessary, routes can also be defined inline using the routes
property.
The two tabs below provide an identical route structure, one using
routesDirectory
and the other using inline routes
.
- Using routesDirectory
- Using routes
import { Interval } from "@interval/sdk";
import path from "path";
const interval = new Interval({
endpoint: "wss://<YOUR INTERVAL SERVER URL>/websocket",
apiKey: "<YOUR API KEY>", // get an API key from the Keys page in your Interval dashboard
routesDirectory: path.resolve(__dirname, "routes"),
});
interval.listen();
import { Action, io } from "@interval/sdk";
export default new Action(async () => {
const name = await io.input.text("Your name");
return `Hello, ${name}`;
});
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.",
menuItems: [
{
label: "Create user",
route: "users/create",
},
{
label: "View metrics",
route: "hello_world",
},
],
children: [
io.display.metdata("", {
layout: "card",
data: [
{
label: "Total users",
value: 2,
},
{
label: "Active users",
value: 1,
},
],
}),
io.display.table("Users", {
data: [
{ name: "Alice", email: "alice@example.com" },
{ name: "Bartholomew", email: "bart@example.com" },
],
}),
],
});
},
});
import { Action, io } from "@interval/sdk";
export default new Action({
name: "Create user",
handler: async () => {
// Usual action code, just nested within a route
},
});
import { Interval, Page, io } from "@interval/sdk";
const interval = new Interval({
endpoint: "wss://<YOUR INTERVAL SERVER URL>/websocket",
apiKey: "<YOUR API KEY>", // get an API key from the Keys page in your Interval dashboard
routes: {
hello_world: async () => {
const name = await io.input.text("Your name");
return `Hello, ${name}`;
},
users: new Page({
name: "Users",
handler: async () => {
return new Layout({
title: "Beta users",
description: "These users have signed up for the beta.",
menuItems: [
{
label: "Create user",
route: "users/create",
},
{
label: "View metrics",
route: "hello_world",
},
],
children: [
io.display.metdata("", {
layout: "card",
data: [
{
label: "Total users",
value: 2,
},
{
label: "Active users",
value: 1,
},
],
}),
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
},
},
},
}),
},
});
interval.listen();
info
Standalone object or function exports are not supported for file-based
actions; always use the Action
and Page
class wrappers.