Настройка dhtmlxGantt с Node.js

Это руководство расскажет, как создать приложение с диаграммой Ганта, используя Node.js и REST API для серверной коммуникации. В настройке будет использоваться MySQL для управления базой данных. Если вы используете другую технологию, доступны другие варианты интеграции:

Эта реализация будет использовать существующие модули Node.js, чтобы минимизировать шаблонный код. Полный исходный код доступен на GitHub.

Доступно также видео-руководство, которое поможет вам создать диаграмму Ганта с Node.js.

Шаг 1: Настройка проекта

Начните с создания папки для вашего проекта и установки необходимых зависимостей. Будут использоваться следующие модули:

  • Express - легковесный фреймворк для Node.js
  • body-parser - middleware для разбора тел запросов

Создайте папку проекта с именем "dhx-gantt-app" и перейдите в нее:

mkdir dhx-gantt-app
cd dhx-gantt-app

Добавление зависимостей

Создайте файл package.json с помощью следующей команды:

npm init -y

После создания файла обновите его, чтобы включить необходимые зависимости. Он должен выглядеть так:

package.json

{
  "name": "dhx-gantt-app",
  "version": "1.0.2",
  "description": "",
  "main": "server.js",
  "dependencies": {
    "body-parser": "^1.19.1",
    "express": "^4.17.2"
  },
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "node server.js"
  },
  "keywords": [],
  "author": "",
  "license": "MIT"
}

Установите зависимости с помощью:

npm install

Настройка бэкенда

Бэкенд будет простым Express приложением с одним JavaScript файлом (server.js) для логики сервера, папкой для статических файлов (public) и HTML страницей.

Структура проекта будет выглядеть следующим образом:

dhx-gantt-app
├── node_modules
├── server.js 
├── package.json 
└── public 
    └── index.html

Создайте файл server.js и добавьте следующий код:

server.js

const express = require('express');
const bodyParser = require('body-parser');
const path = require('path');
 
const port = 1337;
const app = express();
 
app.use(express.static(path.join(__dirname, "public")));
app.use(bodyParser.urlencoded({ extended: true }));
 
app.listen(port, () =>{
    console.log("Server is running on port "+port+"...");
});

Этот код настраивает сервер для:

  • Обслуживания статических файлов из папки public
  • Прослушивания порта 1337

Далее создайте папку public для хранения главной страницы (index.html).

Папка public также может содержать JavaScript и CSS файлы для dhtmlxGantt. Для упрощения, в этом руководстве тултип будет загружаться с CDN, поэтому будет включена только HTML страница.

Шаг 2: Добавление Ганта на страницу

Создайте папку public и добавьте файл index.html со следующим содержимым:

index.html

<!DOCTYPE html>
<head>
  <meta http-equiv="Content-type" content="text/html; charset=utf-8">
 
  <script src="https://cdn.dhtmlx.com/gantt/edge/dhtmlxgantt.js"></script>
  <link href="https://cdn.dhtmlx.com/gantt/edge/dhtmlxgantt.css" rel="stylesheet">
 
  <style type="text/css">
    html, body {
      height: 100%;
      padding: 0;
      margin: 0;
      overflow: hidden;
    }
</style> </head> <body> <div id="gantt_here" style='width:100%; height:100%;'></div> <script type="text/javascript">
    gantt.init("gantt_here");
</script> </body>

Запустите сервер с помощью:

node server.js

Посетите http://127.0.0.1:1337 в вашем браузере, чтобы увидеть пустую диаграмму Ганта.

Шаг 3: Настройка базы данных

Создайте базу данных с двумя таблицами для задач и ссылок:

CREATE TABLE `gantt_links` (
  `id` INT(11) NOT NULL AUTO_INCREMENT,
  `source` INT(11) NOT NULL,
  `target` INT(11) NOT NULL,
  `type` VARCHAR(1) NOT NULL,
  PRIMARY KEY (`id`)
);
CREATE TABLE `gantt_tasks` (
  `id` INT(11) NOT NULL AUTO_INCREMENT,
  `text` VARCHAR(255) NOT NULL,
  `start_date` datetime NOT NULL,
  `duration` INT(11) NOT NULL,
  `progress` FLOAT NOT NULL,
  `parent` INT(11) NOT NULL,
  PRIMARY KEY (`id`)
);

Добавьте тестовые данные:

