Build a view

If you're looking for more in-depth Views documentation, check out Views overview.
Airplane makes it incredibly easy to build UIs ("views") that you and your teammates can use.
In this guide, we'll build a customer dashboard where you can see and operate on customer accounts:

Before you begin

If you haven't yet, first install the Airplane CLI by following the instructions at Installing the CLI. (If you have already installed the Airplane CLI, ensure you have the latest version by Updating the CLI.)
To develop views, you must have node and npm installed, with a node version of at least 14.
bash
Copied
1
node -v
2
npm -v
We recommend nvm for installing and managing versions of node and npm.
bash
Copied
1
nvm use 18 || nvm install 18

Create a view

Once you've installed and logged in to the CLI, navigate to the folder where you want to create your view and run:
shell
Copied
1
airplane init --template=views_getting_started --reset-demo-db
This will use the Views getting started template to populate your directory with some starter code. (--reset-demo-db will re-initialize your demo database.)
Once that command has finished, navigate into the created /views_getting_started directory and open up the contents in your code editor of choice.
You'll see files were created, including:
  • customer_dashboard.airplane.tsx The entrypoint to the view. This is where your code mostly lives. Views must have a suffix of .airplane.tsx or .view.tsx to be recognized by Airplane.
  • package.json Configures dependencies used in your view.
  • tasks/* Tasks that the view calls to populate its data. (See Build a SQL task.)

Develop your view locally

To preview your view and tasks, run:
shell
Copied
1
airplane dev
After a few moments, a dev server will start. Press ENTER or navigate to the printed URL in your browser to open your view in the Airplane Studio.
Open customer_dashboard.airplane.tsx to see the code that powers the view. The view shows a table of a company's accounts using the following code.
tsx
Copied
1
const CustomerDashboard = () => {
2
return (
3
<Stack>
4
<Title>Customer dashboard</Title>
5
<Table title="Accounts" task="demo_list_accounts" />
6
</Stack>
7
);
8
};
Notice that the Table component has a prop (a React term for component input) called task that is set to demo_list_accounts. This means that the table is a Task backed component.
With just one line of code, we've created a table that executes the task with slug demo_list_accounts, populates its rows and columns using the task's output, and handles details like loading and error states. In our example view, the table is backed by a demo task, but you can use any task you've deployed to Airplane.
While components can be used without Airplane tasks, task backed components are easier to set up and provide best practices out of the box.

Customize the accounts table

Task backed components provide great out-of-the-box simplicity, but you might want to customize your component for improved user experience.
Let's define the columns of the table. This allows us to customize the column labels and omit the id column. The id column's data will still be around, but won't be shown to the user.
jsx
Copied
1
<Table
2
title="Accounts"
3
task="demo_list_accounts"
4
columns={[
5
{ label: "Company Name", accessor: "company_name" },
6
{ label: "Country", accessor: "country" },
7
{ label: "Signup Date", accessor: "signup_date" },
8
]}
9
/>

Edit accounts with row actions

Let's add a new feature to our table that allows users to update the name of a company associated with an account.
jsx
Copied
1
<Table
2
title="Accounts"
3
task="demo_list_accounts"
4
columns={[
5
{ label: "Company Name", accessor: "company_name", canEdit: true },
6
{ label: "Country", accessor: "country" },
7
{ label: "Signup Date", accessor: "signup_date" },
8
]}
9
rowActions={{ slug: "demo_update_account", label: "Update" }}
10
/>
We've made the company_name column editable by setting canEdit to true. This allows a user to edit the company's name using a text input box.
To update the account and save the edit, we've add a rowAction that calls the Airplane task with slug demo_update_account. The task will automatically be passed the value of the row as parameters (including the updated company name and the hidden id) and persist the edit to our data store. By default, the rowAction button will have a label that is equal to the Airplane task (demo_update_account), but we can change it to Update just like we changed the company_name column.
Go ahead and change the company name and hit the Update button. Refresh the page to see that the update was saved!

Add a users table using component state

Next, let's add a new table that displays the users for a given account when the account is selected.
jsx
Copied
1
const CustomerDashboard = () => {
2
const accountsState = useComponentState("accounts");
3
const selectedAccount = accountsState.selectedRow;
4
5
return (
6
<Stack>
7
<Title>Customer dashboard</Title>
8
<Table
9
id="accounts"
10
title="Accounts"
11
task="demo_list_accounts"
12
columns={[
13
{ label: "Company Name", accessor: "company_name", canEdit: true },
14
{ label: "Country", accessor: "country" },
15
{ label: "Signup Date", accessor: "signup_date" },
16
]}
17
rowActions={{ slug: "demo_update_account", label: "Update" }}
18
rowSelection="single"
19
/>
20
21
{selectedAccount && (
22
<Table
23
title="Users"
24
task={{
25
slug: "demo_list_account_users",
26
params: { account_id: selectedAccount.id },
27
}}
28
hiddenColumns={["id", "role"]}
29
/>
30
)}
31
</Stack>
32
);
33
};
Let's unpack what just happened:
First, we set the rowSelection prop on the accounts table to single, indicating that users should be able to select a single account.
Next, useComponentState lets us grab the selected rows via component state. Each component saves its state onto component state so that it can be accessed elsewhere. Here we're getting the state for component with id="accounts" (the accounts table) and then pulling the selectedRow off the state.
jsx
Copied
1
const accountsState = useComponentState("accounts");
2
const selectedAccount = accountsState.selectedRow;
Reference a component's doc page to see its component state API. For example, Table state API.
Finally, we are rendering a new task backed table that calls the demo_list_account_users task, passing in the id of the selected account as a parameter.
Go ahead and try to select a row. A users table should appear beneath the accounts table that shows users for that account.
Selecting a row on the table displays another Table of users who belong to that team, also backed by an Airplane task. The view uses component state to communicate between the team table and the users table.

Create a user detail card where you can promote a user

Let's add one more feature to our accounts dashboard: the ability to view more detailed information and take an action on a user.
jsx
Copied
1
const CustomerDashboard = () => {
2
const accountsState = useComponentState("accounts");
3
const selectedAccount = accountsState.selectedRow;
4
5
const usersState = useComponentState("users");
6
const selectedUsers = usersState.selectedRows;
7
8
return (
9
<Stack>
10
<Title>Customer dashboard</Title>
11
<Table
12
id="accounts"
13
title="Accounts"
14
task="demo_list_accounts"
15
columns={[
16
{ label: "Company Name", accessor: "company_name", canEdit: true },
17
{ label: "Country", accessor: "country" },
18
{ label: "Signup Date", accessor: "signup_date" },
19
]}
20
rowActions={{ slug: "demo_update_account", label: "Update" }}
21
rowSelection="single"
22
/>
23
24
{selectedAccount && (
25
<>
26
<Table
27
id="users"
28
title="Users"
29
task={{
30
slug: "demo_list_account_users",
31
params: { account_id: selectedAccount.id },
32
}}
33
hiddenColumns={["id", "role"]}
34
rowSelection="checkbox"
35
/>
36
<Stack direction="row">
37
{selectedUsers.map((user) => (
38
<UserDetail key={user.id} user={user} />
39
))}
40
</Stack>
41
</>
42
)}
43
</Stack>
44
);
45
};
46
47
const UserDetail = ({ user }) => {
48
return (
49
<Card width="1/3">
50
<Stack>
51
<DescriptionList
52
items={[
53
{ term: "ID", description: user.id },
54
{ term: "Name", description: user.name },
55
{ term: "Role", description: user.role },
56
]}
57
/>
58
<Stack align="end">
59
<Button
60
task={{
61
slug: "demo_promote_account_user",
62
params: { id: user.id },
63
refetchTasks: "demo_list_account_users",
64
}}
65
preset="secondary"
66
>
67
Promote
68
</Button>
69
</Stack>
70
</Stack>
71
</Card>
72
);
73
};
First, make users selectable by adding rowSelection="checkbox" to the users table. This allows us to select one or more users using checkboxes.
Next, get the component state of the users table.
jsx
Copied
1
const usersState = useComponentState("users");
2
const selectedUsers = usersState.selectedRows;
Render each selected user using a custom UserDetail component. We use direction="row" on the Stack to render the user details side by side and a .map to loop over the selected users and create a UserDetail for each one.
The UserDetail component uses a Description list to render the user's id, name and role and a task backed Button that executes the demo_promote_account_user task when clicked.
Notice that the Button has an additional prop refetchTasks. All Airplane tasks are automatically cached, so when we make a change that may affect the output of a task, we need to manually instruct the system to invalidate the cache and refetch the task. In this case, promoting a user changes the output of demo_list_account_users, so we tell the button to refetch this task once the demo_promote_account_user task is complete.
Voilà—now when you select one or more users, you can see more information about each of them in a card. The card also contains a Promote button that promotes the user to a more senior role and then refreshes the users.

Deploy your view to Airplane

Now that we have finished developing our view, run:
shell
Copied
1
airplane deploy .
Once deployed, go to your Library to see your view in action!

Wrapping up

Views allow building powerful UIs with a few simple components (Components overview). Because Airplane Views are written with code, you can integrate custom NPM packages (Dependency management) and write custom components.