Добавил окно авторизации и визуализацию меню в виде древа

pull/2/head
DmitriyA 2025-02-20 12:01:48 +00:00
parent f8eab83bb4
commit 517d3893be
12 changed files with 371 additions and 436 deletions

View File

@ -1,9 +1,22 @@
{ [
"labels": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], {
"datasets": [ "timestamp": "2025-02-18 12:00",
{ "value": 10
"data": [50, 52, 55, 53, 60, 58, 65, 62, 66, 72], },
"data2": [20,56,74,45,21,20,56,74,45,21] {
} "timestamp": "2025-02-18 12:05",
] "value": 12
} },
{
"timestamp": "2025-02-18 12:10",
"value": 15
},
{
"timestamp": "2025-02-18 12:15",
"value": 13
},
{
"timestamp": "2025-02-18 12:20",
"value": 17
}
]

View File

@ -1,29 +1,26 @@
import React from "react"; import React, { useState } from "react";
import Dashboard from "./Components/Dashboard"; import Dashboard from "./Components/Dashboard";
import NetworkSpeedChart from './Charts/TestCharts'; import LoginModal from "./Components/LoginModal"; // Импортируем компонент авторизации
import NetworkSpeedChart2 from './Charts/TestCharts2' import "./Style/LoginModal.css"; // Импортируем стили
import NetworkSpeedChart3 from './Charts/TestCharts3'
function App() { function App() {
const [isAuthenticated, setIsAuthenticated] = useState(false); // Состояние авторизации
const [showLoginModal, setShowLoginModal] = useState(true); // Показывать ли модальное окно
const handleLogin = () => {
setIsAuthenticated(true); // Устанавливаем авторизацию
setShowLoginModal(false); // Скрываем модальное окно
};
return ( return (
<div style={{ display: "flex", height: "100vh", overflow: "hidden" }}> <div style={{ display: "flex", height: "100vh", overflow: "hidden" }}>
<Dashboard /> {!isAuthenticated && showLoginModal && (
<LoginModal onLogin={handleLogin} onClose={() => setShowLoginModal(false)} />
)}
{isAuthenticated && <Dashboard />}
</div> </div>
); );
/*
return (
<div style={{ padding: "20px" }}>
<h1>Dashboard</h1>
<Dashboard />
<div style={{ marginBottom: "40px" }}>
<h2>Примеры импорта данных</h2>
<NetworkSpeedChart />
<NetworkSpeedChart2 />
<NetworkSpeedChart3 />
</div>
</div>
);
*/
} }
export default App; export default App;

View File