INSERT INTO `gantt_tasks` VALUES 
('1', 'Project #1', '2017-04-01 00:00:00', '5', '0.8', '0'),
('2', 'Task #1', '2017-04-06 00:00:00', '4', '0.5', '1'),
('3', 'Task #2', '2017-04-05 00:00:00', '6', '0.7', '1'),
('4', 'Task #3', '2017-04-07 00:00:00', '2', '0', '1'),
('5', 'Task #1.1', '2017-04-05 00:00:00', '5', '0.34', '2'),
('6', 'Task #1.2', '2017-04-11 13:22:17', '4', '0.5', '2'),
('7', 'Task #2.1', '2017-04-07 00:00:00', '5', '0.2', '3'),
('8', 'Task #2.2', '2017-04-06 00:00:00', '4', '0.9', '3');

Для получения более подробной информации, обратитесь к этому примеру.

Шаг 4: Загрузка данных

Чтобы загрузить данные, установите необходимые модули MySQL:

npm install bluebird@3.7.2 --save
npm install promise-mysql@5.1.0 --save
npm install date-format-lite@17.7.0 --save

Обновите server.js, добавив следующее:

server.js

const express = require('express');
const bodyParser = require('body-parser');
const path = require('path');
 
const port = 1337;
const app = express();
 
app.use(express.static(path.join(__dirname, "public")));
app.use(bodyParser.urlencoded({ extended: true }));
 
app.listen(port, () =>{
    console.log("Server is running on port "+port+"...");
});
 
const Promise = require('bluebird');
require("date-format-lite");
 
const mysql = require('promise-mysql');
async function serverСonfig() {
    const db = await mysql.createPool({
        host: 'localhost',
        user: 'root',
        password: '',
        database: 'gantt_howto_node'
    });
    app.get("/data", (req, res) => {
        Promise.all([
            db.query("SELECT * FROM gantt_tasks"),
            db.query("SELECT * FROM gantt_links")
        ]).then(results => {
            let tasks = results[0],
                links = results[1];
 
            for (let i = 0; i < tasks.length; i++) {
              tasks[i].start_date = tasks[i].start_date.format("YYYY-MM-DD hh:mm:ss");
              tasks[i].open = true;
            }
 
            res.send({
                data: tasks,
                collections: { links: links }
            });
 
        }).catch(error => {
            sendResponse(res, "error", null, error);
        });
    });
 
    function sendResponse(res, action, tid, error) {
 
        if (action == "error")
            console.log(error);
 
        let result = {
            action: action
        };
        if (tid !== undefined && tid !== null)
            result.tid = tid;
 
        res.send(result);
    }
};
serverСonfig();

Этот код подключается к базе данных и настраивает маршрут (GET /data) для получения данных о задачах и ссылках, форматируя их для клиента.

Обновите index.html, чтобы загрузить данные:

public/index.html

gantt.config.date_format = "%Y-%m-%d %H:%i:%s"; 
gantt.init("gantt_here");
 
gantt.load("/data");

Посетите http://127.0.0.1:1337, чтобы увидеть диаграмму Ганта с тестовыми данными.

Шаг 5: Сохранение изменений

Чтобы обрабатывать обновления с клиентской стороны, настройте сохранение данных. Добавьте gantt.dataProcessor в index.html:

public/index.html

gantt.config.date_format = "%Y-%m-%d %H:%i:%s";
 
gantt.init("gantt_here");
 
gantt.load("/data");
 
const dp = new gantt.dataProcessor("/data");dp.init(gantt);dp.setTransactionMode("REST");

Это позволяет отправлять обновления с клиентской стороны обратно на сервер.

Запросы и ответы

Когда пользователи выполняют действия, такие как добавление, изменение или удаление задач или ссылок, DataProcessor отправляет AJAX-запрос на соответствующий URL. Эти запросы включают все необходимые параметры для сохранения изменений в базе данных.

Поскольку DataProcessor установлен в режим REST, он использует определенные HTTP-глаголы для различных операций. Вы можете найти подробный список этих глаголов вместе с деталями запросов и ответов в документации по REST API.

Следующий шаг - добавление необходимых маршрутов и обработчиков в файл server.js, чтобы изменения, внесенные на клиентской стороне, правильно отражались в базе данных. Вот как выглядит код:

server.js

