/*
Парус 8 - Панели мониторинга
Компонент: Циклограмма
*/
//---------------------
//Подключение библиотек
//---------------------
import React, { useEffect, useState, useRef } from "react"; //Классы React
import PropTypes from "prop-types"; //Контроль свойств компонента
import {
Box,
Typography,
Dialog,
DialogActions,
DialogContent,
Button,
List,
ListItem,
ListItemText,
Link,
Divider,
IconButton,
Icon
} from "@mui/material"; //Интерфейсные компоненты
import { P8PAppInlineError } from "./p8p_app_message"; //Встраиваемое сообщение об ошибке
import { hasValue } from "../core/utils"; //Вспомогательный функции
//---------
//Константы
//---------
//Уровни масштаба
const P8P_CYCLOGRAM_ZOOM = [0.2, 0.4, 0.7, 1, 1.5, 2, 2.5];
//Параметры элементов циклограммы
const NDEFAULT_LINE_HEIGHT = 20;
const NDEFAULT_HEADER_HEIGHT = 35;
//Высота заголовка
const TITLE_HEIGHT = "44px";
//Высота панели масштабирования
const ZOOM_HEIGHT = "56px";
//Стили
const STYLES = {
CYCLOGRAM_TITLE: { height: TITLE_HEIGHT },
CYCLOGRAM_ZOOM: { height: ZOOM_HEIGHT },
HEADER_COLUMN: {
fontSize: "12px",
textOverflow: "ellipsis",
overflow: "hidden",
whiteSpace: "pre",
textAlign: "center",
lineHeight: "3",
padding: "0px 5px"
},
CYCLOGRAM_BOX: (noData, title, zoomBar) => ({
position: "relative",
overflow: "auto",
padding: "0px 8px",
height: `calc(100% - ${zoomBar ? ZOOM_HEIGHT : "0px"} - ${title ? TITLE_HEIGHT : "0px"})`,
display: noData ? "none" : ""
}),
GRID_ROW: index => (index % 2 === 0 ? { backgroundColor: "#ffffff" } : { backgroundColor: "#f5f5f5" }),
GROUP_HEADER_BOX: {
border: "1px solid",
backgroundColor: "#ebebeb",
display: "flex",
alignItems: "center",
justifyContent: "center"
},
GROUP_HEADER: {
fontSize: "14px",
textAlign: "center",
wordWrap: "break-word"
},
TASK_EDITOR_CONTENT: { minWidth: 400, overflowX: "auto" },
TASK_EDITOR_LIST: { width: "100%", minWidth: 300, maxWidth: 700, bgcolor: "background.paper" },
TASK_BOX: (lineHeight, bgColor, textColor, highlightColor) => ({
display: "flex",
alignItems: "center",
backgroundColor: bgColor ? bgColor : "#b4b9bf",
...(textColor ? { color: textColor } : {}),
height: lineHeight,
"&:hover": {
...(highlightColor
? { backgroundColor: `${highlightColor} !important`, filter: "brightness(1) !important" }
: { filter: "brightness(1.25) !important" }),
cursor: "pointer !important"
}
}),
TASK: lineHeight => {
const availableLines = Math.floor(lineHeight / 18);
return {
width: "100%",
fontSize: "12px",
overflowWrap: "break-word",
wordBreak: "break-all",
overflow: "hidden",
textOverflow: "ellipsis",
display: "-webkit-box",
lineHeight: "18px",
maxHeight: lineHeight,
WebkitLineClamp: availableLines < 1 ? 1 : availableLines,
WebkitBoxOrient: "vertical"
};
}
};
//Структура колонки
const P8P_CYCLOGRAM_COLUMN_SHAPE = PropTypes.shape({
name: PropTypes.string.isRequired,
start: PropTypes.number.isRequired,
end: PropTypes.number.isRequired
});
//Структура группы
const P8P_CYCLOGRAM_GROUP_SHAPE = PropTypes.shape({
name: PropTypes.string.isRequired,
height: PropTypes.number.isRequired,
width: PropTypes.number.isRequired,
visible: PropTypes.bool.isRequired
});
//Структура задачи
const P8P_CYCLOGRAM_TASK_SHAPE = PropTypes.shape({
id: PropTypes.string.isRequired,
rn: PropTypes.number.isRequired,
name: PropTypes.string.isRequired,
fullName: PropTypes.string.isRequired,
lineNumb: PropTypes.number.isRequired,
start: PropTypes.number.isRequired,
end: PropTypes.number.isRequired,
group: PropTypes.string,
bgColor: PropTypes.string,
textColor: PropTypes.string,
highlightColor: PropTypes.string
});
//Структура динамического атрибута задачи
const P8P_CYCLOGRAM_TASK_ATTRIBUTE_SHAPE = PropTypes.shape({
name: PropTypes.string.isRequired,
caption: PropTypes.string.isRequired,
visible: PropTypes.bool.isRequired
});
//--------------------------------
//Вспомогательные классы и функции
//--------------------------------
//Определение сдвига для максимальной ширины колонок
const getShift = (columns, currentColumnsMaxWidth, maxCyclogramWidth) => {
//Определяем доступное пространство для расширения
let maxWidthDiff = maxCyclogramWidth - currentColumnsMaxWidth;
//Инициализируем значение сдвига
let shift = 1;
//Если доступно больше ширины и есть пространство для расширения
if (maxCyclogramWidth > currentColumnsMaxWidth && maxCyclogramWidth - maxWidthDiff > columns.length) {
//Определяем доступный сдвиг колонок
shift = maxCyclogramWidth / currentColumnsMaxWidth;
}
//Возвращаем сдвиг
return shift;
};
//Формирование стилей для группы
const getGroupStyles = (indexGrp, highlightColor) => {
return `.main .TaskGrp${indexGrp}:hover .TaskGrp${indexGrp} {
${highlightColor ? `background: ${highlightColor};` : `filter: brightness(1.15);`}
}
.main:has(.TaskGrp${indexGrp}:hover) .TaskGrpHeader${indexGrp} {
display: block;
}
`;
//cursor: pointer;
};
//Фон строк таблицы
const P8PCyclogramRowsGrid = ({ rows, maxWidth, lineHeight }) => {
return (
{rows.map((el, index) => (
))}
);
};
//Контроль свойств - Фон строк таблицы
P8PCyclogramRowsGrid.propTypes = {
rows: PropTypes.array.isRequired,
maxWidth: PropTypes.number.isRequired,
lineHeight: PropTypes.number.isRequired
};
//Линии строк таблицы
const P8PCyclogramRowsLines = ({ rows, maxWidth, lineHeight }) => {
return (
{rows.map((el, index) => (
))}
);
};
//Контроль свойств - Линии строк таблицы
P8PCyclogramRowsLines.propTypes = {
rows: PropTypes.array.isRequired,
maxWidth: PropTypes.number.isRequired,
lineHeight: PropTypes.number.isRequired
};
//Линии колонок таблицы
const P8PCyclogramColumnsLines = ({ columns, shift, y1, y2 }) => {
//Инициализируем старт текущей колонки
let prevColumnEnd = 0;
return (
{columns.map((column, index) => {
//Аккумулируем окончание последней колонки с учетом сдвига
prevColumnEnd = index !== 0 ? prevColumnEnd + (columns[index - 1].end - columns[index - 1].start) * shift : 0;
return ;
})}
);
};
//Контроль свойств - Линии колонок таблицы
P8PCyclogramColumnsLines.propTypes = {
columns: PropTypes.array.isRequired,
shift: PropTypes.number.isRequired,
y1: PropTypes.number.isRequired,
y2: PropTypes.number.isRequired
};
//Фон таблицы циклограммы
const P8PCyclogramGrid = ({ tasks, columns, shift, maxWidth, maxHeight, lineHeight }) => {
//Формируем массив строк исходя из максимального значения строки задачи
const rows = Array.from(Array(Math.max(...tasks.map(o => o.lineNumb)) + 1).keys());
return (
);
};
//Контроль свойств - Фон таблицы циклограммы
P8PCyclogramGrid.propTypes = {
tasks: PropTypes.array.isRequired,
columns: PropTypes.array.isRequired,
shift: PropTypes.number.isRequired,
maxWidth: PropTypes.number.isRequired,
maxHeight: PropTypes.number.isRequired,
lineHeight: PropTypes.number.isRequired
};
//Колонка заголовка циклограммы
const P8PCyclogramHeaderColumn = ({ column, start, shift, columnRenderer }) => {
//Рассчитываем ширину колонки
const columnWidth = column.end - column.start;
//Формируем собственное отображение, если требуется
const customView = columnRenderer ? columnRenderer({ column }) : null;
return (
<>
{customView ? (
customView
) : (
{column.name}
)}
>
);
};
//Контроль свойств - Колонка заголовка циклограммы
P8PCyclogramHeaderColumn.propTypes = {
column: PropTypes.object.isRequired,
start: PropTypes.number.isRequired,
shift: PropTypes.number.isRequired,
maxHeight: PropTypes.number.isRequired,
lastElement: PropTypes.bool,
columnRenderer: PropTypes.func
};
//Заголовок циклограммы
const P8PCyclogramHeader = ({ columns, shift, maxWidth, maxHeight, columnRenderer, headerBlock }) => {
//Инициализируем старт текущей колонки
let prevColumnEnd = 0;
return (
{columns.map((column, index) => {
//Аккумулируем окончание последней колонки с учетом сдвига
prevColumnEnd = index !== 0 ? prevColumnEnd + (columns[index - 1].end - columns[index - 1].start) * shift : 0;
return (
);
})}
);
};
//Контроль свойств - Заголовок циклограммы
P8PCyclogramHeader.propTypes = {
columns: PropTypes.array.isRequired,
shift: PropTypes.number.isRequired,
maxWidth: PropTypes.number.isRequired,
maxHeight: PropTypes.number.isRequired,
columnRenderer: PropTypes.func,
headerBlock: PropTypes.object
};
//Задача циклограммы
const P8PCyclogramTask = ({ task, indexGrp, shift, lineHeight, openTaskEditor, taskRenderer }) => {
//Рассчитываем ширину задачи
const width = task.end !== 0 ? (task.end - task.start) * shift : 0;
//Формируем собственное отображение, если требуется
const customView = taskRenderer ? taskRenderer({ task, taskHeight: lineHeight, taskWidth: width }) || {} : {};
return (
openTaskEditor(task)}
>
{customView.data ? (
customView.data
) : (
{task.name}
)}
);
};
//Контроль свойств - Группы циклограммы
P8PCyclogramTask.propTypes = {
task: PropTypes.object.isRequired,
indexGrp: PropTypes.number,
shift: PropTypes.number.isRequired,
lineHeight: PropTypes.number.isRequired,
openTaskEditor: PropTypes.func.isRequired,
taskRenderer: PropTypes.func
};
//Основная информация циклограммы
const P8PCyclogramMain = ({
columns,
groups,
tasks,
shift,
lineHeight,
maxWidth,
maxHeight,
openTaskEditor,
groupHeaderRenderer,
taskRenderer,
columnRenderer,
headerBlock
}) => {
//Инициализируем коллекцию тасков с группами
const tasksWithGroup = tasks.filter(task => hasValue(task.groupName));
//Инициализируем коллекцию тасков без групп
const tasksWithoutGroup = tasks.filter(task => !hasValue(task.groupName));
//Инициализируем коллекцию отображаемых групп
const visibleGroups = groups ? groups.filter(group => group.visible) : [];
return (
{visibleGroups.length !== 0
? visibleGroups.map((grp, indexGrp) => {
//Считываем задачи группы
let groupTasks = tasksWithGroup.filter(task => task.groupName === grp.name);
//Если по данной группе нет тасков - ничего не выводим
if (groupTasks.length === 0) {
return null;
}
return (
{groupTasks.map((task, index) => (
))}
);
})
: null}
{tasksWithoutGroup.map((task, index) => {
return (
);
})}
{visibleGroups.length !== 0 ? (
{visibleGroups.map((grp, indexGrp) => {
//Инициализируем параметры группы
let defaultView = null;
let customView = null;
let groupHeaderX = 0;
let groupHeaderY = 0;
let groupTasks = tasksWithGroup.filter(task => task.groupName === grp.name);
//Если по данной группе нет тасков - ничего не выводим
if (groupTasks.length === 0) {
return null;
}
//Если требуется отображать заголовок группы
if (grp.visible) {
//Формируем отображение по умолчанию
defaultView = (
{grp.name}
);
//Формируем собственное отображение, если требуется
customView = groupHeaderRenderer ? groupHeaderRenderer({ group: grp }) : null;
//Рассчитываем координаты заголовка группы
groupHeaderX = Math.min(...groupTasks.map(o => o.start)) * shift;
groupHeaderY = NDEFAULT_HEADER_HEIGHT + Math.min(...groupTasks.map(o => o.lineNumb)) * lineHeight - grp.height - 5;
}
return (
{customView ? customView : defaultView}
);
})}
) : null}
);
};
//Контроль свойств - Основная информация циклограммы
P8PCyclogramMain.propTypes = {
columns: PropTypes.array.isRequired,
groups: PropTypes.array,
tasks: PropTypes.array.isRequired,
shift: PropTypes.number.isRequired,
lineHeight: PropTypes.number.isRequired,
maxWidth: PropTypes.number.isRequired,
maxHeight: PropTypes.number.isRequired,
openTaskEditor: PropTypes.func.isRequired,
groupHeaderRenderer: PropTypes.func,
taskRenderer: PropTypes.func,
columnRenderer: PropTypes.func,
headerBlock: PropTypes.object
};
//Редактор задачи
const P8PCyclogramTaskEditor = ({
task,
taskAttributes,
onOk,
onCancel,
taskAttributeRenderer,
taskDialogRenderer,
nameCaption,
okBtnCaption,
cancelBtnCaption
}) => {
//Собственное состояние
const [state, setState] = useState({
start: task.start,
end: task.end
});
//Отображаемые атрибуты
const dispTaskAttributes =
Array.isArray(taskAttributes) && taskAttributes.length > 0 ? taskAttributes.filter(attr => attr.visible && hasValue(task[attr.name])) : [];
//При сохранении
const handleOk = () => (onOk && state.start && state.end ? onOk() : null);
//При отмене
const handleCancel = () => (onCancel ? onCancel() : null);
//Генерация содержимого
return (
);
};
//Контроль свойств - Редактор задачи
P8PCyclogramTaskEditor.propTypes = {
task: P8P_CYCLOGRAM_TASK_SHAPE,
taskAttributes: PropTypes.arrayOf(P8P_CYCLOGRAM_TASK_ATTRIBUTE_SHAPE),
onOk: PropTypes.func,
onCancel: PropTypes.func,
taskAttributeRenderer: PropTypes.func,
taskDialogRenderer: PropTypes.func,
nameCaption: PropTypes.string.isRequired,
okBtnCaption: PropTypes.string.isRequired,
cancelBtnCaption: PropTypes.string.isRequired
};
//Циклограмма
const P8PCyclogram = ({
containerStyle,
lineHeight,
title,
titleStyle,
onTitleClick,
zoomBar,
zoom,
columns,
columnRenderer,
groups,
groupHeaderRenderer,
tasks,
taskRenderer,
taskAttributes,
taskAttributeRenderer,
taskDialogRenderer,
noDataFoundText,
nameTaskEditorCaption,
okTaskEditorBtnCaption,
cancelTaskEditorBtnCaption
}) => {
//Хук основного блока (для последующего определения доступной ширины)
const mainBlock = useRef(null);
//Хук для заголовка таблицы
const headerBlock = useRef(null);
//Собственное состояние
const [state, setState] = useState({
noData: true,
loaded: false,
lineHeight: NDEFAULT_LINE_HEIGHT,
maxWidth: 0,
maxHeight: 0,
shift: 0,
zoom: P8P_CYCLOGRAM_ZOOM.includes(zoom) ? zoom : 1,
tasks: [],
editTask: null
});
//Обновление масштаба циклограммы
const handleZoomChange = direction => {
//Считываем текущий индекс
const currentIndex = P8P_CYCLOGRAM_ZOOM.indexOf(state.zoom);
setState(pv => ({
...pv,
zoom:
currentIndex + direction !== P8P_CYCLOGRAM_ZOOM.length && currentIndex + direction !== -1
? P8P_CYCLOGRAM_ZOOM[currentIndex + direction]
: pv.zoom
}));
};
//Открытие редактора задачи
const openTaskEditor = task => setState(pv => ({ ...pv, editTask: { ...task } }));
//При сохранении задачи в редакторе
const handleTaskEditorSave = () => {
setState(pv => ({ ...pv, editTask: null }));
};
//При закрытии редактора задачи без сохранения
const handleTaskEditorCancel = () => setState(pv => ({ ...pv, editTask: null }));
//При скролле блока
const handleScroll = e => {
//Изменяем позицию заголовка таблицы относительно скролла
headerBlock.current.setAttribute("transform", "translate(0," + e.currentTarget.scrollTop + ")");
};
//При изменении данных
useEffect(() => {
//Если есть колонки и задачи
if (Array.isArray(columns) && columns.length > 0 && Array.isArray(tasks) && tasks.length > 0) {
//Определяем текущую максимальную ширину колонок
let currentColumnsMaxWidth = Math.max(...columns.map(o => o.end));
//Определяем доступный сдвиг для ширины колонок (16 - паддинг по бокам)
let columnShift = getShift(columns, currentColumnsMaxWidth, mainBlock.current.offsetWidth - 16) * state.zoom;
//Устанавливаем значения исходя из колонок/задач
setState(pv => ({
...pv,
loaded: true,
lineHeight: lineHeight ? lineHeight : NDEFAULT_LINE_HEIGHT,
maxWidth: columnShift !== 0 ? currentColumnsMaxWidth * columnShift : currentColumnsMaxWidth,
maxHeight: NDEFAULT_HEADER_HEIGHT + (Math.max(...tasks.map(o => o.lineNumb)) + 1) * (lineHeight ? lineHeight : NDEFAULT_LINE_HEIGHT),
shift: columnShift,
tasks: tasks,
noData: false
}));
} else {
//Устанавливаем значения исходя из колонок/задач
setState(pv => ({
...pv,
noData: true
}));
}
}, [columns, lineHeight, state.zoom, tasks]);
//Генерация содержимого
return (
<>
{state.noData ?
: null}
{state.loaded ? (
<>
{title ? (
{onTitleClick ? (
onTitleClick()}>
{title}
) : (
title
)}
) : null}
{zoomBar ? (
handleZoomChange(1)}
disabled={state.zoom == P8P_CYCLOGRAM_ZOOM[P8P_CYCLOGRAM_ZOOM.length - 1]}
>
zoom_in
handleZoomChange(-1)} disabled={state.zoom == P8P_CYCLOGRAM_ZOOM[0]}>
zoom_out
) : null}
>
) : null}
{state.editTask ? (
) : null}
>
);
};
//Контроль свойств - Циклограмма
P8PCyclogram.propTypes = {
containerStyle: PropTypes.object,
lineHeight: PropTypes.number,
title: PropTypes.string,
titleStyle: PropTypes.object,
onTitleClick: PropTypes.func,
zoomBar: PropTypes.bool,
zoom: PropTypes.number,
columns: PropTypes.arrayOf(P8P_CYCLOGRAM_COLUMN_SHAPE).isRequired,
columnRenderer: PropTypes.func,
groups: PropTypes.arrayOf(P8P_CYCLOGRAM_GROUP_SHAPE),
groupHeaderRenderer: PropTypes.func,
tasks: PropTypes.arrayOf(P8P_CYCLOGRAM_TASK_SHAPE).isRequired,
taskRenderer: PropTypes.func,
taskAttributes: PropTypes.arrayOf(P8P_CYCLOGRAM_TASK_ATTRIBUTE_SHAPE),
taskAttributeRenderer: PropTypes.func,
taskDialogRenderer: PropTypes.func,
noDataFoundText: PropTypes.string.isRequired,
nameTaskEditorCaption: PropTypes.string.isRequired,
okTaskEditorBtnCaption: PropTypes.string.isRequired,
cancelTaskEditorBtnCaption: PropTypes.string.isRequired
};
//----------------
//Интерфейс модуля
//----------------
export { P8PCyclogram };