redesign and fix graphics
test-org/trust-module-frontend/pipeline/pr-rc This commit looks good Details

pull/29/head
DmitriyA 2025-03-27 10:09:58 -04:00
parent ed2e03e202
commit bd96278895
6 changed files with 465 additions and 226 deletions

View File

@ -1,9 +1,11 @@
import React, { useState } from 'react'; import React, { useState, useRef, useEffect } from 'react';
import { LineChart, XAxis, YAxis, CartesianGrid, Tooltip, Line, ResponsiveContainer } from 'recharts'; import { LineChart, XAxis, YAxis, CartesianGrid, Tooltip, Line, ResponsiveContainer, ReferenceArea } from 'recharts';
const LineChartComponent = ({ chartData, metricName, colors, description, onRangeSelect, filteredData }) => { const LineChartComponent = ({ chartData, metricName, colors, description, onRangeSelect, filteredData }) => {
const [selectionStart, setSelectionStart] = useState(null); const [selectionArea, setSelectionArea] = useState(null);
const [selectionEnd, setSelectionEnd] = useState(null); const [isSelecting, setIsSelecting] = useState(false);
const chartRef = useRef(null);
const containerRef = useRef(null);
const allTimes = Object.values(chartData) const allTimes = Object.values(chartData)
.flat() .flat()
@ -21,27 +23,48 @@ const LineChartComponent = ({ chartData, metricName, colors, description, onRang
const displayData = filteredData || data; const displayData = filteredData || data;
const handleClick = (e) => { // Блокировка выделения текста при перетаскивании
if (!e || !e.activeLabel) return; useEffect(() => {
const handleSelectStart = (e) => {
const clickedTime = e.activeLabel; if (isSelecting) {
e.preventDefault();
if (!selectionStart) {
setSelectionStart(clickedTime);
} else if (!selectionEnd) {
setSelectionEnd(clickedTime);
const startIndex = data.findIndex(point => point.time === selectionStart);
const endIndex = data.findIndex(point => point.time === clickedTime);
onRangeSelect({ startIndex, endIndex });
setSelectionStart(null);
setSelectionEnd(null);
} }
}; };
// Упрощенный Tooltip без указания instance document.addEventListener('selectstart', handleSelectStart);
return () => document.removeEventListener('selectstart', handleSelectStart);
}, [isSelecting]);
const handleMouseDown = (e) => {
if (!e || !e.activeLabel) return;
setIsSelecting(true);
setSelectionArea({ start: e.activeLabel, end: null });
};
const handleMouseMove = (e) => {
if (!selectionArea?.start || !e?.activeLabel) return;
setSelectionArea(prev => ({ ...prev, end: e.activeLabel }));
};
const handleMouseUp = () => {
setIsSelecting(false);
if (!selectionArea?.start || !selectionArea?.end) {
setSelectionArea(null);
return;
}
const startIndex = data.findIndex(point => point.time === selectionArea.start);
const endIndex = data.findIndex(point => point.time === selectionArea.end);
onRangeSelect({
startIndex: Math.min(startIndex, endIndex),
endIndex: Math.max(startIndex, endIndex)
});
setSelectionArea(null);
};
const CustomTooltip = ({ active, payload, label }) => { const CustomTooltip = ({ active, payload, label }) => {
if (active && payload && payload.length) { if (active && payload && payload.length) {
return ( return (
@ -49,10 +72,15 @@ const LineChartComponent = ({ chartData, metricName, colors, description, onRang
backgroundColor: '#fff', backgroundColor: '#fff',
padding: '10px', padding: '10px',
border: '1px solid #ccc', border: '1px solid #ccc',
borderRadius: '4px' borderRadius: '4px',
boxShadow: '0 2px 5px rgba(0,0,0,0.1)'
}}> }}>
<p style={{ fontWeight: 'bold', marginBottom: '5px' }}>{`Время: ${label}`}</p> <p style={{ fontWeight: 'bold', marginBottom: '5px' }}>{`Время: ${label}`}</p>
<p>{`Значение: ${payload[0].value}`}</p> {payload.map((item, index) => (
<p key={index} style={{ color: item.color }}>
{`${item.name}: ${item.value}`}
</p>
))}
</div> </div>
); );
} }
@ -60,12 +88,41 @@ const LineChartComponent = ({ chartData, metricName, colors, description, onRang
}; };
return ( return (
<div> <div
style={{ position: 'relative' }}
ref={containerRef}
className={isSelecting ? 'no-selection' : ''}
>
<style>
{`
.no-selection {
user-select: none;
-webkit-user-select: none;
}
`}
</style>
<div style={{
position: 'absolute',
top: 10,
left: 10,
backgroundColor: 'rgba(255,255,255,0.8)',
padding: '5px 10px',
borderRadius: 4,
fontSize: 12,
zIndex: 10,
pointerEvents: 'none'
}}>
</div>
<ResponsiveContainer width="100%" height={400}> <ResponsiveContainer width="100%" height={400}>
<LineChart <LineChart
data={displayData} data={displayData}
onClick={handleClick} onMouseDown={handleMouseDown}
onMouseMove={handleMouseMove}
onMouseUp={handleMouseUp}
margin={{ top: 5, right: 30, left: 20, bottom: 5 }} margin={{ top: 5, right: 30, left: 20, bottom: 5 }}
ref={chartRef}
> >
<CartesianGrid strokeDasharray="3 3" stroke="#f0f0f0" /> <CartesianGrid strokeDasharray="3 3" stroke="#f0f0f0" />
<XAxis <XAxis
@ -81,7 +138,6 @@ const LineChartComponent = ({ chartData, metricName, colors, description, onRang
content={<CustomTooltip />} content={<CustomTooltip />}
cursor={{ stroke: '#ccc', strokeWidth: 1 }} cursor={{ stroke: '#ccc', strokeWidth: 1 }}
/> />
{/* Убрали <Legend /> чтобы скрыть имена instance */}
{Object.keys(chartData).map((key, index) => ( {Object.keys(chartData).map((key, index) => (
<Line <Line
key={key} key={key}
@ -91,9 +147,18 @@ const LineChartComponent = ({ chartData, metricName, colors, description, onRang
strokeWidth={2} strokeWidth={2}
dot={false} dot={false}
activeDot={{ r: 6 }} activeDot={{ r: 6 }}
// Убрали name чтобы не отображалось в tooltip name={key}
/> />
))} ))}
{selectionArea?.start && selectionArea?.end && (
<ReferenceArea
x1={selectionArea.start}
x2={selectionArea.end}
strokeOpacity={0.3}
fill="#4a6baf"
/>
)}
</LineChart> </LineChart>
</ResponsiveContainer> </ResponsiveContainer>
</div> </div>

View File

@ -92,7 +92,7 @@ const PrometheusChart = ({ metricName }) => {
else if (range <= 86400) step = 120; else if (range <= 86400) step = 120;
else step = 300; else step = 300;
const response = await axios.get(`${import.meta.env.VITE_BACK_URL}/metrics`, { const response = await axios.get(`${import.meta.env.VITE_BACK_URL}/api/metrics`, {
params: { params: {
metric: metricName, metric: metricName,
start, start,
@ -161,10 +161,17 @@ const PrometheusChart = ({ metricName }) => {
const handleRangeChange = (event) => { const handleRangeChange = (event) => {
const selectedValue = event.target.value; const selectedValue = event.target.value;
const range = TIME_RANGES.find(range => range.value === parseInt(selectedValue, 10)); const range = TIME_RANGES.find(range => range.value === parseInt(selectedValue, 10));
setSelectedRange(range);
// Принудительно сбрасываем состояние
setSelectedGraphRange(null);
setFilteredData(null);
// Обновляем диапазон
setSelectedRange({ ...range }); // Создаем новый объект, чтобы React увидел изменение
setUseCustomRange(false); setUseCustomRange(false);
setSelectedGraphRange(null); // Сбрасываем выбранный диапазон
setFilteredData(null); // Сбрасываем отфильтрованные данные // Принудительно обновляем данные
fetchData();
}; };
const handleCustomRangeChange = () => { const handleCustomRangeChange = () => {
@ -173,6 +180,12 @@ const PrometheusChart = ({ metricName }) => {
setFilteredData(null); // Сбрасываем отфильтрованные данные setFilteredData(null); // Сбрасываем отфильтрованные данные
}; };
const handleResetZoom = () => {
setSelectedGraphRange(null);
setFilteredData(null);
fetchData(); // Принудительно обновляем данные
};
useEffect(() => { useEffect(() => {
if (selectedGraphRange) { if (selectedGraphRange) {
const { startIndex, endIndex } = selectedGraphRange; const { startIndex, endIndex } = selectedGraphRange;
@ -236,7 +249,8 @@ const PrometheusChart = ({ metricName }) => {
marginBottom: '15px' marginBottom: '15px'
}}> }}>
{/* Стандартные диапазоны */} {/* Стандартные диапазоны */}
<div style={{ flex: '1 1 200px' }}> <div style={{ flex: '1 1 200px', display: 'flex', gap: '10px', alignItems: 'flex-end' }}>
<div style={{ flex: '1' }}>
<label htmlFor="time-range" style={{ <label htmlFor="time-range" style={{
display: 'block', display: 'block',
marginBottom: '5px', marginBottom: '5px',
@ -262,6 +276,29 @@ const PrometheusChart = ({ metricName }) => {
</select> </select>
</div> </div>
{/* Кнопка сброса */}
<button
onClick={handleResetZoom}
style={{
padding: '8px 16px',
backgroundColor: '#f0f0f0',
color: '#333',
border: '1px solid #ddd',
borderRadius: '4px',
cursor: 'pointer',
transition: 'all 0.2s',
height: '36px',
whiteSpace: 'nowrap'
}}
onMouseOver={(e) => e.target.style.backgroundColor = '#e0e0e0'}
onMouseOut={(e) => e.target.style.backgroundColor = '#f0f0f0'}
>
Сбросить
</button>
</div>
{/* Кастомный диапазон */} {/* Кастомный диапазон */}
<div style={{ flex: '1 1 300px' }}> <div style={{ flex: '1 1 300px' }}>
<div style={{ <div style={{
@ -351,7 +388,7 @@ const PrometheusChart = ({ metricName }) => {
Текущий диапазон: {useCustomRange Текущий диапазон: {useCustomRange
? `${startDate.toLocaleString()} - ${endDate.toLocaleString()}` ? `${startDate.toLocaleString()} - ${endDate.toLocaleString()}`
: selectedRange.label} : selectedRange.label}
</div> </div >
{/* График */} {/* График */}
<LineChartComponent <LineChartComponent

View File

@ -0,0 +1,34 @@
import React from 'react';
import Button from '@mui/material/Button';
import { styled } from '@mui/material/styles';
import CircularProgress from '@mui/material/CircularProgress';
const StyledButton = styled(Button)(({ theme }) => ({
margin: theme.spacing(1),
// Дополнительные стили
}));
const CustomButton = ({
children,
variant = 'contained',
color = 'primary',
loading = false,
startIcon,
endIcon,
...props
}) => {
return (
<StyledButton
variant={variant}
color={color}
startIcon={startIcon && !loading ? startIcon : undefined}
endIcon={endIcon && !loading ? endIcon : undefined}
disabled={loading}
{...props}
>
{loading ? <CircularProgress size={24} /> : children}
</StyledButton>
);
};
export default CustomButton;

View File

@ -17,7 +17,7 @@ const LoginModal = ({ onLogin, onClose }) => {
try { try {
// Отправляем данные на бэкенд // Отправляем данные на бэкенд
const response = await fetch(`${import.meta.env.VITE_BACK_URL}/auth/login`, { const response = await fetch(`${import.meta.env.VITE_BACK_URL}/api/auth/login`, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',

View File

@ -1,12 +1,27 @@
import React, { useEffect, useRef, useState } from "react"; import React, { useEffect, useRef, useState } from "react";
import "../../Style/TreeTable.css"; import {
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
Paper,
Button,
Collapse,
Box,
Typography,
useTheme,
Tooltip
} from '@mui/material';
import { statusManager1, statusManager2 } from "../TreeChart/dataUtils"; import { statusManager1, statusManager2 } from "../TreeChart/dataUtils";
const TreeTable = ({ data }) => { const TreeTable = ({ data }) => {
const theme = useTheme();
const tableRef = useRef(null); const tableRef = useRef(null);
const [fontSize, setFontSize] = useState(16); const [fontSize, setFontSize] = useState(16);
const [log, setLog] = useState([]); const [log, setLog] = useState([]);
const [isLogVisible, setIsLogVisible] = useState(true); const [isLogVisible, setIsLogVisible] = useState(false);
const adjustFontSize = () => { const adjustFontSize = () => {
if (tableRef.current) { if (tableRef.current) {
@ -27,6 +42,13 @@ const TreeTable = ({ data }) => {
} }
}; };
useEffect(() => {
adjustFontSize();
window.addEventListener('resize', adjustFontSize);
return () => window.removeEventListener('resize', adjustFontSize);
}, [data]);
// Логирование статусов
useEffect(() => { useEffect(() => {
const newLog = []; const newLog = [];
const traverse = (items) => { const traverse = (items) => {
@ -35,7 +57,7 @@ const TreeTable = ({ data }) => {
newLog.push({ newLog.push({
title: item.title, title: item.title,
status: item.status, status: item.status,
time: new Date().toLocaleTimeString(), // Добавляем время time: new Date().toLocaleTimeString(),
}); });
} }
if (item.items) { if (item.items) {
@ -44,204 +66,285 @@ const TreeTable = ({ data }) => {
}); });
}; };
traverse(data.items); traverse(data.items);
setLog(prevLog => [...newLog, ...prevLog].slice(0, 50));
// Ограничиваем количество сообщений до 50
setLog((prevLog) => [...newLog, ...prevLog].slice(0, 50));
}, [data]); }, [data]);
const filteredData = data.items.filter((item) => item.title !== "Функциональные задачи"); const filteredData = data.items.filter(item => item.title !== "Функциональные задачи");
// Функция для отображения заголовков // Компонент индикаторов статуса
const renderHeaders = (items) => { const StatusIndicators = ({ status }) => (
<>
<Box
sx={{
width: '4px',
height: '20px',
display: 'inline-block',
backgroundColor: statusManager1.getStatusColor(status),
marginRight: '4px',
verticalAlign: 'middle'
}}
/>
<Box
sx={{
width: '4px',
height: '20px',
display: 'inline-block',
backgroundColor: statusManager2.getStatusColor(status),
marginRight: '8px',
verticalAlign: 'middle'
}}
/>
</>
);
// Ячейка с тултипом
const TableCellWithTooltip = ({ children, title, ...props }) => (
<Tooltip title={title} arrow>
<TableCell {...props}>
{children}
</TableCell>
</Tooltip>
);
// Рендер заголовков (первый уровень)
const renderMainHeaders = (items) => {
return items.map((item) => { return items.map((item) => {
const colSpan = item.items ? item.items.length : 1; const colSpan = item.items ? item.items.length : 1;
return ( return (
<th key={item.id} colSpan={colSpan} className="tree-table-header" title={item.title}> <TableCellWithTooltip
<div className="header-content"> key={item.id}
<div colSpan={colSpan}
className="status-indicator-bar" align="center"
style={{ backgroundColor: statusManager1.getStatusColor(item.status) }} title={item.title}
/> sx={{
<div backgroundColor: theme.palette.background.paper,
className="status-indicator-bar" border: `1px solid ${theme.palette.divider}`,
style={{ padding: '8px',
backgroundColor: statusManager2.getStatusColor(item.status), whiteSpace: 'nowrap',
marginLeft: "5px", overflow: 'hidden',
textOverflow: 'ellipsis'
}} }}
/> >
<StatusIndicators status={item.status} />
<Typography component="span" variant="subtitle2" noWrap>
{item.title} {item.title}
</div> </Typography>
</th> </TableCellWithTooltip>
); );
}); });
}; };
// Функция для отображения подзаголовков // Рендер подзаголовков (второй уровень)
const renderSubHeaders = (items) => { const renderSubHeaders = (items) => {
return items.map((item) => { return items.flatMap((item) => {
if (item.items) { if (item.items) {
return item.items.map((child) => ( return item.items.map((child) => (
<th key={child.id} className="tree-table-header" title={child.title}> <TableCellWithTooltip
<div className="header-content"> key={child.id}
<div align="center"
className="status-indicator-bar" title={child.title}
style={{ backgroundColor: statusManager1.getStatusColor(child.status) }} sx={{
/> backgroundColor: theme.palette.background.paper,
<div border: `1px solid ${theme.palette.divider}`,
className="status-indicator-bar" padding: '8px',
style={{ whiteSpace: 'nowrap',
backgroundColor: statusManager2.getStatusColor(child.status), overflow: 'hidden',
marginLeft: "5px", textOverflow: 'ellipsis'
}} }}
/> >
<StatusIndicators status={child.status} />
<Typography component="span" variant="subtitle2" noWrap>
{child.title} {child.title}
</div> </Typography>
</th> </TableCellWithTooltip>
)); ));
} else {
return (
<th key={item.id} className="tree-table-header" title={item.title}>
<div className="header-content">
<div
className="status-indicator-bar"
style={{ backgroundColor: statusManager1.getStatusColor(item.status) }}
/>
<div
className="status-indicator-bar"
style={{
backgroundColor: statusManager2.getStatusColor(item.status),
marginLeft: "5px",
}}
/>
{item.title}
</div>
</th>
);
} }
return (
<TableCellWithTooltip
key={item.id}
align="center"
title={item.title}
sx={{
backgroundColor: theme.palette.background.paper,
border: `1px solid ${theme.palette.divider}`,
padding: '8px',
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis'
}}
>
<StatusIndicators status={item.status} />
<Typography component="span" variant="subtitle2" noWrap>
{item.title}
</Typography>
</TableCellWithTooltip>
);
}); });
}; };
// Функция для отображения данных // Рендер данных (третий уровень)
const renderData = (items) => { const renderDataCells = (items) => {
return items.map((item) => { return items.flatMap((item) => {
if (item.items) { if (item.items) {
return item.items.map((child) => { return item.items.flatMap((child) => {
if (child.items) { if (child.items) {
return child.items.map((subChild) => ( return child.items.map((subChild) => (
<td key={subChild.id} className="tree-table-cell" title={subChild.title}> <TableCellWithTooltip
<div className="cell-content"> key={subChild.id}
<div title={subChild.title}
className="status-indicator-bar" sx={{
style={{ backgroundColor: statusManager1.getStatusColor(subChild.status) }} border: `1px solid ${theme.palette.divider}`,
/> padding: '8px',
<div whiteSpace: 'nowrap',
className="status-indicator-bar" overflow: 'hidden',
style={{ textOverflow: 'ellipsis'
backgroundColor: statusManager2.getStatusColor(subChild.status),
marginLeft: "5px",
}} }}
/> >
<span className="cell-text">{subChild.title}</span> <StatusIndicators status={subChild.status} />
</div> <Typography component="span" variant="body2" noWrap>
</td> {subChild.title}
</Typography>
</TableCellWithTooltip>
)); ));
} else {
return (
<td key={child.id} className="tree-table-cell" title={child.title}>
<div className="cell-content">
<div
className="status-indicator-bar"
style={{ backgroundColor: statusManager1.getStatusColor(child.status) }}
/>
<div
className="status-indicator-bar"
style={{
backgroundColor: statusManager2.getStatusColor(child.status),
marginLeft: "5px",
}}
/>
<span className="cell-text">{child.title}</span>
</div>
</td>
);
} }
return (
<TableCellWithTooltip
key={child.id}
title={child.title}
sx={{
border: `1px solid ${theme.palette.divider}`,
padding: '8px',
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis'
}}
>
<StatusIndicators status={child.status} />
<Typography component="span" variant="body2" noWrap>
{child.title}
</Typography>
</TableCellWithTooltip>
);
}); });
} else {
return (
<td key={item.id} className="tree-table-cell" title={item.title}>
<div className="cell-content">
<div
className="status-indicator-bar"
style={{ backgroundColor: statusManager1.getStatusColor(item.status) }}
/>
<div
className="status-indicator-bar"
style={{
backgroundColor: statusManager2.getStatusColor(item.status),
marginLeft: "5px",
}}
/>
<span className="cell-text">{item.title}</span>
</div>
</td>
);
} }
return (
<TableCellWithTooltip
key={item.id}
title={item.title}
sx={{
border: `1px solid ${theme.palette.divider}`,
padding: '8px',
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis'
}}
>
<StatusIndicators status={item.status} />
<Typography component="span" variant="body2" noWrap>
{item.title}
</Typography>
</TableCellWithTooltip>
);
}); });
}; };
return ( return (
<div className="tree-table-container"> <Box sx={{ width: '100%' }}>
<table ref={tableRef} className="tree-table" style={{ fontSize: `${fontSize}px` }}> <TableContainer
<thead> component={Paper}
<tr> ref={tableRef}
<th sx={{
colSpan={filteredData.reduce((acc, item) => acc + (item.items ? item.items.length : 1), 0)} fontSize: `${fontSize}px`,
className="tree-table-header" width: '100%',
title={data.title} '& .MuiTableCell-root': {
> py: 1,
<div className="header-content"> px: 2
<div }
className="status-indicator-bar"
style={{ backgroundColor: statusManager1.getStatusColor(data.status) }}
/>
<div
className="status-indicator-bar"
style={{
backgroundColor: statusManager2.getStatusColor(data.status),
marginLeft: "5px",
}} }}
/> >
<Table sx={{ width: '100%', tableLayout: 'fixed' }}>
<TableHead>
{/* Основной заголовок таблицы */}
<TableRow>
<TableCellWithTooltip
colSpan={filteredData.reduce((acc, item) => acc + (item.items ? item.items.length : 1), 0)}
align="center"
title={data.title}
sx={{
backgroundColor: theme.palette.background.paper,
border: `1px solid ${theme.palette.divider}`,
padding: '8px'
}}
>
<StatusIndicators status={data.status} />
<Typography component="span" variant="subtitle1" fontWeight="bold" noWrap>
{data.title} {data.title}
</div> </Typography>
</th> </TableCellWithTooltip>
</tr> </TableRow>
<tr>{renderHeaders(filteredData)}</tr>
<tr>{renderSubHeaders(filteredData)}</tr> {/* Строка с основными заголовками */}
</thead> <TableRow>
<tbody> {renderMainHeaders(filteredData)}
<tr className="tree-table-row">{renderData(filteredData)}</tr> </TableRow>
</tbody>
</table> {/* Строка с подзаголовками (которая пропала в предыдущей версии) */}
<button <TableRow>
{renderSubHeaders(filteredData)}
</TableRow>
</TableHead>
<TableBody>
<TableRow>
{renderDataCells(filteredData)}
</TableRow>
</TableBody>
</Table>
</TableContainer>
<Button
variant="outlined"
onClick={() => setIsLogVisible(!isLogVisible)} onClick={() => setIsLogVisible(!isLogVisible)}
className="toggle-log-button" size="small"
style={{ marginTop: "10px" }} sx={{ mt: 2 }}
> >
{isLogVisible ? "Скрыть лог" : "Показать лог"} {isLogVisible ? 'Скрыть историю изменения статусов' : 'Показать историю изменения статусов'}
</button> </Button>
{isLogVisible && (
<div className="status-log"> <Collapse in={isLogVisible}>
<h3>Лог статусов</h3> <Box sx={{
<ul> mt: 2,
p: 2,
border: `1px solid ${theme.palette.divider}`,
borderRadius: 1,
backgroundColor: theme.palette.background.paper
}}>
<Typography variant="h6" gutterBottom>
История изменения статусов
</Typography>
<Box component="ul" sx={{
pl: 2,
maxHeight: 200,
overflow: 'auto',
listStyle: 'none'
}}>
{log.map((entry, index) => ( {log.map((entry, index) => (
<li key={index} style={{ color: statusManager1.getStatusColor(entry.status) }}> <Box
component="li"
key={index}
sx={{
py: 1,
borderBottom: `1px solid ${theme.palette.divider}`,
color: statusManager1.getStatusColor(entry.status)
}}
>
[{entry.time}] {entry.status}: {entry.title} [{entry.time}] {entry.status}: {entry.title}
</li> </Box>
))} ))}
</ul> </Box>
</div> </Box>
)} </Collapse>
</div> </Box>
); );
}; };

View File

@ -17,9 +17,9 @@ export const lightTheme = createTheme({
main: "#0f55bec2", main: "#0f55bec2",
}, },
custom: { custom: {
background: "#FFFFFF", background: "#025EA1",
text: "#000000", text: "#000000",
sidebar: "#3d74c7", sidebar: "#025EA1",
sidebarText: "#FFFFFF", sidebarText: "#FFFFFF",
modalBackground: "#FFFFFF", modalBackground: "#FFFFFF",
modalBtnBackground: "#0f55bec2", modalBtnBackground: "#0f55bec2",