Mats Bryntse
3 February 2023

How to build a Gantt chart into Microsoft Teams with Bryntum

Looking for a way to streamline your team’s project management process? Look no further! A Gantt chart is a powerful […]

Looking for a way to streamline your team’s project management process? Look no further! A Gantt chart is a powerful tool for visualizing and managing project timelines and it’s especially useful for teams working on complex projects: Team members can clearly see the progress of a project, identify potential roadblocks, and make necessary adjustments in real-time.

Integrating a Gantt chart into your team’s workflow in your Microsoft Teams account changes the game. Your team has easy access to the most up-to-date project information along with the collaboration tools of Microsoft Teams. No more relying on outdated spreadsheets and confusing project-management software.

In this tutorial, we’ll guide you through the process of setting up your own Bryntum Gantt application in Microsoft Teams and show you how to customize it to meet the specific needs of your team.

What you’ll need

We will work from starter code hosted on GitHub to create our app. You’ll need a few other tools too:

Set up a new blank app in Teams

To create a new blank app in Teams, you’ll need to first install the Microsoft Teams Toolkit. The Teams Toolkit provides several useful tools that help in the app development process, such as tools for provisioning and deployment of cloud resources and the ability to publish the app to the Teams store.

Install Teams Toolkit

Open VS Code and navigate to the Extensions tab.

Search for “Teams Toolkit” and select “Install” next to it.

Once installed, you’ll see the Teams Toolkit icon appear in the VS Code Activity Bar.

Check that sideloading is enabled

We will use the Teams Toolkit for previewing and testing our app, a process known as sideloading. You will need to have a Microsoft tenant with admin access or a Microsoft Developer account to proceed.

To check whether your account has a tenant with admin access, follow these steps:

1. Log in to Teams, select the Store icon, select “Manage your apps”, then select “Upload an app”.

2. If you see the option to “Upload a custom app”, then you can test and preview your app with this account.

If you do not have admin access, sign up for the Microsoft 365 developer program by following these steps:

1. Go to the Microsoft 365 Developer Program.

2. Select “Join Now” and follow the onscreen instructions.

3. In the welcome screen, select “Set up E5 subscription”.

4. Set up your administrator account. When you’re finished, you will be given a new email address (seen under “Administrator”) that you can use to log in to Teams with admin access to a tenant with sideloading enabled.

Now log in to the Teams Toolkit on VS Code with your admin account.

Clone starter code

The Teams Toolkit extension provides a number of template applications that you can experiment with.

To make setting up our project a little easier, clone the starter code for our Teams app here and open it in VS Code.

The starter code is a Teams tab template app with some extra folders already set up so we don’t have to do that in this tutorial.

There are a lot of files in our directory, most of which contain configuration for our tab app. We will focus on the files in the tab folder.

Write the frontend code (Bryntum)

To install Bryntum Gantt, open a terminal and navigate to the tabs folder with `cd tabs`.

Access the npm registry by following this step on the Bryntum website.

Now install a TypeScript Gantt template by running the following command from the tabs folder:

npx create-react-app my-app --template @bryntum/cra-template-typescript-gantt

You can preview the app by running these three commands from the terminal:

cd my-app
npm install
npm run start

You will be able to see the Gantt under http://localhost:3000. Your browser should automatically open it for you.

We will not be using this template in this tutorial. We have updated the package.json file in the tabs folder to include the dependencies we need for a Bryntum Gantt. If you open up package.json in the tabs folder, you will see the following under the dependencies section:

  "@bryntum/gantt": "npm:@bryntum/gantt-trial@5.2.8",
  "@bryntum/gantt-react": "5.2.8",

And the following in the devDependencies section:

  "babel-preset-react-app": "npm:@bryntum/babel-preset-react-app@10.0.0",

Compare these lines with the lines in the same sections of package.json in the my-app folder. If the lines differ, go ahead and replace these lines in the tabs version with the ones you have just installed as they will be the version you specified in your installation.

Now cd to your tabs folder and run `npm i` if you made any changes to package.json there. You should now have access to the Bryntum Gantt in your Teams tab starter code and we can get to building.

Set up the Gantt configuration

Navigate to the Gantt folder (find it in tabs/src/components/) and create a new file called GanttCofig.tsx. Paste the following code into the file:

