Build a view

Airplane Views are currently in beta, and minor details may change. We'd love to hear any feedback or requests at hello@airplane.dev.
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 Upgrading 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
If you are using Windows, we recommend using WSL and following the instructions for Linux.

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
Once that command has finished, navigate into the created /views_getting_started directory and open up the contents in your favorite code editor.
You'll see files were created, including:
  • customer_dashboard/​customer_dashboard.view.tsx The entrypoint to the view. This is where your code mostly lives.
  • customer_dashboard/​customer_dashboard.view.yaml The view definition file. This is where you define metadata such as the name and description of the view.
  • package.json Configures dependencies used in your view.
  • tasks/* Tasks that the view calls to populate its data. (See Build a SQL task.)

Deploy tasks

The view that you initialized includes Airplane tasks in the tasks/ directory. The view calls these tasks to populate itself with data—tasks are the backend to the view's frontend.
Before you can develop a view against a task, the task must be deployed to Airplane. (This is a limitation that will be removed in the future.) You can deploy the tasks by running:
shell
Copied
1
airplane deploy tasks --yes

Develop your view locally

Once your view is created and your tasks are deployed, run:
shell
Copied
1
airplane views dev
After a few moments, a dev server will start. Press ENTER or navigate to the printed URL in your browser (http://127.0.0.1:5173) to see your view.
Open customer_dashboard/​customer_dashboard.view.tsx to see the code that powers the view. The view shows a table of a company's accounts using the following code.
jsx
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 which is set to demo_list_accounts.
This prop instructs the table to call the task with slug demo_list_accounts and to populate its rows and columns using the output of the task. This table is a Task backed component—it sets its own data, loading and error states from a task. 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 make the following changes to the table:
  • Hide the id column (the column's data will still be around, we just won't show it to the user).
  • Update the auto-generated company_name column label to Company.
jsx
Copied
1
<Table
2
title="Accounts"
3
task="demo_list_accounts"
4
hiddenColumns={["id"]}
5
columns={[{ accessor: "company_name", label: "Company" }]}
6
/>

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
hiddenColumns={["id"]}
5
columns={[{ accessor: "company_name", label: "Company", canEdit: true }]}
6
rowActions={{ slug: "demo_update_account", label: "Update" }}
7
/>
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
hiddenColumns={["id"]}
13
columns={[{ accessor: "company_name", label: "Company", canEdit: true }]}
14
rowActions={{ slug: "demo_update_account", label: "Update" }}
15
rowSelection="single"
16
/>
17
18
{selectedAccount && (
19
<Table
20
title="Users"
21
task={{
22
slug: "demo_list_account_users",
23
params: { account_id: selectedAccount.id },
24
}}
25
hiddenColumns={["id", "role"]}
26
/>
27
)}
28
</Stack>
29
);
30
};
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
hiddenColumns={["id"]}
16
columns={[{ accessor: "company_name", label: "Company", canEdit: true }]}
17
rowActions={{ slug: "demo_update_account", label: "Update" }}
18
rowSelection="single"
19
/>
20
21
{selectedAccount && (
22
<Stack>
23
<Table
24
id="users"
25
title="Users"
26
task={{
27
slug: "demo_list_account_users",
28
params: { account_id: selectedAccount.id },
29
}}
30
hiddenColumns={["id", "role"]}
31
rowSelection="checkbox"
32
/>
33
<Stack direction="row" grow>
34
{selectedUsers.map((user) => (
35
<UserDetail user={user} />
36
))}
37
</Stack>
38
</Stack>
39
)}
40
</Stack>
41
);
42
};
43
44
const UserDetail = ({ user }) => {
45
const userDetail = `### User: _${user.id}_
46
### Name
47
${user.name}
48
### Role
49
**${user.role}**`;
50
51
return (
52
<Card>
53
<Text>{userDetail}</Text>
54
<Button
55
task={{
56
slug: "demo_promote_account_user",
57
params: { id: user.id },
58
refetchTasks: "demo_list_account_users",
59
}}
60
>
61
Promote
62
</Button>
63
</Card>
64
);
65
};
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 Text to render the user's name and role and a task backed Button that executes the demo_promote_account_user task when clicked. Note that Text automatically parses the Markdown in the user details.
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 viwes are written with code, you can integrate custom NPM packages (Dependency management) and write custom components.