// добавление новой задачи
app.post("/data/task", (req, res) => {
    let task = getTask(req.body);
 
    db.query("INSERT INTO gantt_tasks(text, start_date, duration, progress, parent)"
        + " VALUES (?,?,?,?,?)",
        [task.text, task.start_date, task.duration, task.progress, task.parent])
    .then(result => {
        sendResponse(res, "inserted", result.insertId);
    })
    .catch(error => {
        sendResponse(res, "error", null, error);
    });
});
 
// обновление задачи
app.put("/data/task/:id", (req, res) => {
    let sid = req.params.id,
        task = getTask(req.body);
 
    db.query("UPDATE gantt_tasks SET text = ?, start_date = ?, "
        + "duration = ?, progress = ?, parent = ? WHERE id = ?",
        [task.text, task.start_date, task.duration, task.progress, task.parent, sid])
    .then(result => {
        sendResponse(res, "updated");
    })
    .catch(error => {
        sendResponse(res, "error", null, error);
    });
});
 
// удаление задачи
app.delete("/data/task/:id", (req, res) => {
    let sid = req.params.id;
    db.query("DELETE FROM gantt_tasks WHERE id = ?", [sid])
    .then(result => {
        sendResponse(res, "deleted");
    })
    .catch(error => {
        sendResponse(res, "error", null, error);
    });
});
 
// добавление ссылки
app.post("/data/link", (req, res) => {
    let link = getLink(req.body);
 
    db.query("INSERT INTO gantt_links(source, target, type) VALUES (?,?,?)",
        [link.source, link.target, link.type])
    .then(result => {
        sendResponse(res, "inserted", result.insertId);
    })
    .catch(error => {
        sendResponse(res, "error", null, error);
    });
});
 
// обновление ссылки
app.put("/data/link/:id", (req, res) => {
    let sid = req.params.id,
        link = getLink(req.body);
 
    db.query("UPDATE gantt_links SET source = ?, target = ?, type = ? WHERE id = ?",
        [link.source, link.target, link.type, sid])
    .then(result => {
        sendResponse(res, "updated");
    })
    .catch(error => {
        sendResponse(res, "error", null, error);
    });
});
 
// удаление ссылки
app.delete("/data/link/:id", (req, res) => {
    let sid = req.params.id;
    db.query("DELETE FROM gantt_links WHERE id = ?", [sid])
    .then(result => {
        sendResponse(res, "deleted");
    })
    .catch(error => {
        sendResponse(res, "error", null, error);
    });
});
 
function getTask(data) {
    return {
        text: data.text,
        start_date: data.start_date.date("YYYY-MM-DD"),
        duration: data.duration,
        progress: data.progress || 0,
        parent: data.parent
    };
}
 
function getLink(data) {
    return {
        source: data.source,
        target: data.target,
        type: data.type
    };
}

Эта настройка включает маршруты для обработки операций как с задачами, так и с ссылками. Запросы, связанные с задачами, используют URL "/data/task", а для ссылок - "/data/link".

Операции просты:

  • POST используется для добавления новых элементов в базу данных.
  • PUT используется для изменения существующих записей.
  • DELETE используется для удаления элементов.

Ответы отправляются обратно в виде JSON-объектов, указывающих тип выполненной операции или ошибку, если что-то пошло не так. Для POST-запросов ответ также включает ID новой записи в базе данных, что помогает сопоставить новый элемент на клиентской стороне.

Как только все настроено, открытие http://127.0.0.1:1337 должно показать полностью рабочую диаграмму Ганта.

Диаграмма Ганта


Хранение порядка задач

Диаграмма Ганта на клиентской стороне позволяет пользователям изменять порядок задач путем перетаскивания. Если вы планируете использовать эту функцию, вам нужно будет хранить порядок задач в базе данных. Подробную информацию об этом можно найти здесь.

Вот как включить эту функцию в приложении.

Включение изменения порядка задач на клиенте

Начните с включения изменения порядка задач в интерфейсе. Обновите конфигурацию Ганта в представлении "Index" следующим образом:

public/index.html

gantt.config.order_branch = true;gantt.config.order_branch_free = true; 
gantt.init("gantt_here");

Далее отразите эти изменения на сервере. Порядок задач будет храниться в столбце под названием sortorder. Обновите таблицу gantt_tasks следующим образом:

CREATE TABLE `gantt_tasks` (
  `id` int(11) NOT NULL  AUTO_INCREMENT PRIMARY KEY,
  `text` varchar(255) COLLATE utf8_unicode_ci NOT NULL,
  `start_date` datetime NOT NULL,
  `duration` int(11) NOT NULL,
  `progress` float NOT NULL DEFAULT 0,
  `parent` int(11) NOT NULL,
  `sortorder` int(11) NOT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;

Если таблица уже существует, вы можете просто добавить столбец:

ALTER TABLE `gantt_tasks` ADD COLUMN `sortorder` int(11) NOT NULL;

Теперь обновите файл server.js:

  1. Измените GET /data, чтобы возвращать задачи, упорядоченные по столбцу sortorder:

server.js

app.get("/data", (req, res) => {
    Promise.all([
        db.query("SELECT * FROM gantt_tasks ORDER BY sortorder ASC"),         db.query("SELECT * FROM gantt_links")
    ]).then(results => {
        let tasks = results[0],
            links = results[1];
 
        for (let i = 0; i < tasks.length; i++) {
            tasks[i].start_date = tasks[i].start_date.format("YYYY-MM-DD hh:mm:ss");
            tasks[i].open = true;
        }
 
        res.send({
            data: tasks,
            collections: { links: links }
        });
 
    }).catch(error => {
        sendResponse(res, "error", null, error);
    });
});
  1. Назначьте начальное значение sortorder для вновь добавленных задач:

server.js

app.post("/data/task", (req, res) => {
    let task = getTask(req.body);
 
    db.query("SELECT MAX(sortorder) AS maxOrder FROM gantt_tasks")
    .then(result => {          let orderIndex = (result[0].maxOrder || 0) + 1;         return db.query("INSERT INTO gantt_tasks(text, start_date, duration," 
          + "progress, parent, sortorder) VALUES (?,?,?,?,?,?)",
          [task.text, task.start_date, task.duration, task.progress, task.parent, 
            orderIndex]);     })
    .then(result => {
        sendResponse(res, "inserted", result.insertId);
    })
    .catch(error => {
        sendResponse(res, "error", null, error);
    });
});
  1. Обновите порядок задач, когда пользователи изменяют их порядок:

server.js

// обновление задачи
app.put("/data/task/:id", (req, res) => {
  let sid = req.params.id,
    target = req.body.target,
    task = getTask(req.body);
 
  Promise.all([
    db.query("UPDATE gantt_tasks SET text = ?, start_date = ?," 
      + "duration = ?, progress = ?, parent = ? WHERE id = ?",
      [task.text, task.start_date, task.duration, task.progress, 
        task.parent, sid]),
    updateOrder(sid, target)   ])
    .then(result => {
      sendResponse(res, "updated");
    })
    .catch(error => {
      sendResponse(res, "error", null, error);
    });
});
 
function updateOrder(taskId, target) {
  let nextTask = false;
  let targetOrder;
 
  target = target || "";
 
  if (target.startsWith("next:")) {
    target = target.substr("next:".length);
    nextTask = true;
  }
 
  return db.query("SELECT * FROM gantt_tasks WHERE id = ?", [target])
    .then(result => {
      if (!result[0])
        return Promise.resolve();
 
      targetOrder = result[0].sortorder;
      if (nextTask)
        targetOrder++;
 
      return db.query("UPDATE gantt_tasks SET sortorder"+
        " = sortorder + 1 WHERE sortorder >= ?", [targetOrder])
      .then(result => {
        return db.query("UPDATE gantt_tasks SET sortorder = ? WHERE id = ?",
          [targetOrder, taskId]);
      });
    });
}

Готовый к использованию демонстрационный проект доступен на GitHub.


Безопасность приложения

Компонент Gantt не включает встроенные механизмы защиты от угроз, таких как SQL-инъекции, XSS или CSRF-атаки. Обеспечение безопасности приложения ложится на плечи разработчиков. Подробнее об этом можно узнать в руководстве по безопасности.


Устранение неполадок

Если после завершения настройки диаграмма Ганта не отображает задачи и ссылки, обратитесь к руководству по устранению неполадок для помощи в диагностике проблемы.


Что дальше

Теперь у вас есть полностью функциональная диаграмма Ганта. Полный код доступен на GitHub, где вы можете клонировать или загрузить его для своих проектов.

Изучите дополнительные руководства по функциям Ганта или учебные материалы по интеграции Ганта с другими серверными фреймворками.

К началу