import { GanttConfig, ProjectModel } from '@bryntum/gantt';
const project = new ProjectModel({
  transport: {
    load: {
      url: 'data/gantt-data.json',
    },
  },
  autoLoad: true,
});
const ganttConfig: Partial<GanttConfig> = {
  width: '100vw',
  height: '100vh',
  dependencyIdField: 'sequenceNumber',
  columns: [{ type: 'name', width: 250, text: 'Tasks' }],
  viewPreset: 'weekAndDayLetter',
  barMargin: 10,
  project,
};
project.load();
export { ganttConfig };

Here we set up the configuration for our Gantt chart. We start by importing the ProjectModel and GanttConfig objects from the @bryntum/gantt library.

Next we create an instance of the ProjectModel and configure it with a transport, which is used to specify the location of the data that will be used to populate the chart (in this case, the data is located in a file called gantt-data.json). The autoLoad property is set to true so that the data will be automatically loaded when the project is created.

Then we define the GanttConfig. This will help configure our Gantt chart with properties like the width and height of the chart, the field that will be used to identify dependencies between tasks, the columns that will be displayed in the chart, the view preset to be used (weekAndDayLetter), the margin between the bars, and the project object that was created earlier.

Finally, the load() method of the project object is called to load the data and create the chart. The ganttConfig object is then exported so that it can be used in other parts of the application. We will use ganttConfig to set up our final Gantt chart and to adjust its readOnly property depending on whether a logged-in Teams user has permission to edit the Gantt chart.

Create the Gantt component

Now create a new file called Gantt.tsx in the Gantt folder and paste the following code into it:

import React, { useRef, useEffect } from 'react';
import { BryntumGantt } from '@bryntum/gantt-react';
import { ganttConfig } from './GanttConfig';
import '../../styles/App.scss';
interface Props<T> {
  role: number;
}
export default function Gantt<T extends unknown>({ role }: Props<T>) {
  const gantt = useRef<BryntumGantt>(null);
  useEffect(() => {
    if (role === 0) {
      ganttConfig.readOnly = true;
    } else {
      ganttConfig.readOnly = false;
    }
  }, [role]);
  return <BryntumGantt ref={gantt} {...ganttConfig} />;
}

Here we create a React component that renders a Gantt chart using the Bryntum Gantt library for React.

First we make some imports from the React library and import the Bryntum Gantt library, the ganttConfig we created in the previous step, and some styling (which we will create in the next step).

We define the React component as a functional component with a generic type T and an interface Props with a role property. We then define a BryntumGantt component using the useRef hook and use the useEffect hook to listen to changes on the role prop.

The useEffect hook runs a callback function that checks the value of the role property. If the value is 0, it makes the Gantt read-only by assigning `ganttConfig.readOnly = true`, else it assigns `ganttConfig.readOnly = false`. This allows us to update the Gantt’s readOnly property to give a user editing permission based on their role in the Teams environment.

Finally, the BryntumGantt component is returned, configured with ganttConfig.

To make the Gantt component accessible throughout your project, create a file called index.tsx in the Gantt folder and paste the following code into it:

import Gantt from './Gantt';
export default Gantt;

We will use the Bryntum Stockholm theme to style our Gantt. Navigate to the styles folder in tabs/src/styles/ and create a new file called App.scss. Paste the following code into the new file:

// import bryntum theme
@import '~@bryntum/gantt/gantt.stockholm.css';
body,
html {
  margin: 0;
  display: flex;
  flex-direction: column;
  height: 100vh;
  font-family: Poppins, 'Open Sans', Helvetica, Arial, sans-serif;
  font-size: 14px;
}
#container {
  flex: 1 1 100%;
}

This theme is required to render the Bryntum Gantt correctly. Follow this link for more information on styling your Bryntum Gantt.

Access user’s Microsoft details with TeamsFX

We need to access the logged-in Teams user’s data in order to determine their role. We will use this information to turn off edit access to our Gantt for users who don’t have the necessary permissions.

First, create a file called AcmeGantt.tsx in the components folder. You can rename this file to match your company or app name, but remember to keep a lookout for imports in the tutorial code and update them accordingly.

Next, paste the following code into the AcmeGantt.tsx file:

import { Providers, ProviderState } from '@microsoft/mgt-element';
import { TeamsFxProvider } from '@microsoft/mgt-teamsfx-provider';
import { useGraph } from '@microsoft/teamsfx-react';
import React, { useState, useContext, useEffect } from 'react';
import { TeamsFxContext } from './Context';
import Gantt from '../components/Gantt';
export default function AcmeGantt() {
  const [role, setRole] = useState(0);
  return (
    <div>
      <Gantt role={role} />
    </div>
  );
}

Here we make some imports necessary for accessing the user’s Teams data, like the @microsoft/mgt-element and @microsoft/mgt-teamsfx-provider packages and the useGraph function from @microsoft/teamsfx-react.

Next we define our application AcmeGantt and declare a state variable called role (with an initial value of 0) using the useState hook. This role will be set using setRole after we have accessed the user’s data in the next step.

To get access to the user’s Teams data, paste the following code above the return of the AcmeGantt component:

const { teamsfx } = useContext(TeamsFxContext);
const dataGraph = useGraph(
  async (graph, teamsfx, scope) => {
    // Call graph api directly to get user profile information
    const profile = await graph.api('/me').get();
    // Initialize Graph Toolkit TeamsFx provider
    const provider = new TeamsFxProvider(teamsfx, scope);
    Providers.globalProvider = provider;
    Providers.globalProvider.setState(ProviderState.SignedIn);
    return { profile };
  },
  { scope: ['User.Read'], teamsfx: teamsfx }
);
useEffect(() => {
  dataGraph.reload();
}, []);

Here we define a useGraph hook that will retrieve data from the Microsoft Graph API.

The useGraph hook accepts a callback function that is executed when the component is rendered. Inside the callback function, it makes a direct call to the Microsoft Graph API to get the user’s profile information and assigns the response to the profile variable.

We then define a TeamsFxProvider class to provide authentication and API access to the Microsoft Graph API for Teams-specific functionality.

The callback function returns an object containing the profile variable with our app user’s Microsoft Teams data.

Finally, we define a useEffect hook to call the reload method of dataGraph when the component is rendered.

Now we need to render the AcmeGantt component again so that we can use the user’s Teams data to determine the readOnly configuration of the Bryntum Gantt chart. Paste the following code above the return of the AcmeGantt component:

useEffect(() => {
  let managerRole = 0;
  if (dataGraph.data) {
    if (dataGraph.data?.profile.jobTitle === 'Manager') {
      managerRole = 1;
    }
    setRole(managerRole);
  }
}, [dataGraph.data, setRole]);

Here we define a useEffect hook to run some logic whenever the component is rendered. The hook takes in the user data we received from dataGraph and the setRole function to update the role variable of our Gantt based on this data.

In this example, we will check if the user has a jobTitle of 'Manager' in their Microsoft Teams account. We will enable edit access if they do and restrict edit access if not. You can check the returned data and determine for yourself what data you would like to base these permissions on.

The useEffect hook declares a variable called managerRole, which is initially set to the value of 0. It then checks if the dataGraph.data variable exists. If it does, it checks if the jobTitle property of the profile object in dataGraph.data is equal to 'Manager'. If it is, it sets the managerRole variable to 1. This will trigger the setRole function, which will rerender the AcmeGantt component with an updated Gantt configuration.

Run your app

To run your app, you need to import it into the Tab.tsx file, which can be found in the pages folder.

To import your app, paste the following import at the top of the file (make sure to adjust the import based on your app’s name):

import AcmeGantt from '../components/AcmeGantt';

Then replace this comment:

{
  /* <Your app name here /> */
}

With an instance of your app:

<AcmeGantt />

Finally, navigate to the Teams Toolkit extension in VS Code and click “Preview your Teams app” in the “Development” section.

The app will start up your browser. Once you’ve logged in to Microsoft Teams, you will be prompted to add your app to your Microsoft Teams account. Click “Add”.

Your Teams tab app has been added to your account in testing mode and your Bryntum Gantt is rendering. If you cannot edit your Gantt, it may be because your profile does not have the jobTitle of manager at the moment. No worries, to fix this for development, find this line of code in AcmeGantt.tsx:

<Gantt role={role} />

And change it to:

<Gantt role={1} />

This will configure the app with edit access the next time you reload the page or app.

Save changes

Now you may notice that after a page refresh, any changes you make to your Gantt are reset. This is because the project data is not currently being stored and updated in a database. In the next section, we will configure our Gantt to communicate with a server that will update our tasks stored in a SQL database.

Write the backend code (API Bryntum)

To get our data from another source, we need to update the Gantt configuration file. Navigate to ganttConfig.tsx and replace the project definition with the following code:

const project = new ProjectModel({
  taskStore: {
    autoTree: true,
    transformFlatData: true,
  },
  // specify data source
  transport: {
    load: {
      url: 'http://localhost:8010/data',
    },
    sync: {
      url: 'http://localhost:8010/api',
    },
  },
  autoLoad: true,
  autoSync: true,
  validateResponse: true,
});

Here we have recreated the ProjectModel with new configuration.

First we create a taskStore for the project and set autoTree and transformFlatData to true to automatically transform the formatting of our data.

Next, the transport options are set to load data from a specified URL. This URL will be a route on the server we create in the next step. We use sync to specify the URL to which the project should send data when it is synced.

Finally, we set the autoLoad, autoSync, and validateResponse options to true to automatically load data, sync data, and validate responses from our server.

Now our Gantt is configured to fetch data from a server at http://localhost:8010/.

Create the server

Navigate to the server folder in the project’s root and run `npm install` in your terminal to install the packages we need to create our server.

Next, create a file called server.js in the server folder and paste the following code into it:

const express = require('express');
const bodyParser = require('body-parser');
const uuid = require('uuid');
const cors = require('cors');
require('dotenv').config();
const app = express();
const port = process.env.PORT || 8010;
app.use(
  cors({
    origin: 'https://localhost:53000',
    credentials: true,
  })
);
app.disable('x-powered-by');
app.use(bodyParser.json());
app.listen(port, () => {
  console.log('Server is running on port ' + port + '...');
});

Here we set up our Express server with some configuration for communicating with our Teams app. We use bodyParser because Bryntum sends request objects in JSON format. We import uuid to generate unique IDs and enable CORS (Cross-Origin Resource Sharing). We use app.disable('x-powered-by'); to hide the server’s technology stack from potential attackers.

Create the CRUD functions

Now we need to create the functions that will perform CRUD operations on our database and define the server routes those functions will use to interact with our database.

First define the serverConfig function, which will run when the server is started, to connect to the MySQL database. The serverConfig function has a helper function called sendResponse that formats the response of create, update, or delete operations from the Bryntum Gantt chart. The response format required is described in the Bryntum Gantt chart docs.

const mysql = require('mysql2/promise');
const db = mysql.createPool({
  host: process.env.HOST,
  user: process.env.MYSQL_USER,
  password: process.env.PASSWORD,
  database: process.env.DATABASE,
});
async function serverСonfig() {
  function sendResponse(
    res,
    action,
    requestId,
    error,
    taskUpdates,
    dependencyUpdates,
    tasksRemoved,
    dependenciesRemoved
  ) {
    if (action == 'error') console.log(error);
    let result = {
      success: action === 'error' ? false : true,
    };
    if (requestId !== undefined && requestId !== null)
      result.requestId = requestId;
    // Operations to be added
    res.send(result);
    return;
  }
}
serverСonfig();

Now create a .env file in the root folder and add the following lines for connecting to a MySQL database:

HOST=localhost
MYSQL_USER=
PASSWORD=
DATABASE=bryntum
PORT=8010

Add the root password and username for your MySQL server.

Load data from MySQL to the Bryntum Gantt chart

To get data from MySQL to our Bryntum Gantt chart, add the following GET method /data route to server.js, above the sendResponse function:

// Read
app.get('/data', async (req, res) => {
  try {
    const results = await Promise.all([
      db.query('SELECT * FROM tasks'),
      db.query('SELECT * FROM dependencies'),
    ]);
    const tasks = results[0][0],
      dependencies = results[1][0];
    res.send({
      success: true,
      tasks: {
        rows: tasks,
      },
      dependencies: {
        rows: dependencies,
      },
    });
  } catch (error) {
    sendResponse(res, 'error', null, error, [], [], [], []);
  }
});

We defined this GET method route in our ganttConfig, which will load data from this /data route.

Updating tasks and dependencies

In the ganttConfig.tsx file we created earlier, we set the sync URL of our project’s transport to http://localhost:8010/api. This is the route our app will send task update data to, so it needs to handle new, deleted, and updated tasks and dependencies.

To handle sync requests from our Bryntum Gantt chart, add the following method in the serverConfig function above the sendResponse function:

// Create, Update, Delete (Tasks & Dependencies)
app.post('/api', async function (req, res) {
  const db = mysql.createPool({
    host: process.env.HOST,
    user: process.env.MYSQL_USER,
    password: process.env.PASSWORD,
    database: process.env.DATABASE,
  });
  let requestId = '';
  let lastKey = '';
  let err = null;
  const taskUpdates = [];
  const tasksRemoved = [];
  const dependencyUpdates = [];
  const dependenciesRemoved = [];
  console.log(
    `\n${req.method} ${req.url} --> ${JSON.stringify(req.body, `\t`, 2)}`
  );
  for (const [key, value] of Object.entries(req.body)) {
    if (key === 'requestId') {
      requestId = value;
    }
    if (key === 'tasks') {
      for (const [key2, value2] of Object.entries(value)) {
        if (key2 === 'added') {
          value2.forEach((addObj) => taskUpdates.push(addObj));
          value2[0].id = uuid.v4();
          const val = await createOperation(value2[0], 'tasks');
          lastKey = val.msg;
          err = val.error;
        }
        if (key2 === 'updated') {
          console.log(`updated tasks...`, value2);
          value2.forEach((updateObj) => taskUpdates.push(updateObj));
          const val = await updateOperation(value2, 'tasks');
          lastKey = val.msg;
          err = val.error;
        }
        if (key2 === 'removed') {
          tasksRemoved.push(value2[0]);
          const val = await deleteOperation(value2[0].id, 'tasks');
          lastKey = val.msg;
          err = val.error;
        }
      }
    }
    if (key === 'dependencies') {
      for (const [key2, value2] of Object.entries(value)) {
        if (key2 === 'added') {
          value2[0].id = uuid.v4();
          value2.forEach((addObj) => dependencyUpdates.push(addObj));
          const val = await createOperation(value2[0], 'dependencies');
          lastKey = val.msg;
          err = val.error;
        }
        if (key2 === 'updated') {
          value2.forEach((updateObj) => dependencyUpdates.push(updateObj));
          const val = await updateOperation(value2, 'dependencies');
          lastKey = val.msg;
          err = val.error;
        }
        if (key2 === 'removed') {
          dependenciesRemoved.push(value2[0]);
          const val = await deleteOperation(value2[0].id, 'dependencies');
          lastKey = val.msg;
          err = val.error;
        }
      }
    }
  }
  sendResponse(
    res,
    lastKey,
    requestId,
    err,
    taskUpdates,
    dependencyUpdates,
    tasksRemoved,
    dependenciesRemoved
  );
});

When changes are made to our Gantt, the client-side Bryntum library will make a POST request to the /api endpoint to keep the data in the database in sync with the client-side UI. The code we added will handle the incoming POST request and inspect the request body to determine what action should be taken on which data model. We first determine which data model to apply a sync request to and then inspect the type of operation to perform.

For example, when adding a task to the database, there will be a POST request to the /api route. The route will check if the sync request body object has a key called tasks and then check if the nested object key is an operation called added. If this is the case for both, it will call the createOperation function (which we will define later on) and insert the added task into our database using the MySQL client. Then we insert the relevant columns to be stored and ignore keys of the object that we don’t need.

A similar process is followed for updating and removing tasks and for adding, updating, and removing dependencies.

Now let’s add the createOperation, updateOperation, and deleteOperation functions to handle the CRUD functionality of our server.

Update function

Add the following code at the bottom of server.js:

async function updateOperation(updates, table) {
  try {
    await Promise.all(
      updates.map(({ id, ...update }) => {
        return db.query(
          `
          UPDATE ${table}
          SET ${Object.keys(update)
            .map((key) => `${key} = ?`)
            .join(', ')}
          WHERE id = ?
        `,
          Object.values(update).concat(id)
        );
      })
    );
    return { msg: 'update', error: null };
  } catch (error) {
    return { msg: 'error', error };
  }
}

There may be multiple updates to perform at once, so we loop through the update property array and create key and value arrays for each update. We use these to construct UPDATE database queries. The Promise.all method from the bluebird library handles all of the async updates at once. The updateOperation function returns a result object. The response is sent using the sendResponse method, which formats the response correctly. The table argument allows us to add tasks to either the tasks table or the dependencies table.

Add function

Now add the following function above the updateOperation function:

async function createOperation(addObj, table) {
  const valArr = [];
  const keyArr = [];
  for (const [key, value] of Object.entries(addObj)) {
    if (
      key !== 'baselines' &&
      key !== 'from' &&
      key !== 'to' &&
      key !== '$PhantomId' &&
      key !== 'segments' &&
      key !== 'ignoreResourceCalendar'
    ) {
      keyArr.push(`\`${key}\``);
      valArr.push(value);
    }
  }
  try {
    await db.query(
      `INSERT INTO ${table} (${keyArr.join(', ')}) VALUES (${Array(
        keyArr.length
      )
        .fill('?')
        .join(',')})`,
      valArr
    );
    return { msg: 'added', error: null };
  } catch (error) {
    return { msg: 'error', error: error };
  }
}

Delete function

Now add the following deleteOperation function below the updateOperation function:

async function deleteOperation(id, table) {
  try {
    await db.query(`DELETE FROM ${table} WHERE id = ?`, [id]);
    return { msg: 'deleted', error: null };
  } catch (error) {
    return { msg: 'error', error: error };
  }
}

We use the id argument to find the task and delete it using the DELETE MySQL query.

Update the `sendResponse` function

We can now update our sendResponse function to include the operations we added for tasks and dependencies:

function sendResponse(
  res,
  action,
  requestId,
  error,
  taskUpdates,
  dependencyUpdates,
  tasksRemoved,
  dependenciesRemoved
) {
  if (action == 'error') console.log(error);
  const result = {
    success: action === 'error' ? false : true,
  };
  if (requestId !== undefined && requestId !== null)
    result.requestId = requestId;
  // updated tasks
  result.tasks = {};
  result.tasks.rows = [];
  if (taskUpdates.length) {
    result.tasks.rows = [...result.tasks.rows, ...taskUpdates];
  }
  // deleted tasks
  result.tasks.removed = [];
  if (tasksRemoved.length) {
    result.tasks.removed = [...result.tasks.removed, ...tasksRemoved];
  }
  // updated dependencies
  result.dependencies = {};
  result.dependencies.rows = [];
  if (dependencyUpdates.length) {
    result.dependencies.rows = [
      ...result.dependencies.rows,
      ...dependencyUpdates,
    ];
  }
  // deleted dependencies
  result.dependencies.removed = [];
  if (dependenciesRemoved.length) {
    result.dependencies.removed = [
      ...result.dependencies.removed,
      ...dependenciesRemoved,
    ];
  }
  res.send(result);
  return;
}

In this function, we format the response to include the results of the operations applied and send the response to the client to complete the handling of the HTTP request.

The API endpoints have now been successfully created to be compatible with our implementation of the Bryntum Gantt chart.

Configure the MySQL database

To set up a MySQL database locally, we’ll install MySQL Server and MySQL Workbench. MySQL Workbench is a MySQL GUI that we’ll use to create a database with tables for the Gantt data and to run queries.

Download MySQL Server and MySQL Workbench from the MySQL community downloads page. If you’re using Windows, you can use the MySQL Installer to download MySQL products. Use the default configurations when configuring MySQL Server and Workbench. Make sure that you configure the MySQL Server to start at system startup for convenience.

Open the MySQL Workbench desktop application. Open the local instance of the MySQL Server that you configured.

We’ll write our MySQL queries in the query tab and execute the queries by pressing the yellow lightning bolt button.

Let’s run some MySQL queries in MySQL Workbench to create, use, and populate a database for our Bryntum Gantt.

Run the following query in MySQL Workbench to create a database called bryntum:

CREATE DATABASE bryntum;

Now run the following query to set the bryntum database for use:

USE bryntum;

Let’s create the two tables we need for our Bryntum Gantt chart data: tasks and dependencies.

Run the following query to create the tasks table for our task data:

CREATE TABLE `tasks` (
  `id2` varchar(80) NOT NULL,
  `parentId` varchar(80) DEFAULT NULL,
  `name` varchar(255) DEFAULT NULL,
  `startDate` datetime DEFAULT NULL,
  `endDate` datetime DEFAULT NULL,
  `effort` float(11,2) DEFAULT NULL,
  `effortUnit` varchar(255) DEFAULT 'hour',
  `duration` float(11,2) unsigned DEFAULT NULL,
  `durationUnit` varchar(255) DEFAULT 'day',
  `percentDone` float(11,2) unsigned DEFAULT '0.00',
  `schedulingMode` varchar(255) DEFAULT NULL,
  `note` text,
  `constraintType` varchar(255) DEFAULT NULL,
  `constraintDate` datetime DEFAULT NULL,
  `manuallyScheduled` tinyint DEFAULT '1',
  `effortDriven` tinyint DEFAULT '0',
  `inactive` tinyint DEFAULT '0',
  `cls` varchar(255) DEFAULT NULL,
  `iconCls` varchar(255) DEFAULT NULL,
  `color` varchar(255) DEFAULT NULL,
  `parentIndex` int DEFAULT '0',
  `expanded` tinyint DEFAULT '0',
  `calendar` int DEFAULT NULL,
  `deadline` datetime DEFAULT NULL,
  `direction` varchar(255) DEFAULT NULL,
  `$PhantomId` varchar(255) DEFAULT NULL,
  `unscheduled` tinyint(1) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `parentId` (`parentId`),
  KEY `calendar` (`calendar`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

Run the following query to create the dependencies table for our dependency data:

CREATE TABLE `dependencies` (
  `id` varchar(80) NOT NULL,
  `fromEvent` varchar(80) DEFAULT NULL,
  `toEvent` varchar(80) DEFAULT NULL,
  `type` int DEFAULT '2',
  `cls` varchar(255) DEFAULT NULL,
  `lag` float(11,2) DEFAULT '0.00',
  `lagUnit` varchar(255) DEFAULT 'day',
  `$PhantomId` varchar(255) DEFAULT NULL,
  `active` tinyint(1) DEFAULT NULL,
  `fromSide` varchar(255) DEFAULT NULL,
  `toSide` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `fromEvent` (`fromEvent`),
  KEY `toEvent` (`toEvent`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

Now add some example tasks data to the tasks table:

INSERT INTO `tasks` (`id`, `name`, `expanded`, `iconCls`, `percentDone`, `startDate`, `endDate`, `parentId`, `effort`, `duration`, `parentIndex`) VALUES
("1000", 'Launch SaaS Product', 1, '', 34.248366013071895, '2022-11-14 00:00:00', '2022-11-29 00:00:00', NULL, 153.00, 15.00, 0),
("1", 'Setup web server', 1, '', 42.30769230769231, '2022-11-14 00:00:00', '2022-11-29 00:00:00', "1000", 13.00, 15.00, 0),
("11", 'Install Apache', 0, '', 50.00, '2022-11-14 00:00:00', '2022-11-17 00:00:00', "1", 3.00, 3.00, 0),
("12", 'Configure firewall', 0, '', 50.00, '2022-11-18 00:00:00', '2022-11-29 00:00:00', "1", 3.00, 11.00, 1);

We can also add some example dependencies data:

INSERT INTO `dependencies` (`id`, `fromEvent`, `toEvent`, `lag`) VALUES
("1","11","12",2);

Run your app

Now that our database, server, and Teams app is set up, we can run our application with data persistence. Navigate to the server folder in your terminal and run:

npm install
npm run start

Next, open up another terminal, navigate to the tabs folder, and run:

npm install

Now navigate to the Teams Toolkit extension in VS Code and click “Preview your Teams app” in the “Development” section.

After logging in to your Teams account and adding your test application, you will see your Bryntum Gantt rendered. If you make changes to tasks and reload the page, the changes will persist.

You can also update multiple properties of a task by right-clicking on a taskbar, clicking “Edit”, and then changing multiple values. The updateOperation function will also persist task reordering using drag-and-drop functionality.

Next steps

You now have a working version of the Gantt chart application in Microsoft Teams that you can use to visualize and manage project timelines.

So what comes next? Now that you’ve tested and verified the functionality of the application, you might like to deploy it to your production environment. Take a look at this Teams Toolkit documentation to find out how you can deploy your app to the cloud.

Are you already using Gantt chart inside Teams? If you’ve implemented Bryntum Gantt with MS Teams, we would be interested in hearing your feedback.

Mats Bryntse

Bryntum Gantt