@ -12,9 +12,8 @@ import {
Legend, Legend,
TimeScale, TimeScale,
} from 'chart.js'; } from 'chart.js';
import 'chartjs-adapter-date-fns'; // Импортируем адаптер дат import 'chartjs-adapter-date-fns';
// Регистрируем компоненты Chart.js
ChartJS.register( ChartJS.register(
CategoryScale, CategoryScale,
LinearScale, LinearScale,
@ -23,141 +22,72 @@ ChartJS.register(
Title, Title,
Tooltip, Tooltip,
Legend, Legend,
TimeScale // Регистрируем временную шкалу TimeScale
); );
const MAX_DATA_POINTS = 50;
const NetworkSpeedChart2 = () => { const NetworkSpeedChart2 = () => {
const [chartData, setChartData] = useState({ const [chartData, setChartData] = useState({ labels: [], datasets: [] });
labels: [],
datasets: [],
});
const chartRef = useRef(null); // Референс на график
// Функция для загрузки данных
const fetchData = async () => { const fetchData = async () => {
try { try {
const response = await axios.get('http://192.168.2.33:3000/metrics?metric=node_time_seconds'); const response = await axios.get('http://192.168.2.33:3000/metrics?metric=node_time_seconds');
const newData = response.data; const newData = response.data;
console.log('New data from backend:', newData); // Проверяем новые данные
// Обновляем состояние, добавляя новые данные к существующим
setChartData((prevChartData) => { setChartData((prevChartData) => {
// Группируем новые данные по устройству (device)
const newGroupedData = newData.reduce((acc, entry) => { const newGroupedData = newData.reduce((acc, entry) => {
const device = entry.device; if (!acc[entry.device]) acc[entry.device] = [];
if (!acc[device]) { acc[entry.device].push({ x: new Date(entry.timestamp), y: entry.value });
acc[device] = [];
}
acc[device].push(entry);
return acc; return acc;
}, {}); }, {});
// Создаем новый набор данных
const newDatasets = Object.keys(newGroupedData).map((device, index) => { const newDatasets = Object.keys(newGroupedData).map((device, index) => {
// Находим существующий dataset для этого устройства const existingDataset = prevChartData.datasets.find((d) => d.label === `Device: ${device}`);
const existingDataset = prevChartData.datasets.find((dataset) => dataset.label === `Device: ${device}`); const updatedData = existingDataset ? [...existingDataset.data, ...newGroupedData[device]] : newGroupedData[device];
// Если dataset уже существует, добавляем новые данные к нему
if (existingDataset) {
return {
...existingDataset,
data: [
...existingDataset.data,
...newGroupedData[device].map((entry) => ({
x: new Date(entry.timestamp), // Временная метка
y: entry.value, // Значение
})),
],
};
}
// Если dataset не существует, создаем новый
return { return {
label: `Device: ${device}`, label: `Device: ${device}`,
data: newGroupedData[device].map((entry) => ({ data: updatedData.slice(-MAX_DATA_POINTS),
x: new Date(entry.timestamp),
y: entry.value,
})),
borderColor: `hsl(${(index * 360) / Object.keys(newGroupedData).length}, 70%, 50%)`, borderColor: `hsl(${(index * 360) / Object.keys(newGroupedData).length}, 70%, 50%)`,
backgroundColor: `hsla(${(index * 360) / Object.keys(newGroupedData).length}, 70%, 50%, 0.2)`, backgroundColor: `hsla(${(index * 360) / Object.keys(newGroupedData).length}, 70%, 50%, 0.2)`,
tension: 0.2, tension: 0.2,
}; };
}); });
// Обновляем labels (метки времени) return { labels: newDatasets[0]?.data.map((d) => d.x) || [], datasets: newDatasets };
const newLabels = [
...prevChartData.labels,
...newData.map((entry) => new Date(entry.timestamp)),
];
return {
labels: newLabels,
datasets: newDatasets,
};
}); });
} catch (error) { } catch (error) {
console.error('Ошибка при загрузке метрик:', error); console.error('Ошибка при загрузке метрик:', error);
} }
}; };
// Загружаем данные при монтировании компонента и обновляем каждые 5 секунд
useEffect(() => { useEffect(() => {
fetchData(); fetchData();
const interval = setInterval(fetchData, 5000); const interval = setInterval(fetchData, 5000);
return () => clearInterval(interval);
// Очищаем интервал и уничтожаем график при размонтировании компонента
return () => {
clearInterval(interval);
if (chartRef.current) {
chartRef.current.destroy();
}
};
}, []); }, []);
// Опции графика
const options = { const options = {
responsive: true, responsive: true,
plugins: { plugins: {
legend: { legend: { position: 'top' },
position: 'top', title: { display: true, text: 'node_time_seconds' },
},
title: {
display: true,
text: 'node_time_seconds',
},
}, },
scales: { scales: {
x: { x: {
type: 'time', // Используем временную шкалу type: 'time',
time: { time: { unit: 'second', displayFormats: { second: 'HH:mm:ss' } },
unit: 'second', // Единица времени title: { display: true, text: 'Time' },
displayFormats: {
second: 'HH:mm:ss', // Формат отображения времени
},
},
title: {
display: true,
text: 'Time',
},
},
y: {
title: {
display: true,
text: 'Данные',
},
}, },
y: { title: { display: true, text: 'Value' } },
}, },
animation: { animation: { duration: 1000, easing: 'linear' },
duration: 1000, // Длительность анимации
easing: 'linear', // Тип анимации
},
}; };
return ( return (
<div style={{ width: '800px', height: '400px' }}> <div style={{ width: '800px', height: '400px' }}>
<Line ref={chartRef} data={chartData} options={options} /> <Line data={chartData} options={options} />
</div> </div>
); );
}; };

View File

@ -1,165 +1,68 @@
import React, { useEffect, useState, useRef } from 'react'; import React, { useEffect, useState } from 'react';
import axios from 'axios';
import { Line } from 'react-chartjs-2'; import { Line } from 'react-chartjs-2';
import { import axios from 'axios';
Chart as ChartJS, import { Chart as ChartJS, Title, Tooltip, Legend, LineElement, CategoryScale, LinearScale } from 'chart.js';
CategoryScale,
LinearScale,
PointElement,
LineElement,
Title,
Tooltip,
Legend,
TimeScale,
} from 'chart.js';
import 'chartjs-adapter-date-fns'; // Импортируем адаптер дат
// Регистрируем компоненты Chart.js // Регистрация компонентов Chart.js
ChartJS.register( ChartJS.register(Title, Tooltip, Legend, LineElement, CategoryScale, LinearScale);
CategoryScale,
LinearScale,
PointElement,
LineElement,
Title,
Tooltip,
Legend,
TimeScale // Регистрируем временную шкалу
);
const NetworkSpeedChart3 = () => { const SimpleGraph = () => {
const [chartData, setChartData] = useState({ const [data, setData] = useState([]);
labels: [],
datasets: [],
});
const chartRef = useRef(null); // Референс на график
// Функция для загрузки данных
const fetchData = async () => {
try {
const response = await axios.get('http://192.168.2.33:3000/metrics?metric=node_memory_MemAvailable_bytes');
const newData = response.data;
console.log('New data from backend:', newData); // Проверяем новые данные
// Обновляем состояние, добавляя новые данные к существующим
setChartData((prevChartData) => {
// Группируем новые данные по устройству (device)
const newGroupedData = newData.reduce((acc, entry) => {
const device = entry.device;
if (!acc[device]) {
acc[device] = [];
}
acc[device].push(entry);
return acc;
}, {});
// Создаем новый набор данных
const newDatasets = Object.keys(newGroupedData).map((device, index) => {
// Находим существующий dataset для этого устройства
const existingDataset = prevChartData.datasets.find((dataset) => dataset.label === `Device: ${device}`);
// Если dataset уже существует, добавляем новые данные к нему
if (existingDataset) {
return {
...existingDataset,
data: [
...existingDataset.data,
...newGroupedData[device].map((entry) => ({
x: new Date(entry.timestamp), // Временная метка
y: entry.value, // Значение
})),
],
};
}
// Если dataset не существует, создаем новый
return {
label: `Device: ${device}`,
data: newGroupedData[device].map((entry) => ({
x: new Date(entry.timestamp),
y: entry.value,
})),
borderColor: `hsl(${(index * 360) / Object.keys(newGroupedData).length}, 70%, 50%)`,
backgroundColor: `hsla(${(index * 360) / Object.keys(newGroupedData).length}, 70%, 50%, 0.2)`,
tension: 0.2,
};
});
// Обновляем labels (метки времени)
const newLabels = [
...prevChartData.labels,
...newData.map((entry) => new Date(entry.timestamp)),
];
return {
labels: newLabels,
datasets: newDatasets,
};
});
} catch (error) {
console.error('Ошибка при загрузке метрик:', error);
}
};
// Загружаем данные при монтировании компонента и обновляем каждые 5 секунд
useEffect(() => { useEffect(() => {
fetchData(); const fetchData = async () => {
const interval = setInterval(fetchData, 5000); try {
// Загружаем данные из файла с использованием axios
const response = await axios.get('/data.json'); // Путь должен быть относительно папки public
const rawData = response.data;
// Очищаем интервал и уничтожаем график при размонтировании компонента // Проверяем, что данные действительно массив
return () => { if (Array.isArray(rawData)) {
clearInterval(interval); const chartData = rawData.map(item => ({
if (chartRef.current) { timestamp: item.timestamp,
chartRef.current.destroy(); value: item.value,
}));
setData(chartData);
} else {
throw new Error('Ошибка: Данные не являются массивом.');
}
} catch (error) {
console.error('Error fetching data:', error);
} }
}; };
fetchData();
}, []); }, []);
// Опции графика if (data.length === 0) return <div>Loading...</div>;
const options = {
// Настройки графика
const chartOptions = {
responsive: true, responsive: true,
plugins: { plugins: {
legend: {
position: 'top',
},
title: { title: {
display: true, display: true,
text: 'node_memory_MemAvailable_bytes', text: 'Simple Data Graph',
}, },
}, },
scales: {
x: {
type: 'time', // Используем временную шкалу
time: {
unit: 'second', // Единица времени
displayFormats: {
second: 'HH:mm:ss', // Формат отображения времени
},
},
title: {
display: true,
text: 'Time',
},
},
y: {
title: {
display: true,
text: 'Данные',
},
},
},
animation: {
duration: 1000, // Длительность анимации
easing: 'linear', // Тип анимации
},
}; };
return ( const chartData = {
<div style={{ width: '800px', height: '400px' }}> labels: data.map(item => item.timestamp), // Массив меток для оси X
<Line ref={chartRef} data={chartData} options={options} /> datasets: [
</div> {
); label: 'Value',
data: data.map(item => item.value), // Массив значений для оси Y
borderColor: 'rgb(75, 192, 192)',
backgroundColor: 'rgba(75, 192, 192, 0.2)',
fill: false,
tension: 0.1,
},
],
};
return <Line data={chartData} options={chartOptions} />;
}; };
export default NetworkSpeedChart3; export default SimpleGraph;

View File

@ -2,11 +2,11 @@ import React, { useState, useEffect } from "react";
import SidebarMenu from "./SidebarMenu"; import SidebarMenu from "./SidebarMenu";
import SystemStatusTable from "../Charts/SystemStatusTable"; import SystemStatusTable from "../Charts/SystemStatusTable";
import SystemStatusTableSoftware from "../Charts/SystemStatusTableSoftware"; import SystemStatusTableSoftware from "../Charts/SystemStatusTableSoftware";
import TreeChart from "./TreeChart"; // Подключаем граф import TreeChart from "./TreeChart";
import "../Style/Dashboard.css"; import "../Style/Dashboard.css";
import ErrorIndicator from "./ErrorIndicator"; import ErrorIndicator from "./ErrorIndicator";
import tabContentData from "./tabContent"; import tabContentData from "./tabContent";
import menuData from "./menuData.json"; // Загружаем меню import menuData from "./menuData.json"; // Загружаем новое меню
const Dashboard = () => { const Dashboard = () => {
const [tabs, setTabs] = useState([]); const [tabs, setTabs] = useState([]);
@ -16,24 +16,42 @@ const Dashboard = () => {
useEffect(() => { useEffect(() => {
setTabContent(tabContentData); setTabContent(tabContentData);
setTreeData({ title: "Меню", items: menuData }); // Передаём данные в граф setTreeData(menuData); // Теперь menuData - объект, а не массив
}, []); }, []);
const handleOpenTab = (tabName) => { const handleOpenTab = (id, title) => {
if (!tabs.includes(tabName)) { if (!tabs.includes(id)) {
setTabs([...tabs, tabName]); setTabs([...tabs, id]);
} }
setActiveTab(tabName); setActiveTab(id);
}; };
const handleCloseTab = (tabName) => { const handleCloseTab = (id) => {
const newTabs = tabs.filter((tab) => tab !== tabName); const newTabs = tabs.filter((tab) => tab !== id);
setTabs(newTabs); setTabs(newTabs);
if (activeTab === tabName) { if (activeTab === id) {
setActiveTab(newTabs.length > 0 ? newTabs[newTabs.length - 1] : "Главная"); setActiveTab(newTabs.length > 0 ? newTabs[newTabs.length - 1] : "Главная");
} }
}; };
const renderTabContent = () => {
if (activeTab === "Главная") {
return (
<div>
<h2>Общий мониторинг</h2>
<ErrorIndicator />
<SystemStatusTable />
<SystemStatusTableSoftware />
</div>
);
} else if (activeTab === "Визуализация") {
return <TreeChart data={treeData} onNodeClick={(id, title) => handleOpenTab(id, title)} />;
} else {
const tabData = tabContent[activeTab];
return tabData ? tabData.content : <p>Нет данных</p>;
}
};
return ( return (
<div className="dashboard-container"> <div className="dashboard-container">
<SidebarMenu onOpenTab={handleOpenTab} /> <SidebarMenu onOpenTab={handleOpenTab} />
@ -73,18 +91,7 @@ const Dashboard = () => {
</div> </div>
<div className="content"> <div className="content">
{activeTab === "Главная" ? ( {renderTabContent()}
<div>
<h2>Общий мониторинг</h2>
<ErrorIndicator />
<SystemStatusTable />
<SystemStatusTableSoftware />
</div>
) : activeTab === "Визуализация" ? (
<TreeChart data={treeData} onNodeClick={(node) => handleOpenTab(node.title)} />
) : (
<div dangerouslySetInnerHTML={{ __html: tabContent[activeTab] || "<p>Нет данных</p>" }} />
)}
</div> </div>
</div> </div>
</div> </div>

View File

@ -0,0 +1,49 @@
import React, { useState } from "react";
const Login = ({ onLogin, onClose }) => {
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const [error, setError] = useState("");
const handleSubmit = (e) => {
e.preventDefault();
if (username === "admin" && password === "admin") {
onLogin(); // Успешная авторизация
onClose(); // Закрыть модальное окно
} else {
setError("Неверный логин или пароль");
}
};
return (
<div className="modal-overlay">
<div className="modal">
<h2>Авторизация</h2>
<form onSubmit={handleSubmit}>
<div>
<label>Логин:</label>
<input
type="text"
value={username}
onChange={(e) => setUsername(e.target.value)}
required
/>
</div>
<div>
<label>Пароль:</label>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
/>
</div>
{error && <p className="error">{error}</p>}
<button type="submit">Войти</button>
</form>
</div>
</div>
);
};
export default Login;

View File

@ -2,19 +2,15 @@ import React, { useState } from "react";
import "../Style/SidebarMenu.css"; import "../Style/SidebarMenu.css";
import menuData from "./menuData.json"; import menuData from "./menuData.json";
// Рекурсивный компонент для отображения меню
const MenuItem = ({ item, onSelectItem }) => { const MenuItem = ({ item, onSelectItem }) => {
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const hasChildren = Array.isArray(item.items) && item.items.length > 0; const hasChildren = Array.isArray(item.items) && item.items.length > 0;
const handleClick = () => { const handleClick = () => {
if (hasChildren) { if (hasChildren) {
setIsOpen(!isOpen); // Раскрываем/сворачиваем подменю setIsOpen(!isOpen);
} else { } else {
onSelectItem(item); // Выбираем конечный элемент onSelectItem(item);
} }
}; };
@ -27,32 +23,23 @@ const MenuItem = ({ item, onSelectItem }) => {
{isOpen && hasChildren && ( {isOpen && hasChildren && (
<div className="submenu"> <div className="submenu">
{item.items.map((child, index) => ( {item.items.map((child, index) => (
<MenuItem <MenuItem key={index} item={child} onSelectItem={onSelectItem} />
key={index}
item={typeof child === "string" ? { title: child, id: child } : child}
onSelectItem={onSelectItem}
/>
))} ))}
</div> </div>
)} )}
</div> </div>
); );
}; };
// Основной компонент SidebarMenu
function SidebarMenu({ onOpenTab }) { function SidebarMenu({ onOpenTab }) {
const handleSelectItem = (item) => { const handleSelectItem = (item) => {
onOpenTab(item.id, item.title); // Передаем и ID, и название onOpenTab(item.id, item.title); // Передаем id и title
}; };
return ( return (
<div className="sidebar"> <div className="sidebar">
<h2 className="sidebar-title">Уровень доверия:</h2>
<h2 className="sidebar-title">Меню</h2> <h2 className="sidebar-title">Меню</h2>
{menuData.map((section, index) => ( // Используем menuData вместо menuItems <MenuItem item={menuData} onSelectItem={handleSelectItem} />
<MenuItem key={index} item={section} onSelectItem={handleSelectItem} />
))}
</div> </div>
); );
} }

View File

@ -69,7 +69,7 @@ const TreeChart = ({ data, onNodeClick }) => {
node.on("click", (event, d) => { node.on("click", (event, d) => {
if (onNodeClick) { if (onNodeClick) {
onNodeClick(d.data); onNodeClick(d.data.id, d.data.title); // Передаем id и title
} }
}); });

View File

@ -1,69 +1,55 @@
[ {
{ "title": "Сервис ВКС",
"title": "Выбор сервиса", "items": [
"items": [ {
{ "title": "Функциональные задачи",
"id": "service1", "items": [
"title": "Сервис ВКС" {
}, "id": "system_control",
{ "title": "Контроль системы"
"id": "service2", },
"title": "Сервис 2" {
}, "id": "system_management",
{ "title": "Система управления"
"id": "service3", },
"title": "Сервис 3" {
} "id": "conference",
] "title": "Проведение ВКС"
}, },
{ {
"title": "Функциональные задачи", "id": "backup",
"items": [ "title": "Резервное копирование"
{ },
"id": "system_control", {
"title": "Контроль системы" "id": "relay_info",
}, "title": "Ретрансляция информации"
{ }
"id": "system_management", ]
"title": "Система управления" },
}, {
{ "title": "Аппаратное ПО",
"id": "conference", "items": [
"title": "Проведение ВКС" {
}, "id": "hardware_software_1",
{ "title": О1"
"id": "backup", },
"title": "Резервное копирование" {
}, "id": "hardware_software_2",
{ "title": О2"
"id": "relay_info", },
"title": "Ретрансляция информации" {
} "id": "hardware_software_3",
] "title": О3"
}, },
{ {
"title": "Аппаратное ПО", "id": "hardware_software_4",
"items": [ "title": О4"
{ },
"id": "hardware_software_1", {
"title": О1" "id": "hardware_software_5",
}, "title": О5"
{ }
"id": "hardware_software_2", ]
"title": О2" }
}, ]
{ }
"id": "hardware_software_3",
"title": О3"
},
{
"id": "hardware_software_4",
"title": О4"
},
{
"id": "hardware_software_5",
"title": О5"
}
]
}
]

View File

@ -1,14 +1,18 @@
import React from "react"; import React from "react";
import NetworkSpeedChart2 from '../Charts/TestCharts2';
const tabContent = { const tabContent = {
service1: <div><h2>Сервис ВКС</h2></div>, service1: { title: "Сервис ВКС", content: <div><h2>Сервис ВКС</h2></div> },
service2: <div><h2>Сервис 2</h2></div>, system_control: { title: "Контроль системы", content: <div><h2>Контроль системы</h2><p>Описание контроля.</p></div> },
service3: <div><h2>Сервис 3</h2></div>, system_management: { title: "Система управления", content: <div><h2>Система управления</h2><p>Описание системы управления.</p></div> },
system_control: <div><h2>Контроль системы</h2><p>Описание контроля.</p></div>, conference: { title: "Проведение ВКС", content: <div><h2>Проведение ВКС</h2><p>Информация о проведении ВКС.</p></div> },
system_management: <div><h2>Система управления</h2><p>Описание системы управления.</p></div>, backup: { title: "Резервное копирование", content: <div><h2>Резервное копирование</h2><p>Процесс резервного копирования.</p></div> },
conference: <div><h2>Проведение ВКС</h2><p>Информация о проведении ВКС.</p></div>, relay_info: { title: "Ретрансляция информации", content: <div><h2>Ретрансляция информации</h2><p>Детали ретрансляции.</p></div> },
backup: <div><h2>Резервное копирование</h2><p>Процесс резервного копирования.</p></div>, hardware_software_1: { title: "График скорости сети", content: <div><h2>График скорости сети</h2><NetworkSpeedChart2 /></div> },
relay_info: <div><h2>Ретрансляция информации</h2><p>Детали ретрансляции.</p></div>, hardware_software_2: { title: О2", content: <div><h2>ПО2</h2></div> },
hardware_software_3: { title: О3", content: <div><h2>ПО3</h2></div> },
hardware_software_4: { title: О4", content: <div><h2>ПО4</h2></div> },
hardware_software_5: { title: О5", content: <div><h2>ПО5</h2></div> },
}; };
export default tabContent; export default tabContent;

55
src/Style/LoginModal.css Normal file
View File

@ -0,0 +1,55 @@
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
}
.modal {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
max-width: 400px;
width: 100%;
}
.modal h2 {
margin-bottom: 20px;
}
.modal label {
display: block;
margin-bottom: 5px;
}
.modal input {
width: 100%;
padding: 8px;
margin-bottom: 10px;
border: 1px solid #ccc;
border-radius: 4px;
}
.modal button {
padding: 10px 20px;
background: #007bff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.modal button:hover {
background: #0056b3;
}
.error {
color: red;
margin-bottom: 10px;
}

View File

@ -1,53 +1,57 @@
table { table {
width: 100%; width: 100%;
table-layout: fixed; /* Фиксированная ширина столбцов */ table-layout: fixed;
} /* Фиксированная ширина столбцов */
}
th, td { th,
width: 25%; /* Равномерное распределение ширины для 4 столбцов */ td {
padding: 10px; width: 25%;
text-align: left; /* Равномерное распределение ширины для 4 столбцов */
border-bottom: 1px solid #ddd; padding: 10px;
} text-align: left;
border-bottom: 1px solid #ddd;
color: #333;
}
.status { .status {
padding: 5px 10px; padding: 5px 10px;
border-radius: 5px; border-radius: 5px;
color: white; color: white;
} }
.status.normal { .status.normal {
background-color: green; background-color: green;
} }
.status.warning { .status.warning {
background-color: orange; background-color: orange;
} }
.status.critical { .status.critical {
background-color: red; background-color: red;
} }
.details { .details {
padding: 10px; padding: 10px;
background-color: #f9f9f9; background-color: #f9f9f9;
border-radius: 5px; border-radius: 5px;
} }
button { button {
background-color: #007bff; background-color: #007bff;
color: white; color: white;
border: none; border: none;
padding: 5px 10px; padding: 5px 10px;
border-radius: 5px; border-radius: 5px;
cursor: pointer; cursor: pointer;
} }
button:hover { button:hover {
background-color: #0056b3; background-color: #0056b3;
} }
caption { caption {
position: relative; position: relative;
margin-right: 100% ; margin-right: 100%;
} }