WEB APP: Компонент - Диаграмма Ганта

This commit is contained in:
Mikhail Chechnev 2023-10-13 23:59:47 +03:00
parent eca8d40c94
commit 0d3158b261
6 changed files with 2561 additions and 12 deletions

View File

@ -36,11 +36,17 @@ export const BUTTONS = {
MORE: "Ещё" //Догрузка данных
};
//Текст элементов ввода
export const INPUTS = {
//Метки атрибутов, сопроводительные надписи
export const CAPTIONS = {
VALUE: "Значение",
VALUE_FROM: "С",
VALUE_TO: "По"
VALUE_TO: "По",
NUMB: "Номер",
NAME: "Наименование",
START: "Начало",
END: "Окончание",
PROGRESS: "Прогресс",
LEGEND: "Легенда"
};
//Типовые сообщения об ошибках

462
app/components/p8p_gantt.js Normal file
View File

@ -0,0 +1,462 @@
/*
Парус 8 - Панели мониторинга
Компонент: Диаграмма Ганта
*/
//---------------------
//Подключение библиотек
//---------------------
import React, { useEffect, useState, useCallback } from "react"; //Классы React
import PropTypes from "prop-types"; //Контроль свойств компонента
import {
Box,
IconButton,
Icon,
Typography,
Dialog,
DialogActions,
DialogContent,
TextField,
Button,
List,
ListItem,
ListItemText,
Divider,
Slider,
Link
} from "@mui/material"; //Интерфейсные компоненты
import { P8PAppInlineError } from "./p8p_app_message"; //Встраиваемое сообщение об ошибке
//---------
//Константы
//---------
//Уровни масштаба
const P8P_GANTT_ZOOM = [0, 1, 2, 3, 4];
//Уровни масштаба (строковые наименования в терминах библиотеки)
const P8P_GANTT_ZOOM_VIEW_MODES = {
0: "Quarter Day",
1: "Half Day",
2: "Day",
3: "Week",
4: "Month"
};
//Структура задачи
const P8P_GANTT_TASK_SHAPE = PropTypes.shape({
id: PropTypes.string.isRequired,
rn: PropTypes.number.isRequired,
numb: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
fullName: PropTypes.string.isRequired,
start: PropTypes.string.isRequired,
end: PropTypes.string.isRequired,
progress: PropTypes.number,
dependencies: PropTypes.array,
readOnly: PropTypes.bool,
readOnlyDates: PropTypes.bool,
readOnlyProgress: PropTypes.bool,
bgColor: PropTypes.string,
textColor: PropTypes.string
});
//Структура динамического атрибута задачи
const P8P_GANTT_TASK_ATTRIBUTE_SHAPE = PropTypes.shape({
name: PropTypes.string.isRequired,
caption: PropTypes.string.isRequired
});
//Структура описания цвета задачи
const P8P_GANTT_TASK_COLOR_SHAPE = PropTypes.shape({
bgColor: PropTypes.string,
textColor: PropTypes.string,
desc: PropTypes.string.isRequired
});
//Стили
const STYLES = {
TASK_EDITOR_LIST: { width: "100%", minWidth: 300, maxWidth: 700, bgcolor: "background.paper" }
};
//--------------------------------
//Вспомогательные классы и функции
//--------------------------------
//Проверка существования значения
const hasValue = value => typeof value !== "undefined" && value !== null && value !== "";
//Редактор задачи
const P8PGanttTaskEditor = ({
task,
taskAttributes,
taskColors,
onOk,
onCancel,
taskAttributeRenderer,
numbCaption,
nameCaption,
startCaption,
endCaption,
progressCaption,
legendCaption,
okBtnCaption,
cancelBtnCaption
}) => {
//Собственное состояние
const [state, setState] = useState({
start: task.start,
end: task.end,
progress: task.progress
});
//При сохранении
const handleOk = () => (onOk && state.start && state.end ? onOk({ task, start: state.start, end: state.end, progress: state.progress }) : null);
//При отмене
const handleCancel = () => (onCancel ? onCancel() : null);
//При изменении сроков
const handlePeriodChanged = e => {
setState(prev => ({ ...prev, [e.target.name]: e.target.value }));
};
//При изменении прогресса
const handleProgressChanged = (e, newValue) => {
console.log(newValue);
setState(prev => ({ ...prev, progress: newValue }));
};
//Описание легенды для задачи
let legend = null;
if (Array.isArray(taskColors)) {
const colorDesc = taskColors.find(color => task.bgColor === color.bgColor && task.textColor === color.textColor);
if (colorDesc)
legend = (
<ListItemText
secondaryTypographyProps={{
p: 1,
sx: {
...(colorDesc.bgColor ? { backgroundColor: colorDesc.bgColor } : {}),
...(colorDesc.textColor ? { color: colorDesc.textColor } : {})
}
}}
primary={legendCaption}
secondary={colorDesc.desc}
/>
);
}
//Генерация содержимого
return (
<Dialog open onClose={handleCancel}>
<DialogContent>
<List sx={STYLES.TASK_EDITOR_LIST}>
<ListItem alignItems="flex-start">
<ListItemText primary={numbCaption} secondary={task.numb} />
</ListItem>
<Divider component="li" />
<ListItem alignItems="flex-start">
<ListItemText primary={nameCaption} secondary={task.fullName} />
</ListItem>
<Divider component="li" />
<ListItem alignItems="flex-start">
<ListItemText
secondaryTypographyProps={{ component: "span" }}
primary={startCaption}
secondary={
<TextField
error={!state.start}
disabled={task.readOnly === true || task.readOnlyDates === true}
name="start"
fullWidth
required
InputLabelProps={{ shrink: true }}
type={"date"}
value={state.start}
onChange={handlePeriodChanged}
variant="standard"
size="small"
margin="normal"
/>
}
/>
</ListItem>
<Divider component="li" />
<ListItem alignItems="flex-start">
<ListItemText
secondaryTypographyProps={{ component: "span" }}
primary={endCaption}
secondary={
<TextField
error={!state.end}
disabled={task.readOnly === true || task.readOnlyDates === true}
name="end"
fullWidth
required
InputLabelProps={{ shrink: true }}
type={"date"}
value={state.end}
onChange={handlePeriodChanged}
variant="standard"
size="small"
margin="normal"
/>
}
/>
</ListItem>
<Divider component="li" />
{hasValue(task.progress) ? (
<>
<ListItem alignItems="flex-start">
<ListItemText
secondaryTypographyProps={{ component: "span" }}
primary={`${progressCaption}${
task.readOnly === true || task.readOnlyProgress === true ? ` (${task.progress}%)` : ""
}`}
secondary={
<Slider
disabled={task.readOnly === true || task.readOnlyProgress === true}
defaultValue={task.progress}
valueLabelDisplay="auto"
onChange={handleProgressChanged}
/>
}
/>
</ListItem>
<Divider component="li" />
</>
) : null}
{legend ? (
<>
<ListItem alignItems="flex-start">{legend}</ListItem>
<Divider component="li" />
</>
) : null}
{Array.isArray(taskAttributes) && taskAttributes.length > 0
? taskAttributes
.filter(attr => hasValue(task[attr.name]))
.map((attr, i) => {
const defaultView = task[attr.name];
const customView = taskAttributeRenderer ? taskAttributeRenderer({ task, attribute: attr }) : null;
return (
<React.Fragment key={i}>
<ListItem alignItems="flex-start">
<ListItemText
primary={attr.caption}
secondaryTypographyProps={{ component: "span" }}
secondary={customView ? customView : defaultView}
/>
</ListItem>
{i < taskAttributes.length - 1 ? <Divider component="li" /> : null}
</React.Fragment>
);
})
: null}
</List>
</DialogContent>
<DialogActions>
<Button disabled={!state.start || !state.end || task.readOnly} onClick={handleOk}>
{okBtnCaption}
</Button>
<Button onClick={handleCancel}>{cancelBtnCaption}</Button>
</DialogActions>
</Dialog>
);
};
//Контроль свойств - Редактор задачи
P8PGanttTaskEditor.propTypes = {
task: P8P_GANTT_TASK_SHAPE,
taskAttributes: PropTypes.arrayOf(P8P_GANTT_TASK_ATTRIBUTE_SHAPE),
taskColors: PropTypes.arrayOf(P8P_GANTT_TASK_COLOR_SHAPE),
onOk: PropTypes.func,
onCancel: PropTypes.func,
taskAttributeRenderer: PropTypes.func,
numbCaption: PropTypes.string.isRequired,
nameCaption: PropTypes.string.isRequired,
startCaption: PropTypes.string.isRequired,
endCaption: PropTypes.string.isRequired,
progressCaption: PropTypes.string.isRequired,
legendCaption: PropTypes.string.isRequired,
okBtnCaption: PropTypes.string.isRequired,
cancelBtnCaption: PropTypes.string.isRequired
};
//-----------
//Тело модуля
//-----------
//Диаграмма Ганта
const P8PGantt = ({
height,
title,
titleStyle,
onTitleClick,
zoomBar,
readOnly,
readOnlyDates,
readOnlyProgress,
zoom,
tasks,
taskAttributes,
taskColors,
onTaskDatesChange,
onTaskProgressChange,
taskAttributeRenderer,
noDataFoundText,
numbTaskEditorCaption,
nameTaskEditorCaption,
startTaskEditorCaption,
endTaskEditorCaption,
progressTaskEditorCaption,
legendTaskEditorCaption,
okTaskEditorBtnCaption,
cancelTaskEditorBtnCaption
}) => {
//Собственное состояние
const [state, setState] = useState({
noData: true,
gantt: null,
zoom: P8P_GANTT_ZOOM.includes(zoom) ? zoom : 3,
editTask: null
});
//Отображение диаграммы
const showGantt = useCallback(() => {
if (!state.gantt) {
// eslint-disable-next-line no-undef
const gantt = new Gantt("#__gantt__", tasks, {
view_mode: P8P_GANTT_ZOOM_VIEW_MODES[state.zoom],
date_format: "YYYY-MM-DD",
language: "ru",
readOnly,
readOnlyDates,
readOnlyProgress,
on_date_change: (task, start, end, isMain) => (onTaskDatesChange ? onTaskDatesChange({ task, start, end, isMain }) : null),
on_progress_change: (task, progress) => (onTaskProgressChange ? onTaskProgressChange({ task, progress }) : null),
on_click: openTaskEditor
});
setState(pv => ({ ...pv, gantt, noData: false }));
} else {
state.gantt.refresh(tasks);
setState(pv => ({ ...pv, noData: false }));
}
}, [state.gantt, state.zoom, readOnly, readOnlyDates, readOnlyProgress, tasks, onTaskDatesChange, onTaskProgressChange]);
//Обновление масштаба диаграммы
const handleZoomChange = direction =>
setState(pv => ({
...pv,
zoom: pv.zoom + direction < 0 ? 0 : pv.zoom + direction >= P8P_GANTT_ZOOM.length ? P8P_GANTT_ZOOM.length - 1 : pv.zoom + direction
}));
//Открытие редактора задачи
const openTaskEditor = task => setState(pv => ({ ...pv, editTask: { ...task } }));
//При сохранении задачи в редакторе
const handleTaskEditorSave = ({ task, start, end, progress }) => {
setState(pv => ({ ...pv, editTask: null }));
if (onTaskDatesChange && (task.start != start || task.end != end)) onTaskDatesChange({ task, start, end, isMain: true });
if (onTaskProgressChange && task.progress != progress) onTaskProgressChange({ task, progress });
};
//При закрытии редактора задачи без сохранения
const handleTaskEditorCancel = () => setState(pv => ({ ...pv, editTask: null }));
//При изменении масштаба
useEffect(() => {
if (state.gantt) state.gantt.change_view_mode(P8P_GANTT_ZOOM_VIEW_MODES[state.zoom]);
}, [state.gantt, state.zoom]);
//При изменении списка задач
useEffect(() => {
if (Array.isArray(tasks) && tasks.length > 0) showGantt();
else setState(pv => ({ ...pv, noData: true }));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [tasks]);
//Генерация содержимого
return (
<div>
{state.gantt && state.noData ? <P8PAppInlineError text={noDataFoundText} /> : null}
{state.gantt && !state.noData && title ? (
<Typography p={1} sx={{ ...(titleStyle ? titleStyle : {}) }} align="center" color="textSecondary" variant="subtitle1">
{onTitleClick ? (
<Link component="button" variant="body2" underline="hover" onClick={() => onTitleClick()}>
{title}
</Link>
) : (
title
)}
</Typography>
) : null}
{state.gantt && !state.noData && zoomBar ? (
<Box p={1}>
<IconButton onClick={() => handleZoomChange(-1)} disabled={state.zoom == 0}>
<Icon>zoom_in</Icon>
</IconButton>
<IconButton onClick={() => handleZoomChange(1)} disabled={state.zoom == P8P_GANTT_ZOOM.length - 1}>
<Icon>zoom_out</Icon>
</IconButton>
</Box>
) : null}
{state.editTask ? (
<P8PGanttTaskEditor
task={state.editTask}
taskAttributes={taskAttributes}
taskColors={taskColors}
onOk={handleTaskEditorSave}
onCancel={handleTaskEditorCancel}
taskAttributeRenderer={taskAttributeRenderer}
numbCaption={numbTaskEditorCaption}
nameCaption={nameTaskEditorCaption}
startCaption={startTaskEditorCaption}
endCaption={endTaskEditorCaption}
progressCaption={progressTaskEditorCaption}
legendCaption={legendTaskEditorCaption}
okBtnCaption={okTaskEditorBtnCaption}
cancelBtnCaption={cancelTaskEditorBtnCaption}
/>
) : null}
<div style={{ height, display: state.noData ? "none" : "" }}>
<svg id="__gantt__" width="100%"></svg>
</div>
</div>
);
};
//Контроль свойств - Диаграмма Ганта
P8PGantt.propTypes = {
height: PropTypes.string.isRequired,
title: PropTypes.string,
titleStyle: PropTypes.object,
onTitleClick: PropTypes.func,
zoomBar: PropTypes.bool,
readOnly: PropTypes.bool,
readOnlyDates: PropTypes.bool,
readOnlyProgress: PropTypes.bool,
zoom: PropTypes.number,
tasks: PropTypes.arrayOf(P8P_GANTT_TASK_SHAPE).isRequired,
taskAttributes: PropTypes.arrayOf(P8P_GANTT_TASK_ATTRIBUTE_SHAPE),
taskColors: PropTypes.arrayOf(P8P_GANTT_TASK_COLOR_SHAPE),
onTaskDatesChange: PropTypes.func,
onTaskProgressChange: PropTypes.func,
taskAttributeRenderer: PropTypes.func,
noDataFoundText: PropTypes.string.isRequired,
numbTaskEditorCaption: PropTypes.string.isRequired,
nameTaskEditorCaption: PropTypes.string.isRequired,
startTaskEditorCaption: PropTypes.string.isRequired,
endTaskEditorCaption: PropTypes.string.isRequired,
progressTaskEditorCaption: PropTypes.string.isRequired,
legendTaskEditorCaption: PropTypes.string.isRequired,
okTaskEditorBtnCaption: PropTypes.string.isRequired,
cancelTaskEditorBtnCaption: PropTypes.string.isRequired
};
//----------------
//Интерфейс модуля
//----------------
export { P8P_GANTT_TASK_SHAPE, P8P_GANTT_TASK_ATTRIBUTE_SHAPE, P8P_GANTT_TASK_COLOR_SHAPE, P8PGantt };

View File

@ -9,11 +9,12 @@
import React from "react"; //Классы React
import { deepCopyObject } from "./core/utils"; //Вспомогательные процедуры и функции
import { TITLES, BUTTONS, TEXTS, INPUTS } from "../app.text"; //Текстовые ресурсы и константы
import { TITLES, BUTTONS, TEXTS, CAPTIONS } from "../app.text"; //Текстовые ресурсы и константы
import { P8PPanelsMenuGrid, P8P_PANELS_MENU_PANEL_SHAPE } from "./components/p8p_data_grid"; //Меню панелей
import { P8PAppWorkspace } from "./components/p8p_app_workspace"; //Рабочее пространство
import { P8PTable, P8P_TABLE_DATA_TYPE, P8P_TABLE_SIZE, P8P_TABLE_FILTER_SHAPE } from "./components/p8p_data_grid"; //Таблица данных
import { P8PDataGrid, P8P_DATA_GRID_DATA_TYPE, P8P_DATA_GRID_SIZE, P8P_DATA_GRID_FILTER_SHAPE } from "./components/p8p_data_grid"; //Таблица данных
import { P8PGantt, P8P_GANTT_TASK_SHAPE, P8P_GANTT_TASK_ATTRIBUTE_SHAPE, P8P_GANTT_TASK_COLOR_SHAPE } from "./components/p8p_gantt"; //Диаграмма Ганта
//---------
//Константы
@ -36,9 +37,9 @@ const P8P_TABLE_CONFIG_PROPS = {
orderAscMenuItemCaption: BUTTONS.ORDER_ASC,
orderDescMenuItemCaption: BUTTONS.ORDER_DESC,
filterMenuItemCaption: BUTTONS.FILTER,
valueFilterCaption: INPUTS.VALUE,
valueFromFilterCaption: INPUTS.VALUE_FROM,
valueToFilterCaption: INPUTS.VALUE_TO,
valueFilterCaption: CAPTIONS.VALUE,
valueFromFilterCaption: CAPTIONS.VALUE_FROM,
valueToFilterCaption: CAPTIONS.VALUE_TO,
okFilterBtnCaption: BUTTONS.OK,
clearFilterBtnCaption: BUTTONS.CLEAR,
cancelFilterBtnCaption: BUTTONS.CANCEL,
@ -51,9 +52,9 @@ const P8P_DATA_GRID_CONFIG_PROPS = {
orderAscMenuItemCaption: BUTTONS.ORDER_ASC,
orderDescMenuItemCaption: BUTTONS.ORDER_DESC,
filterMenuItemCaption: BUTTONS.FILTER,
valueFilterCaption: INPUTS.VALUE,
valueFromFilterCaption: INPUTS.VALUE_FROM,
valueToFilterCaption: INPUTS.VALUE_TO,
valueFilterCaption: CAPTIONS.VALUE,
valueFromFilterCaption: CAPTIONS.VALUE_FROM,
valueToFilterCaption: CAPTIONS.VALUE_TO,
okFilterBtnCaption: BUTTONS.OK,
clearFilterBtnCaption: BUTTONS.CLEAR,
cancelFilterBtnCaption: BUTTONS.CANCEL,
@ -62,6 +63,19 @@ const P8P_DATA_GRID_CONFIG_PROPS = {
objectsCopier: deepCopyObject
};
//Конфигурируемые свойства "Диаграммы Ганта" (P8PGantt)
const P8P_GANTT_CONFIG_PROPS = {
noDataFoundText: TEXTS.NO_DATA_FOUND,
numbTaskEditorCaption: CAPTIONS.NUMB,
nameTaskEditorCaption: CAPTIONS.NAME,
startTaskEditorCaption: CAPTIONS.START,
endTaskEditorCaption: CAPTIONS.END,
progressTaskEditorCaption: CAPTIONS.PROGRESS,
legendTaskEditorCaption: CAPTIONS.LEGEND,
okTaskEditorBtnCaption: BUTTONS.OK,
cancelTaskEditorBtnCaption: BUTTONS.CANCEL
};
//-----------------------
//Вспомогательные функции
//-----------------------
@ -75,6 +89,7 @@ const addConfigChildProps = children =>
if (child.type.name === "P8PPanelsMenuGrid") configProps = P8P_PANELS_MENU_GRID_CONFIG_PROPS;
if (child.type.name === "P8PTable") configProps = P8P_TABLE_CONFIG_PROPS;
if (child.type.name === "P8PDataGrid") configProps = P8P_DATA_GRID_CONFIG_PROPS;
if (child.type.name === "P8PGantt") configProps = P8P_GANTT_CONFIG_PROPS;
return React.createElement(child.type, { ...configProps, ...restProps }, addConfigChildProps(children));
});
@ -89,11 +104,14 @@ const P8PPanelsMenuGridConfigWrapped = (props = {}) => <P8PPanelsMenuGrid {...P8
const P8PAppWorkspaceConfigWrapped = (props = {}) => <P8PAppWorkspace {...P8P_APP_WORKSPACE_CONFIG_PROPS} {...props} />;
//Обёртка для компонента "Таблица" (P8PTable)
const P8PTableConfigWrapped = (props = {}) => <P8PTable {...P8P_DATA_GRID_CONFIG_PROPS} {...props} />;
const P8PTableConfigWrapped = (props = {}) => <P8PTable {...P8P_TABLE_CONFIG_PROPS} {...props} />;
//Обёртка для компонента "Таблица данных" (P8PDataGrid)
const P8PDataGridConfigWrapped = (props = {}) => <P8PDataGrid {...P8P_DATA_GRID_CONFIG_PROPS} {...props} />;
//Обёртка для компонента "Диаграмма Ганта" (P8PGantt)
const P8PGanttConfigWrapped = (props = {}) => <P8PGantt {...P8P_GANTT_CONFIG_PROPS} {...props} />;
//Универсальный элемент-обёртка в параметры конфигурации
const ConfigWrapper = ({ children }) => addConfigChildProps(children);
@ -113,9 +131,14 @@ export {
P8P_DATA_GRID_DATA_TYPE,
P8P_DATA_GRID_SIZE,
P8P_DATA_GRID_FILTER_SHAPE,
P8P_GANTT_CONFIG_PROPS,
P8P_GANTT_TASK_SHAPE,
P8P_GANTT_TASK_ATTRIBUTE_SHAPE,
P8P_GANTT_TASK_COLOR_SHAPE,
P8PPanelsMenuGridConfigWrapped,
P8PAppWorkspaceConfigWrapped,
P8PTableConfigWrapped,
P8PDataGridConfigWrapped,
P8PGanttConfigWrapped,
ConfigWrapper
};

View File

@ -3,13 +3,18 @@
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>Парус 8 - Панели мониторинга</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="description" content="Parus 8 monitoring WEB application" />
<meta name="author" content="CITK Parus" />
<!-- P8 Panels -->
<link rel="stylesheet" href="./css/p8-panels.css" />
<!-- MUI -->
<link rel="stylesheet" href="./css/fonts-roboto.css" />
<link rel="stylesheet" href="./css/fonts-material-icons.css" />
<title>Парус 8 - Панели мониторинга</title>
<!-- Gantt -->
<script src="./libs/frappe-gantt/frappe-gantt.js"></script>
<link rel="stylesheet" href="./libs/frappe-gantt/frappe-gantt.css" />
</head>
<body style="display: block; margin: 0px">
<div id="app-content"></div>

View File

@ -0,0 +1,134 @@
.gantt .grid-background {
fill: none;
}
.gantt .grid-header {
fill: #ffffff;
stroke: #e0e0e0;
stroke-width: 1.4;
}
.gantt .grid-row {
fill: #ffffff;
}
.gantt .grid-row:nth-child(even) {
fill: #f5f5f5;
}
.gantt .row-line {
stroke: #ebeff2;
}
.gantt .tick {
stroke: #e0e0e0;
stroke-width: 0.2;
}
.gantt .tick.thick {
stroke-width: 0.4;
}
.gantt .today-highlight {
fill: #fcf8e3;
opacity: 0.5;
}
.gantt .arrow {
fill: none;
stroke: #666;
stroke-width: 1.4;
}
.gantt .bar {
fill: #b8c2cc;
stroke: #8D99A6;
stroke-width: 0;
transition: stroke-width 0.3s ease;
user-select: none;
}
.gantt .bar-progress {
fill: #a3a3ff;
}
.gantt .bar-invalid {
fill: transparent;
stroke: #8D99A6;
stroke-width: 1;
stroke-dasharray: 5;
}
.gantt .bar-invalid ~ .bar-label {
fill: #555;
}
.gantt .bar-label {
fill: #fff;
dominant-baseline: central;
text-anchor: middle;
font-size: 12px;
font-weight: lighter;
}
.gantt .bar-label.big {
fill: #555;
text-anchor: start;
}
.gantt .handle {
fill: #ddd;
cursor: ew-resize;
opacity: 0;
visibility: hidden;
transition: opacity 0.3s ease;
}
.gantt .bar-wrapper {
cursor: pointer;
outline: none;
}
.gantt .bar-wrapper:hover .bar {
fill: #a9b5c1;
}
.gantt .bar-wrapper:hover .bar-progress {
fill: #8a8aff;
}
.gantt .bar-wrapper:hover .handle {
visibility: visible;
opacity: 1;
}
.gantt .bar-wrapper.active .bar {
fill: #a9b5c1;
}
.gantt .bar-wrapper.active .bar-progress {
fill: #8a8aff;
}
.gantt .lower-text, .gantt .upper-text {
font-size: 12px;
text-anchor: middle;
}
.gantt .upper-text {
fill: #555;
}
.gantt .lower-text {
fill: #333;
}
.gantt .hide {
display: none;
}
.gantt-container {
position: relative;
overflow: auto;
font-size: 12px;
height: 100%;
}
.gantt-container .popup-wrapper {
position: absolute;
top: 0;
left: 0;
background: rgba(0, 0, 0, 0.8);
padding: 0;
color: #959da5;
border-radius: 3px;
}
.gantt-container .popup-wrapper .title {
border-bottom: 3px solid #a3a3ff;
padding: 10px;
}
.gantt-container .popup-wrapper .subtitle {
padding: 10px;
color: #dfe2e5;
}
.gantt-container .popup-wrapper .pointer {
position: absolute;
height: 5px;
margin: 0 0 0 -5px;
border: 5px solid transparent;
border-top-color: rgba(0, 0, 0, 0.8);
}

File diff suppressed because it is too large Load Diff