This tutorial uses UI Kit 1 and @forge/cli
version 7.1.0
. This tutorial won't work if you're using
the latest version of @forge/cli
.
This tutorial demonstrates how to store structured data through custom entities and query that data. This will help you understand how custom entities work in Forge.
You’ll build a Confluence app that can create users with multiple attributes (name, age, and country). This app will also allow you to query those users through any of their attributes, and delete them.
This tutorial assumes you're already familiar with developing on Forge. If this is your first time using Forge, see Getting started for step-by-step instructions on setting up Forge.
To complete this tutorial, you need the following:
npm install -g @forge/cli@latest
on the command line.Create an app based on the Hello world template. Using your terminal complete the following:
Navigate to the directory where you want to create the app.
Create your app by running:
1 2forge create
Enter a name for the app. For example, user-management-via-entities.
Select the UI kit category.
Select the confluence-macro template.
Your app has been created in a directory with the same name as your app; for example user-management-via-entities. Open the app directory to see the files associated with your app.
Install the latest version of the Forge API package:
1 2npm install --save @forge/api@latest
Install the UUID package, which will generate UUID keys required by our app:
1 2npm install --save uuid
We’ll add the scope required by the storage
API and declare the entity we’ll use (along with its indexes).
storage
API by adding the storage:app
scope to the manifest.yml file
. Learn more about adding scopes to call an Atlassian REST API.
1 2permissions: scopes: - storage:app
app
section of the manifest.yml
file, add the following entity declaration:
1 2app: ... storage: entities: - name: 'users' attributes: name: type: 'string' age: type: 'integer' country: type: 'string' indexes: - name: 'by-country' range: - 'country' - name: 'by-country-name' range: - 'name' partition: - 'country'
Here, we declare a users
entity with the following attributes: name
, age
, and country
. This declaration also consists of two indexes:
by-country
: for querying by country
by-country-name
: for querying by name
and country
, togetherSee Indexes for more details about how to construct an index.
Navigate to the app's top-level directory and deploy your app by running:
1 2forge deploy
Before installing your app, check that the indexes are already created first:
1 2forge storage entities indexes list -e development
If the indexes were created successfully, this command should display the following:
Install your app by running:
1 2forge install
Select your Atlassian product using the arrow keys and press the enter key.
Enter the URL for your development site. For example, example.atlassian.net. View a list of your active sites at Atlassian administration.
Once the successful installation message appears, your app is installed and ready to use on the specified site. You can always delete your app from the site by running the forge uninstall
command.
Running the forge install
command only installs your app onto the selected product.
To install onto multiple products, repeat these steps again, selecting another product each time.
Note that the Atlassian Marketplace
does not support cross-product apps yet.
You must run forge deploy
before running forge install
in any of the Forge environments.
We’ll now add a tab for creating users and assign a name, age, and country to them.
In the src/
directory, add a new file named create-user.jsx
with the following contents:
1 2import ForgeUI, { TextField, Form, Select, Option, Fragment, SectionMessage, Text, useState } from '@forge/ui'; import { storage } from '@forge/api' import { v4 as uuidv4 } from 'uuid'; export default function CreateUser() { const [userCreated, setUserCreated] = useState(false); const createUser = async (data) => { try { await storage .entity("users") .set(`user-${uuidv4()}`, { ...data, age: parseInt(data.age), }); setUserCreated(true); } catch (e) { throw e; } } return ( <Fragment> <Form onSubmit={createUser} submitButtonText="Create user"> <TextField name="name" label="Name" type="text" /> <TextField name="age" label="Age" type="number" /> <Select name="country" label="Country"> <Option label="India" value="India" /> <Option label="Australia" value="Australia" /> <Option label="Indonesia" value="Indonesia" /> </Select> </Form> {userCreated && <SectionMessage title="Success" appearance="info"> <Text>User created successfully</Text> </SectionMessage> } </Fragment> ) }
This file will hold the form with the inputs as per your entity definition. This user form will let you create users. Later on, we’ll add the capabilities to query and delete users to this form.
The create-user.jsx
file features the following:
A new component named CreateUser
, which will contain the user form. This component uses a React state named userCreated
, which we’ll use to display a message after a user is created.
A function named createUser
which is a handler to the submit button in the form. This function makes a call to set storage operation and set userCreated
state to true
for showing the success message.
After creating the create-user.jsx
file, continue with the next steps:
Install the uuid
package:
1 2npm install --save uuid
The createUser
function uses the uuid
package to generate unique keys automatically each time a user is created. All attributes (name
, age
, and country
) will be stored as values against each user’s unique key.
Import and add a new Tab
from Forge UI in index.jsx
, which will hold this user form. Your index.jsx
file should look like this:
1 2import ForgeUI,{ render, Fragment, Macro, Tabs, Tab } from "@forge/ui"; import CreateUser from "./create-user"; const App = () => { return ( <Fragment> <Tabs> <Tab label="Create users"> <CreateUser /> </Tab> </Tabs> </Fragment> ); }; export const run = render( <Macro app={<App />} /> );
Re-deploy and verify your changes in the app by running:
1 2forge deploy
Use the form to create several users, with multiple ones for each country from the selection. These will help us in the next step, where we will query these users based on their country and (or) their name.
With your app installed, it’s time to see the app on a page.
/
You should now be able to view the interface for creating users on the page:
After using the form to create users, let’s enhance it with the capability to query those user by country and age:
In the src/
directory, add a new file named user-view.jsx
with the following contents:
1 2import ForgeUI, { Table, Cell, Text, Head, Row } from '@forge/ui'; export default function UsersView({ users }) { return ( <Table> <Head> <Cell> <Text>Name</Text> </Cell> <Cell> <Text>Country</Text> </Cell> <Cell> <Text>Age</Text> </Cell> </Head> { users.map((user) => ( <Row> <Cell> <Text>{user.value.name}</Text> </Cell> <Cell> <Text>{user.value.country}</Text> </Cell> <Cell> <Text>{user.value.age}</Text> </Cell> </Row> )) } </Table> ) }
This file displays the queried data in a table. It also creates a component named UsersView
which displays each user’s attributes. This component doesn’t have any state, and users are passed as a parameter.
Next, add a file in the src/
directory named query-user.jsx
with the following contents:
1 2import ForgeUI, { Fragment, TextField, Form, useState, Select, Option } from '@forge/ui'; import { storage, WhereConditions } from '@forge/api'; import UsersView from './user-view'; export default function QueryUsers({ }) { const [searchResultsByName, setSearchResultsByName] = useState([]); const searchByName = async (data) => { try { let queryBuilder = storage .entity("users") .query() .index('by-country-name', { partition: [data.country] }) if (data.name) { queryBuilder = queryBuilder .where(WhereConditions.beginsWith(data.name)); } const results = await queryBuilder .getMany(); setSearchResultsByName(results.results); } catch (e) { throw e; } } return ( <Fragment> <Form onSubmit={searchByName} submitButtonText="Search"> <Select isRequired name="country" label="Country"> <Option label="India" value="India" /> <Option label="Australia" value="Australia" /> <Option label="Indonesia" value="Indonesia" /> </Select> <TextField name="name" label="Search by name beginning with" type="text" /> </Form> <UsersView users={searchResultsByName} /> </Fragment> ) }
The query-user.jsx
file will hold the form to submit our query parameters. This file:
by-country-name
index to limit queries around a selected country.beginsWith
condition to filter partial or exact matches to the user’s name
. See Filtering methods and Conditions for more information about building queries.QueryUsers
component that captures query results in the setSearchResultsByName
state variable, which is being passed to the UsersView
component (from the user-view.jsx
file).UsersView
component to display the query results.After creating both user-view.jsx
and query-user.jsx
files, continue with the next steps:
Import and add QueryUsers
in your index.jsx
in a Tab
view.
1 2... import QueryUsers from "./query-user" ... ... <Tab label="Query users"> <QueryUsers /> </Tab> ... ); };
Re-deploy the app by running:
1 2forge deploy
Test the app’s Query users tab using several queries, based on the users you created in the previous section.
Finally, let’s add the following buttons to our Query users tab:
storage.entity("entity-name").get
endpoint to fetch details about a selected user, identified by its key. Learn more about the endpoint here.storage.entity("entity-name").delete
endpoint to trigger the deletion of a specific user, identified by its key. Learn more about the endpoint here.To do this, update the user-view.jsx
with the following contents:
1 2import ForgeUI, { Table, Cell, Text, Head, Row, ButtonSet, Button, useState, Code } from '@forge/ui'; import { storage } from '@forge/api'; export default function UsersView({ users }) { const [userDetail, setUserDetail] = useState(null); const getUserDetail = async (user) => { const userDetailResponse = await storage.entity('users').get(user.key); setUserDetail({ key: user.key, value: userDetailResponse }); }; const deleteUser = async (userKey) => { await storage.entity('users').delete(userKey); setUserDetail(null); }; return ( <Table> <Head> <Cell> <Text>Name</Text> </Cell> <Cell> <Text>Country</Text> </Cell> <Cell> <Text>Age</Text> </Cell> <Cell> <Text>Actions</Text> </Cell> <Cell> <Text>User details</Text> </Cell> </Head> { users.map((user) => ( <Row> <Cell> <Text>{user.value.name}</Text> </Cell> <Cell> <Text>{user.value.country}</Text> </Cell> <Cell> <Text>{user.value.age}</Text> </Cell> <Cell> <ButtonSet> <Button text='Get details' onClick={async () => { await getUserDetail(user); }} /> <Button text='Delete user' onClick={async () => { await deleteUser(user.key); }} /> </ButtonSet> </Cell> <Cell> { (userDetail && userDetail.key === user.key) ? ( <Code text={JSON.stringify(userDetail, null, 2)} language="json" /> ) : null } </Cell> </Row> )) } </Table> ) }
After updating user-view.jsx
, re-deploy and verify the app by running:
1 2forge deploy
You can now test both buttons to fetch data about a user or delete them. If needed, create more users to test custom entities even further.
Explore the app storage API in further detail over the following pages:
Rate this page: