Arsalan Khattak
17 December 2024

Create a React Gantt chart with Bryntum and Appwrite

Bryntum Gantt is a performant, fully customizable Gantt chart. As a Bryntum React UI component, it can be used with […]

Bryntum Gantt is a performant, fully customizable Gantt chart. As a Bryntum React UI component, it can be used with all major JavaScript frameworks and integrated with a range of backend services. This guide demonstrates how to use Bryntum Gantt with Appwrite, a backend platform for developing web, mobile, and Flutter applications.

In this tutorial, we’ll do the following:

Here’s what we’ll build:

You can find the code for the completed tutorial in these GitHub repositories:

Creating an Appwrite project

Sign up for an Appwrite account if you don’t already have one.

First, create an organization by entering a name, choosing a plan, and clicking Get started:

Next, create a project by clicking + Create project:

Name your project and select a deployment region:

Once your project has been created, you’ll be directed to the project Overview tab. In the Add a platform section, click the Web button to add a platform for the client-side Bryntum Gantt React app that you’ll connect with this Appwrite project:

Set the Name of the client-side app to Bryntum Gantt app and the Hostname to localhost.

You can skip the optional steps for installing and using the Appwrite SDK in a web app, as we’ll install it later in this guide.

Creating a database

In the Appwrite console, open the Databases page from the navigation menu and click the + Create database button. Name your database:

Each database contains a group of document containers called collections. Each collection contains documents that are identical in structure. Although Appwrite uses the SQL database, MariaDB, it uses the NoSQL database terms, collections and documents, because the Appwrite JSON REST API resembles a traditional NoSQL database API.

The Bryntum Gantt component will store and link data stores in its project. The Bryntum Gantt has the following types of data stores:

Let’s create Appwrite database collections for the task and dependency stores.

Creating a tasks collection

Create a collection for Bryntum Gantt tasks by clicking the + Create collection button and naming the collection tasks.

Open the Attributes tab and create the following attributes, which represent most of the TaskModel fields for a Bryntum Gantt task:

Attribute KeyTypeSizeMinMaxDefaultDefault valueRequiredArray
parentIdString50nullfalsefalse
nameString50truefalse
startDateDatetimenullfalsefalse
endDateDatetimenullfalsefalse
effortFloatnullfalsefalse
durationFloatnullfalsefalse
percentDoneFloat01000falsefalse
noteString1000nullfalsefalse
constraintDateDatetimenullfalsefalse
manuallyScheduledBooleantruefalsefalse
ignoreResourceCalendarBooleanfalsefalsefalse
effortDrivenBooleanfalsefalsefalse
inactiveBooleanfalsefalsefalse
clsString100nullfalsefalse
iconClsString100nullfalsefalse
colorString100nullfalsefalse
expandedBooleanfalsefalsefalse
calendarIntegernullfalsefalse
deadlineDatetimenullfalsefalse
draggableBooleantruefalsefalse
resizableBooleantruefalsefalse
orderedParentIndexIntegernullfalsefalse
parentIndexIntegernullfalsefalse
baselinesString100nullfalsetrue
delayFromParentFloatnullfalsefalse
unscheduledBooleanfalsefalsefalse
segmentsString100nullfalsetrue

Create these Enum attributes:

Attribute KeyTypeElementsDefault valueRequiredArray
effortUnitEnummillisecond, second, minute, hour, day, week, month, quarter, yearhourfalsefalse
durationUnitEnummillisecond, second, minute, hour, day, week, month, yeardayfalsefalse
schedulingModeEnumNormal, FixedDuration, FixedEffort, FixedUnitsNormalfalsefalse
constraintTypeEnumfinishnoearlierthan, finishnolaterthan, mustfinishon, muststarton, startnoearlierthan, startnolaterthannullfalsefalse
directionEnumForward, Backwardnullfalsefalse
projectConstraintResolutionEnumhonor, ignore, conflicthonorfalsefalse

Next, we’ll ensure that only Appwrite users can perform CRUD operations on this collection by adding a role:

Go to the Documents tab and click the + Create document button to create an example task:

Creating a dependencies collection

To create a collection for Bryntum Gantt dependencies, navigate back to the bryntum-gantt database page via the breadcrumbs navigation at the top of the page, click + Create collection, and name the collection dependencies.

Open the Attributes tab and create the following attributes, which represent most of the DependencyModel fields for a Bryntum Gantt dependency:

Attribute KeyTypeSizeMinMaxDefaultDefault valueRequiredArray
clsString50nullfalsefalse
lagFloat0truefalse
activeBooleantruefalsefalse
fromEventString50truefalse
toEventString50truefalse

Create these Enum attributes:

Attribute KeyTypeElementsDefault valueRequiredArray
lagUnitEnumms, s, m, h, d, w, M, y, daydfalsefalse
typeEnum0, 1, 2, 32falsefalse
fromSideEnumtop, left, bottom, rightnullfalsefalse
toSideEnumtop, left, bottom, rightnullfalsefalse

The fromEvent and toEvent attributes store the id values of tasks connected to the dependency as strings.

Create a role to ensure that only Appwrite users can perform CRUD operations on this collection:

Creating an Appwrite function REST API

You can extend Appwrite’s capabilities with Appwrite functions. These server-side serverless functions are managed by Appwrite, so you don’t need to worry about server maintenance or scaling.

Appwrite functions can be triggered by various events, including HTTP requests, SDK methods, server events, and scheduled executions. Each function has a unique URL, configurable environment variables and permissions, and an isolated container in which it is executed.

Let’s build and deploy a Node.js Appwrite function to serve as a REST API for the Bryntum Gantt component.

Creating a Node.js Appwrite function

In the Appwrite console, open the Functions page and click the + Create function button.

Connect to your GitHub account, grant Appwrite access to one or all of your repositories, and then select the Node.js starter template in the Quick start section.

Name your function Bryntum Gantt REST API and select Node.js – 18.0 as the Runtime:

In the Permissions step, ensure that anyone can execute a function using its domain by permitting the following for each section:

Note: Later, we’ll add CRUD operations to the function and set up JWT authentication so that only registered users can perform the CRUD operations.

In the Deployment step, select the Connect with Git option. Depending on the GitHub access permissions you granted Appwrite, choose Create a new repository or Add to existing repository.

Leave the Root directory input empty.

Once your Appwrite function repository has been created or added to an existing repo, you’ll see your created function:

You can see the GitHub repository link and the domain URL for the function in the Active card.

Below the Active card, you’ll see the All deployments card, which shows the deployment. The function automatically deploys when the GitHub repo changes. Each deployment builds the function, as can be seen in the build logs:

In the GitHub repository src folder, there is a main.js file. This main.js file defines the template function that receives a request and sends back a response, as described in the repository’s README file.

We’ll trigger the Node.js function by sending HTTP requests to the function’s domain URL.

Note: You can also execute the function from the Appwrite console by clicking the Execute button at the bottom-right of the Active card:

Creating a local copy of the Appwrite function

Clone your Appwrite function repo so you can work on it locally, replacing <YOUR_GITHUB_USERNAME> and <appwrite_bryntum_gantt_rest_api> with your own values:

git clone git@github.com:<YOUR_GITHUB_USERNAME>/<appwrite_bryntum_gantt_rest_api>.git

Install the dependencies, which include the Appwrite Node.js SDK for enabling Appwrite integration from your Node.js server-side code:

npm install

Any changes that you push to the GitHub repo will trigger a new deployment of the function.

Fetching data from Appwrite

Replace the code in the src/main.js file with the following lines of code:

import { Client, Databases, Query } from 'node-appwrite';
const PROJECT_ID = process.env.PROJECT_ID,
    DATABASE_ID = process.env.DATABASE_ID,TASKS_COLLECTION_ID = process.env.TASKS_COLLECTION_ID,
    DEPENDENCIES_COLLECTION_ID = process.env.DEPENDENCIES_COLLECTION_ID;

We’ll use function environment variables to store the IDs that we’ll use to access our tasks and dependencies collections.

The environment variables in a function are accessed via the runtime language’s system library. In Node.js, they are saved as process.env.

To create function environmental variables, navigate to your function in the Appwrite console and then open the Settings tab. Scroll down to the Environment variables card to create the following environment variables:

PROJECT_ID
DATABASE_ID
TASKS_COLLECTION_ID
DEPENDENCIES_COLLECTION_ID

You can find the PROJECT_ID value on the Overview page, next to your project title.

The Databases page includes a table of all the databases. You can find the DATABASE_ID value in the Database ID column.

Click on a row in the database table to view a table of that database’s tasks and dependencies collections. You can find the COLLECTION_ID value in the Collection ID column of the collections table.

In your local copy of the Appwrite function, add the following lines of code to the src/main.js file:

export default async ({ req, res }) => {
    const client = new Client()
        .setEndpoint('https://cloud.appwrite.io/v1')
        .setProject(PROJECT_ID)
        .setJWT(req.headers['authorization']);
    const databases = new Databases(client);
    // The Appwrite function code in the next sections goes here 
};

This initializes the Appwrite client SDK, sets the endpoint and project, and passes in a secret JSON Web Token (JWT). When you send requests, they are authenticated using JWT authentication and the JWT is added to the authorization header. This ensures that only logged-in users can view or change data.

When a user logs in, a session is created using the Appwrite client SDK. The session is used to generate a client-side JWT that is added to the authorization header when requests are made to the function.

The client is used to create a Databases instance so that the tasks and dependencies collections of databases can be modified via the function.

Add the following lines of code below the databases variable:

if (req.method === 'OPTIONS') {
        return res.send('', 200, {
            'Access-Control-Allow-Origin': 'http://localhost:3000',
            'Access-Control-Allow-Methods': 'POST, GET, OPTIONS',
            'Access-Control-Allow-Headers': 'Content-Type, Authorization',
        })
}

This code handles preflight (OPTIONS) requests.

We’ll make requests to this function from a React app that runs on a different origin, http://localhost:3000, so we need to handle Cross-Origin Resource Sharing (CORS). Although CORS headers are often set using res.setHeader(), the way Appwrite handles responses means that the res.setHeader() header doesn’t work in Appwrite serverless functions. Instead, we’ll need to directly include CORS headers in the response returned by the function.

Add the following lines of code below the req.method === 'OPTIONS' if block:

if (req.method === 'GET') {
        try {
                const tasksResponse = await databases.listDocuments(
                        DATABASE_ID,
                        TASKS_COLLECTION_ID,
                        [Query.orderAsc('parentIndex')]
                );
                const tasks = tasksResponse.documents.map((task) => {
                        task.id = task.$id;
                        // Remove Appwrite-specific fields and fields that are null or undefined.
                        const obj = Object.fromEntries(Object.entries(task).filter(([_, v]) => v != null ).filter(([k, _]) => k[0] != '$'));
                        return obj;
                });
                const dependenciesResponse = await databases.listDocuments(
                        DATABASE_ID,
                        DEPENDENCIES_COLLECTION_ID
                );
                const dependencies = dependenciesResponse.documents.map((dep) => {
                        dep.id = dep.$id;
                        // Remove Appwrite-specific fields and fields that are null or undefined.
                        const obj = Object.fromEntries(Object.entries(dep).filter(([_, v]) => v != null).filter(([k, _]) => k[0] != '$'));
                        return obj;
                });
                return res.json({
                        success : true,
                        tasks   : {
                                rows : tasks
                        },
                        dependencies : {
                                rows : dependencies
                        }
                }, 200, {
                        'Access-Control-Allow-Origin': 'http://localhost:3000',
                });
        } 
        catch(err) {
                return res.json({
                        success : false,
                        message : 'Tasks and dependencies could not be loaded'
                }, 500, {
                        'Access-Control-Allow-Origin': 'http://localhost:3000',
                });
        } 
}

When an authorized GET request is made to this function, the tasks and dependencies data is fetched and returned using the load response structure expected by a Bryntum Gantt.

The databases.listDocuments method gets a list of the documents in the tasks and dependencies collections. The returned tasks are arranged according to their parentIndex so they’ll be displayed in the correct order in the Bryntum Gantt.

Commit the changes and push them to your remote repository:

git add .
git commit -m "add: load tasks and dependencies"
git push origin

In this guide, we need to push changes to the remote repository to test our function. However, Appwrite recently introduced local function development, which allows you to build and test your functions locally without needing to deploy them. To run functions locally, you need to have the latest version of the Appwrite CLI installed on your machine. The CLI checks that you have Docker installed; if you don’t, it provides you with the necessary installation instructions. Local development uses Docker to replicate your production environment.

Syncing data changes to Appwrite

Let’s handle POST requests from Bryntum Gantt sync requests.

When the Bryntum Gantt sends a sync request, it includes the changes for all the linked data stores in a single request with a specific sync request structure.

The response to a sync request also needs to have a specific sync response structure. If a record was created in Appwrite, the response sends the new record id to the client-side Bryntum Gantt. This ensures the Bryntum Gantt app has the correct id for the record.

Add the following import to the code at the top of the src/main.js file:

import { ID } from 'node-appwrite';

Add the following lines of code below the req.method === 'GET' if block:

if (req.method === "POST") {
        const { requestId, tasks, dependencies } = req.body;
        try {
                const response = { requestId, success : true };
                // If task changes are passed.
                if (tasks) {
                        const rows = await applyTableChanges('tasks', tasks);
                        // If there is new data to update the client.
                        if (rows) {
                                response.tasks = { rows };
                        }
                }
                // If dependency changes are passed.
                if (dependencies) {
                        const rows = await applyTableChanges('dependencies', dependencies);
                        // If there is new data to update the client.
                        if (rows) {
                                response.dependencies = { rows };
                        }
                }
                return res.json(response, 200, {
                        'Access-Control-Allow-Origin': 'http://localhost:3000',
                });
        }
        catch(err) {
                return res.json({
                        requestId,
                        success : false,
                        message : 'There was an error syncing the data changes'
                }, 500, {
                    'Access-Control-Allow-Origin': 'http://localhost:3000',
                });
        }
}

When there are data changes in the Bryntum Gantt, the Gantt sends JSON data in POST requests to this Appwrite function to ensure the frontend and Appwrite database data stay in sync.

The request body is then parsed to determine which of the data stores in the project data have been changed.

The applyTableChanges helper function is called for each data store that has been changed. The updated data records are passed in as the second argument of the applyTableChanges function.

Define the applyDataChanges function above the req.method === 'OPTIONS' if block:

async function applyTableChanges(table, changes) {
        let rows;
        if (changes.added) {
                rows = await createOperation(changes.added, table);
        }
        if (changes.removed) {
            await deleteOperation(changes.removed, table);
        }
        if (changes.updated) {
                await updateOperation(changes.updated, table);
        }
        // New task or dependency ids to send to the client.
        return rows;
}

This helper function checks whether the change was the result of an added, removed, or updated operation, and calls the appropriate CRUD operation helper function to update the Appwrite database collections.

Define the createOperation helper function above the applyTableChanges function:

function createOperation(added, table) {
        return Promise.all(
                added.map(async(record) => {
                        const { $PhantomId, ...data } = record;
                        let id;
                        if (table === 'tasks') {
                                id = await addTask(data);
                        }
                        if (table === 'dependencies') {
                                id = await addDependency(data);
                        }
                        // Report to the client that the record identifier has been changed
                        return { $PhantomId, id };
                })
        );
}

This maps through the array of added records and calls the addTask or addDependency helper function.

The phantom identifier, $PhantomId, is a unique, autogenerated client-side value used to identify the record.

Note: You should not allow phantom identifiers to persist in your database. You can read more about phantom identifiers in the Sync request structure section of the Bryntum Crud Manager guide.

Define the deleteOperation function below the createOperation function:

function deleteOperation(deleted, table) {
        return Promise.all(
                deleted.map(({ id }) => {
                        if (table === 'tasks') {
                                removeTask(id);
                        }
                        if (table === 'dependencies') {
                                removeDependency(id);
                        }
                })
        );
}  

This maps through the array of deleted records and calls the removeTask or removeDependency helper function.

Now, define the updateOperation function below the deleteOperation function:

function updateOperation(updated, table) {
        return Promise.all(
                updated.map(({ $PhantomId, id, ...data }) => {
                        if (table === 'tasks') {
                                updateTask(id, data);
                        }
                        if (table === 'dependencies') {
                                updateDependency(id, data);
                        }
                })
        );
}

This maps through the array of updated records and calls the updateTask or updateDependency helper function.

Let’s define the rest of the helper functions.

Add the following addTaskand addDependency functions below the updateOperation function:

async function addTask(task) {
        const { $id } = await databases.createDocument(
                DATABASE_ID,
                TASKS_COLLECTION_ID,
                ID.unique(),
                task
        );
        return $id;
}
async function addDependency(dependency) {
        dependency.type = `${dependency.type}`;
        delete dependency.from;
        delete dependency.to;
        const { $id } = await databases.createDocument(
                DATABASE_ID,
                DEPENDENCIES_COLLECTION_ID,
                ID.unique(),
                dependency
        );
        return $id; 
}

The createDocument method creates a new task or dependency document in the database collection.

Add the following removeTask and removeDependency functions below the addDependency function:

async function removeTask(id) {
        await databases.deleteDocument(DATABASE_ID, TASKS_COLLECTION_ID, id);
}
async function removeDependency(id) {
        await databases.deleteDocument(DATABASE_ID, DEPENDENCIES_COLLECTION_ID, id);
}

The deleteDocument method deletes a task or dependency.

Add the following updateTask and updateDependency functions below the removeDependency function:

async function updateTask(id, task) {
        await databases.updateDocument(DATABASE_ID, TASKS_COLLECTION_ID, id, task);
}
async function updateDependency(id, dependency) {
        await databases.updateDocument(DATABASE_ID, DEPENDENCIES_COLLECTION_ID, id, dependency);
}

The updateDocument method updates a task or dependency.

Commit your changes and push them to your repository.

Now let’s create the Bryntum Gantt React app that will use the Appwrite function to connect with our Appwrite database.

Creating the frontend Bryntum Gantt React app

We’ll use the React Gantt Chart with Bryntum and Appwrite starter repo as our app template.

Follow the steps in the README file to install the dependencies.

Initializing the Appwrite web SDK

Create a .env file containing a VITE_PROJECT_ID environmental variable with your Appwrite project id:

VITE_PROJECT_ID=<YOUR_PROJECT_ID>

Add .env to your .gitignore file:

.env

Create a lib folder in the src folder. In the lib folder, create a appwrite.ts file containing the following lines of code:

import { Account, Client, Databases } from 'appwrite';
const client = new Client();
client
    .setEndpoint('https://cloud.appwrite.io/v1')
    .setProject(import.meta.env.VITE_PROJECT_ID);
export const account = new Account(client);
export const databases = new Databases(client);

This initializes the web client that handles requests to Appwrite, sets the endpoint to the Appwrite cloud URL, and sets the project using the VITE_PROJECT_ID environmental variable.

It then creates an Account instance, which can be used to modify Appwrite user data, and a Databases instance, which can be used to modify the tasks and dependencies collections of Appwrite databases.

Adding authentication

Let’s add the basic email authentication used to register new Appwrite users and log existing users in to the Bryntum Gantt React app. We won’t add email verification or set up password recovery for this app.

Handling user authentication and passing user data to app components

First, we’ll create a custom React hook to handle user authentication and use React context to share user data and authentication methods with our app components.

In the lib folder, create a context folder and a user.tsx file within it.

We’ll add imports, Typescript interfaces, a user context, a custom useUser hook, a user context provider, a login, logout, and register function, and a refresh functionality to the src/lib/context/user.tsx file.

First, add the following imports:

import { ID, Models } from 'appwrite';
import { createContext, useContext, useEffect, useState } from 'react';
import { account } from '../appwrite';

Add the following TypeScript interfaces to define the value of the React user context (created later in the guide) and the Appwrite error type:

interface IUserContext {
    current: Models.User<Models.Preferences> | null;
    login: (email: string, password: string) => Promise<void>;
    logout: () => Promise<void>;
    register: (email: string, password: string) => Promise<void>;
    error: string | null;
    setError: (error: string | null) => void;
    isLoading: boolean;
}
interface AppwriteError {
    type: string;
    message: string;
}

Add a user context and a custom useUser hook so that you can access the user’s data and authentication methods in the user context from any component:

const UserContext = createContext<IUserContext | null>(null);
export function useUser() {
    return useContext(UserContext);
}

Add the following user context provider:

export function UserProvider({ children }: { children: React.ReactNode }) {
    const [user, setUser] = useState<Models.User<Models.Preferences> | null>(null),
        [error, setError] = useState<string | null>(null),
        [isLoading, setIsLoading] = useState<boolean>(true);
    // Initial session check.
    useEffect(() => {
        async function initSession() {
            try {
                const currentUser = await account.get();
                if (currentUser) {
                    setUser(currentUser);
                }
            }
            catch (err) {
                setUser(null);
            }
            finally {
                setIsLoading(false);
            }
        };
        initSession();
    }, []); // Run once on mount.
    return (
        <UserContext.Provider value={{
            current : user,
            login,
            logout,
            register,
            error,
            setError,
            isLoading
        }}>
            {children}
        </UserContext.Provider>
    );
}

This initializes the user, error, and isLoading states. On page load, the effect uses the account.get method to check whether a logged-in user is in the current browser. If a logged-in user is detected, the user state variable is populated with the user data. The user data and authentication methods are added to the value prop of the context provider.

Add the following login function below the state variable declarations in the UserProvider function:

async function login(email: string, password: string) {
        try {
                setIsLoading(true);
                await account.createEmailPasswordSession(email, password);
                const loggedInUser = await account.get();
                setUser(loggedInUser);
                setError(null);
                window.location.replace('/');
        }
        catch (err) {
                const error = err as Error;
                setError(error.message || 'An error occurred');
        }
        finally {
                setIsLoading(false);
        }
}

The account.createEmailPasswordSession method allows each user to log in to their account with their email and password. A successful login then sets the user state and reloads the page.

Add the following logout function below the login function:

async function logout() {
        try {
                await account.deleteSession('current');
        }
        catch (err) {
                console.error('Logout error:', err);
        }
        finally {
                setUser(null);
                setIsLoading(false);
        }
}

The account.deleteSession method logs users out of the app.

Add the following register function below the logout method:

async function register(email: string, password: string) {
        try {
                setIsLoading(true);
                await account.create(ID.unique(), email, password);
                await login(email, password);
        }
        catch (err) {
                const error = err as Error;
                setError(error.message || 'An error occurred');
                setIsLoading(false);
        }
}

The account.create method registers new users.

To make authenticated requests to our Appwrite Bryntum Gantt REST API function, we need to create a JWT using the logged-in user’s session.

Add the following lines of code to the try block of the login and in the if (currentUser) block of the initSession functions:

const accountJWT = await account.createJWT();
sessionStorage.setItem('accountJWT', accountJWT.jwt);

The account.createJWT method creates a JWT for the logged-in user. The JWT secret is valid for 15 minutes or until the user logs out. The JWT is saved in session storage, where it can be accessed for making requests to the Appwrite function.

Add the following line of code to the finally block of the logout function to remove the JWT from session storage once the user has logged out:

sessionStorage.removeItem('accountant');

The JWT is valid for 15 minutes, but the user session is valid for 365 days by default. You can adjust the session length in the Settings tab, found on your project Auth page in the Appwrite console .

Adding refresh functionality

Let’s add refresh functionality so that our JWT token doesn’t become invalid.

Insert the following variable to the top of the src/lib/context/user.tsx file:

const REFRESH_INTERVAL = 14 * 60 * 1000; // 14 minutes

This refreshes the JWT every 14 minutes.

Finally, add the following effect to the UserProvider component:

useEffect(() => {
        if (!user) return; // Don't set up refresh for logged-out users.
        let refreshInterval: number | null = null;
        const refreshToken = async() => {
                try {
                        // First, verify the session is still valid.
                        await account.get();
                        // If the session is valid, refresh the JWT.
                        const accountJWT = await account.createJWT();
                        sessionStorage.setItem('accountJWT', accountJWT.jwt);
                }
                catch (err) {
                        const error = err as any;
                        // Log the user out if there is an unauthorized-scope or session-not-found error.
                        if (error?.type === 'general_unauthorized_scope' ||
                                error?.type === 'user_session_not_found') {
                                if (refreshInterval) {
                                        clearInterval(refreshInterval);
                                }
                                await logout();
                        }
                }
        };
        const handleVisibilityChange = () => {
                if (document.visibilityState === 'visible') {
                        refreshToken();
                }
        };
        // Set up the refresh interval.
        refreshInterval = window.setInterval(refreshToken, REFRESH_INTERVAL);
        // Add the visibility change listener.
        document.addEventListener('visibilitychange', handleVisibilityChange);
        // Cleanup
        return () => {
                if (refreshInterval) {
                        clearInterval(refreshInterval);
                }
                document.removeEventListener('visibilitychange', handleVisibilityChange);
        };
}, [user]); // Only reset the refresh logic when the user changes.

The document.visibilityState property calls the refreshToken function every 14 minutes or when the user returns to the browser tab.

When the user’s session expires, they are logged out and the JWT is removed from session storage.

Wrapping the app with the user provider

Now, let’s wrap our app with the user provider we created.

Replace the code in the src/App.tsx file with the following lines of code:

import { UserProvider } from './lib/context/user';
function App() {
    return (
        <UserProvider>
            <div>TODO</div>
        </UserProvider>
    );
}
export default App;

This allows us to access the context value from any component in the app.

Adding an app navigation bar, login page, and homepage

Let’s create a navigation bar, login page, and homepage for our app. The homepage will house the Bryntum Gantt component.

Adding a Layout component with a navigation bar

Create a components folder within the src folder. Then, create a Layout.tsx file in the components folder and add the following lines of code to it:

import { Outlet } from 'react-router-dom';
import { useUser } from '../lib/context/user';
function Layout() {
    const user = useUser();
    return (
        <div id="app">
            <nav>
                <div>
                    {user?.current
                        ? (
                            <div className='logged-in-items'>
                                <span><b>Logged in as:</b> {user.current.email}</span>
                                <button
                                    type="button"
                                    onClick={() => user.logout()}
                                    disabled={user.isLoading}
                                >
                                Log Out
                                </button>
                            </div>
                        )
                        : (
                            <div className='logged-out-item'>
                            Using Bryntum Gantt with Appwrite
                            </div>
                        )}
                </div>
            </nav>
            <main>
                <Outlet />
            </main>
        </div>
    );
}
export default Layout;

This wraps the Layout component around the login and homepage, adding a navigation bar to the top of both pages. The Layout component uses the React Router Outlet component to render the child route elements that become the pages and uses the useUser custom hook to get the user’s email, which it displays alongside a Logout button in the navigation bar when a user is logged in to the app.

Adding a login page

Create a pages folder in the src folder. In the pages folder, create a Login.tsx file containing the following lines of code:

import { useEffect, useState } from 'react';
import { useUser } from '../lib/context/user';
import { useNavigate } from 'react-router-dom';
function Login() {
    const user = useUser();
    const navigate = useNavigate();
    useEffect(() => {
        if (user?.current) {
            navigate('/');
        }
    }, [user?.current]);
    const [email, setEmail] = useState('');
    const [password, setPassword] = useState('');
    return (
        <div className='login-form-container'>
            <form>
                <fieldset disabled={user?.isLoading}>
                    <input
                        type="email"
                        placeholder="Email"
                        value={email}
                        onChange={(event) => {
                            user?.setError(null);
                            setEmail(event.target.value);
                        }}
                        autoComplete='email'
                    />
                    <input
                        type="password"
                        placeholder="Password"
                        value={password}
                        onChange={(event) => {
                            user?.setError(null);
                            setPassword(event.target.value);
                        }}
                    />
                    <div className='button-container'>
                        <button
                            className="button"
                            type="button"
                            onClick={() => user?.login(email, password)}
                        >
                        Login
                        </button>
                        <button
                            className="button"
                            type="button"
                            onClick={() => user?.register(email, password)}
                        >
                        Register
                        </button>
                    </div>
                    {user?.error ? <p className='error-message'>{user.error}</p> : <p className='error-message'></p>}
                </fieldset>
            </form>
        </div>
    );
}
export default Login;

When a user is logged in, this renders a login form; when no user is logged in, it redirects the user to the '/' route. The register and login functions from the users hook are used to register and log in users.

Adding a homepage

Create a Home.tsx file in the src/pages folder and add the following lines of code to it:

import { useUser } from '../lib/context/user';
function Home() {
    const user = useUser();
    return (
        <div className="home-page">
            {user?.current
                ? (
                    <div>TODO: add Bryntum Gantt</div>
                )
                :
                user?.isLoading ?
                    (
                        <div className="loader-container">
                            <div className="loader"></div>
                        </div>
                    ) :
                    (
                        <p>
                            Please <a href="/login">login or register</a> to view the Bryntum Gantt chart
                        </p>
                    )
            }
        </div>
    );
}
export default Home;

This displays placeholder text for the Bryntum Gantt when a user is logged in and a link to the login page when no user is logged in.

Creating routes with React Router

Replace the code in the src/App.tsx file with the following lines of code:

import { UserProvider } from './lib/context/user';
import  Login from './pages/Login';
import  Home from './pages/Home';
import { BrowserRouter, Route, Routes } from 'react-router-dom';
import Layout from './components/Layout';
function App() {
    return (
        <UserProvider>
            <BrowserRouter>
                <Routes>
                    <Route element={<Layout />}>
                        <Route
                            index
                            element={<Home />}
                        />
                        <Route
                            path="login"
                            element={<Login />}
                        />
                        <Route
                            path="*"
                            element={
                                <div>
                                    Not Found
                                </div>
                            }
                        />
                    </Route>
                </Routes>
            </BrowserRouter>
        </UserProvider>
    );
}
export default App;

This uses React Router for navigation and configures the '/' route to display the homepage and the "/login" route to display the login page.

Adding CSS styling

Add the following CSS styles to the src/index.css file for styling the navigation and pages:

nav {
  padding: 1rem;
  border-bottom: 1px solid #e0e0e7;
  background-color: #0076f8;
  color: #ffffff;
}
nav .logged-in-items {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
}
nav .logged-out-item {
  padding : 0.6rem 0;
}
button {
  border-radius: 3px;
  text-transform: uppercase;
  border: 1px solid transparent;
  padding: 0.6rem 1.2rem;
  font-size: 1em;
  font-weight: 500;
  font-family: inherit;
  background-color: #27ca37;
  color: #f9f9f9;
  cursor: pointer;
  transition: background-color 0.3s;
}
button:hover {
  border-color: #27ca37;
  background-color: #27ca37;
}
button:focus,
button:focus-visible {
  outline: 4px auto -webkit-focus-ring-color;
}
button:disabled {
  background-color: #e0e0e7;
  color: #a0a0a7;
  cursor: not-allowed;
}
a {
  font-weight: 500;
  color: #646cff;
  text-decoration: inherit;
}
a:hover {
  color: #747bff;
}
.loader-container {
  display: flex;
  justify-content: center;
  align-items: center;
}
.loader {
  display: inline-block;
  width: 50px;
  height: 50px;
  border: 3px solid black;
  border-radius: 50%;
  border-top-color: #fff;
  animation: spin 1s ease-in-out infinite;
}
@keyframes spin {
  to { transform: rotate(360deg); }
}
@-webkit-keyframes spin {
  to {transform: rotate(360deg); }
}
.login-form-container {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 1rem;
  height: 100%;
}
.login-form-container form {
  width: 100%;
  max-width: 300px;
}
.login-form-container form fieldset {
  /* remove default styling */
  margin-inline-start: 0;
  margin-inline-end: 0;
  padding-block-start: 0;
  padding-block-end: 0;
  padding-inline-start: 0;
  padding-inline-end: 0;
  border: none;
  min-inline-size: min-content;
  display: flex;
  flex-direction: column;
  gap: 1.2rem;
}
.login-form-container input {
  font-family: inherit;
  font-size: 1rem;
  font-weight: 500;
  padding: 0.6rem 1rem;
  border-radius: 8px;
  border: 1px solid #e0e0e7;
}
.login-form-container .button-container {
  display: flex;
  gap: 1rem;
}
.error-message {
  height: 50px;
  font-size: 0.8rem;
  color: #ff4d4f;
}
.home-page {
  display : flex;
  align-items : center;
  justify-content : center;
  height : 100%;
  width : 100vw;
}

Run the React application and open http://localhost:3000 to see the homepage with a link to the login page. Navigate to the login page and register an account:

You’ll see the placeholder text, “TODO: add Bryntum Gantt.”

Notice that the form shows a message from Appwrite if there are any login or registration errors.

Let’s add a Bryntum Gantt to the homepage of a logged-in user.

Adding a Bryntum Gantt to the homepage

We’ll first install the Bryntum Gantt React component, then create a Bryntum Gantt component and render it on the homepage of logged-in users.

Installing the Bryntum React component

To install the Bryntum Gantt using npm, first need to follow the Bryntum npm repository guide to access the private Bryntum npm registry. Once you’ve logged in to the registry, you can install the Bryntum Gantt component.

If you have a Bryntum Gantt license, install the Gantt with the following command:

npm install @bryntum/gantt @bryntum/gantt-react

If you are trying out the component, install the trial version:

npm install @bryntum/gantt@npm:@bryntum/gantt-trial @bryntum/gantt-react

Creating a Bryntum Gantt config file

Create a ganttConfig.ts file in the src/lib folder and add the following lines of code to it:

import { BryntumGanttProps } from '@bryntum/gantt-react';
const ganttProps : BryntumGanttProps = {
    weekStartDay : 1,
    viewPreset   : 'weekAndDayLetter',
    columns      : [{ type : 'name', field : 'name', width : 250 }]
};
export { ganttProps };

This sets the weekStartDay to 1 so that the week starts on Monday in the Bryntum Gantt.

Creating the Bryntum Gantt component

In the src/components folder, create a Gantt.tsx file containing the following lines of code:

import { BryntumGantt } from '@bryntum/gantt-react';
import { useRef } from 'react';
import { ganttProps } from '../lib/ganttConfig';
function Gantt() {
    const ganttRef = useRef<BryntumGantt>(null);
    return (
        <BryntumGantt
            ref={ganttRef}
            {...ganttProps}
        />
    );
}
export default Gantt;

This passes in the ganttProps to the Bryntum Gantt React component and uses the ganttRef to provide access to the Bryntum Gantt instance.

Note: We won’t use the ganttRef in this guide, but accessing the Bryntum Gantt instance can be useful in other situations.

In the src/pages/Home.tsx file, import the Gantt component:

import Gantt from '../components/Gantt';

Replace the placeholder text rendered when a user logs in to the homepage with the Gantt component:

- <div>TODO: add Bryntum Gantt</div>
+ <Gantt />

Import the Bryntum Gantt Stockholm theme in the src/index.css file:

@import "@bryntum/gantt/gantt.stockholm.css";

This theme is one of five available themes.

Run the React application and open http://localhost:3000. You should see an empty Bryntum Gantt chart:

Now let’s connect the Bryntum Gantt to our Appwrite function.

Connecting to the Appwrite function via the Bryntum Gantt Crud Manager

In the src/lib/ganttConfig.ts file, add the following project property to the ganttProps:

project      : {
        taskStore : {
                transformFlatData : true
        },
        transport : {
                load : {
                        url : import.meta.env.VITE_PROJECT_FUNCTION_DOMAIN_URL,
                        method      : 'GET',
                        headers     : header,
                        credentials : 'omit'
                },
                sync : {
                        url : import.meta.env.VITE_PROJECT_FUNCTION_DOMAIN_URL,
                        method      : 'POST',
                        headers     : header,
                        credentials : 'omit'
                }
        },
        autoLoad         : true,
        autoSync         : true,
        // This config enables response validation and dumps found errors into the browser console.
        // It's only meant to be used as a development-stage helper, so please set it to false for production.
        validateResponse : true
}

Add this header variable to the top of the src/lib/ganttConfig.ts file:

const header = {
    'Content-Type'  : 'application/json',
    'Authorization' : `${sessionStorage.getItem('accountJWT')}`
};

Add your Appwrite function’s domain URL to the .env file:

VITE_PROJECT_FUNCTION_DOMAIN_URL=<PROJECT_FUNCTION_DOMAIN_URL>

The Bryntum Gantt project has a Crud Manager that simplifies loading data from and syncing data changes to a server. The Crud Manager uses the Fetch API as a transport system and uses JSON as the encoding format.

The code in the src/lib/ganttConfig.ts file sets the loadUrl and syncUrl to the Appwrite domain URL and authenticates requests by passing in the JWT (created from the user’s session) in the authorization header.

The transformFlatData Boolean is set to true to automatically transform flat data (loaded from a data store) into the expected tree data format. The data from the Appwrite tasks and dependencies collections has a flat structure; no child tasks are nested in parent tasks. Instead of using nested structures, tasks in Appwrite collections use a parentId property to determine the parent-child relationships between tasks.

Open your local development server, http://localhost:3000, to see that the Bryntum Gantt has now been populated with your example task.

You can create, update, and delete tasks and dependencies, and the changes will be synced to your Appwrite database:

Next steps

This guide provides a starting point for using a Bryntum Gantt with Appwrite. Take a look at the additional features you can add to a Bryntum Gantt on the examples page, such as:

We used basic email authentication for the Bryntum Gantt. You can improve it by adding email verification, password recovery, and other authentication methods such as phone (SMS), magic URL, and OAuth2.

Arsalan Khattak

Bryntum Gantt