677 lines
41 KiB
Twig
Executable File
677 lines
41 KiB
Twig
Executable File
{% extends "layout.twig" %}
|
||
|
||
{% block content %}
|
||
<div class="row">
|
||
<div class="col-12">
|
||
<div class="card">
|
||
<div class="card-header d-flex justify-content-between align-items-center">
|
||
<h3>
|
||
<i class="fas fa-server"></i>
|
||
{{ server.name }}
|
||
{% if server.group_name %}
|
||
<span class="badge ms-2" {% if server.group_color %}style="background-color: {{ server.group_color }}"{% endif %}>
|
||
<i class="fas {{ server.group_icon|default('fa-box') }} me-1"></i>{{ server.group_name }}
|
||
</span>
|
||
{% endif %}
|
||
</h3>
|
||
<div>
|
||
<a href="/servers/{{ server.id }}/edit" class="btn btn-outline-primary me-2">
|
||
<i class="fas fa-edit"></i> <span class="d-none d-sm-inline">Редактировать</span>
|
||
</a>
|
||
<a href="/servers" class="btn btn-outline-secondary">
|
||
<i class="fas fa-arrow-left"></i> <span class="d-none d-sm-inline">Назад к списку</span>
|
||
</a>
|
||
</div>
|
||
</div>
|
||
<div class="card-body">
|
||
<!-- Информация о сервере -->
|
||
<div class="row mb-4">
|
||
<div class="col-md-6">
|
||
<h5>Информация о сервере</h5>
|
||
<table class="table table-borderless">
|
||
<tr>
|
||
<td><strong>Название:</strong></td>
|
||
<td>{{ server.name }}</td>
|
||
</tr>
|
||
<tr>
|
||
<td><strong>Адрес:</strong></td>
|
||
<td>{{ server.address|default('-') }}</td>
|
||
</tr>
|
||
<tr>
|
||
<td><strong>Группа:</strong></td>
|
||
<td>
|
||
{% if server.group_name %}
|
||
<i class="fas {{ server.group_icon|default('fa-box') }}" {% if server.group_color %}style="color: {{ server.group_color }}"{% endif %}></i> {{ server.group_name }}
|
||
{% else %}
|
||
-
|
||
{% endif %}
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<td><strong>Описание:</strong></td>
|
||
<td>{{ server.description|default('-') }}</td>
|
||
</tr>
|
||
<tr>
|
||
<td><strong>Последние метрики:</strong></td>
|
||
<td>
|
||
{% if server.last_metrics_at %}
|
||
{{ server.last_metrics_at|date('d.m.Y H:i:s') }}
|
||
{% else %}
|
||
Нет данных
|
||
{% endif %}
|
||
</td>
|
||
</tr>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Вкладки -->
|
||
<ul class="nav nav-tabs" id="serverTabs" role="tablist">
|
||
<li class="nav-item">
|
||
<button class="nav-link active" id="metrics-tab" data-bs-toggle="tab" data-bs-target="#metrics" type="button" role="tab">
|
||
<i class="fas fa-chart-line"></i> Метрики
|
||
</button>
|
||
</li>
|
||
<li class="nav-item">
|
||
<button class="nav-link" id="services-tab" data-bs-toggle="tab" data-bs-target="#services" type="button" role="tab">
|
||
<i class="fas fa-cogs"></i> Сервисы
|
||
</button>
|
||
</li>
|
||
<li class="nav-item">
|
||
<button class="nav-link" id="thresholds-tab" data-bs-toggle="tab" data-bs-target="#thresholds" type="button" role="tab">
|
||
<i class="fas fa-bell"></i> Пороги
|
||
</button>
|
||
</li>
|
||
</ul>
|
||
|
||
<!-- Содержимое вкладок -->
|
||
<div class="tab-content mt-3">
|
||
<!-- Вкладка "Метрики" -->
|
||
<div class="tab-pane fade show active" id="metrics" role="tabpanel">
|
||
<div class="row mb-3">
|
||
<div class="col-md-12">
|
||
<form method="get" class="row g-2 align-items-end" id="periodForm">
|
||
<input type="hidden" name="tab" value="metrics">
|
||
|
||
<!-- Пресеты -->
|
||
<div class="col-md-3">
|
||
<label class="form-label mb-1">
|
||
<i class="fas fa-clock"></i> Быстрый выбор
|
||
</label>
|
||
<select class="form-select" id="presetSelect" onchange="applyPreset()">
|
||
<option value="">-- Выбрать --</option>
|
||
<option value="30">Последние 30 минут</option>
|
||
<option value="60">Последние 1 час</option>
|
||
<option value="120">Последние 2 часа</option>
|
||
<option value="360">Последние 6 часов</option>
|
||
<option value="720">Последние 12 часов</option>
|
||
<option value="1440">Последние 24 часа</option>
|
||
</select>
|
||
</div>
|
||
|
||
<!-- Дата начала -->
|
||
<div class="col-md-3">
|
||
<label class="form-label mb-1">
|
||
<i class="fas fa-calendar"></i> С
|
||
</label>
|
||
<input type="datetime-local" class="form-control" name="start" id="startDate"
|
||
value="{{ startDate }}" required>
|
||
</div>
|
||
|
||
<!-- Дата окончания -->
|
||
<div class="col-md-3">
|
||
<label class="form-label mb-1">
|
||
<i class="fas fa-calendar"></i> По
|
||
</label>
|
||
<input type="datetime-local" class="form-control" name="end" id="endDate"
|
||
value="{{ endDate }}" required>
|
||
</div>
|
||
|
||
<!-- Кнопки -->
|
||
<div class="col-md-3">
|
||
<button type="submit" class="btn btn-primary w-100">
|
||
<i class="fas fa-search"></i> Применить
|
||
</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="row">
|
||
{% for metricName, metricData in metrics %}
|
||
{% if metricName!="top_cpu_proc" and metricName!="top_ram_proc" %}
|
||
<div class="col-12 mb-4">
|
||
<div class="card">
|
||
<div class="card-header">
|
||
<h6 class="mb-0">
|
||
{{ metricName|replace({'_': ' ', 'load': 'загрузка', 'used': 'использование'})|title }}
|
||
{% if metricData[0].unit %}<small class="text-muted">({{ metricData[0].unit }})</small>{% endif %}
|
||
</h6>
|
||
</div>
|
||
<div class="card-body">
|
||
{% if metricData %}
|
||
<h3 class="text-center text-end">{{ metricData[0].value }}{{ metricData[0].unit|default('') }}</h3>
|
||
<p class="text-muted text-center mb-2">
|
||
{{ metricData[0].created_at|date('d.m.Y H:i:s') }}
|
||
</p>
|
||
|
||
<!-- График для метрики -->
|
||
<div>
|
||
<canvas id="chart-{{ metricName }}" width="100%" height="200"></canvas>
|
||
</div>
|
||
{% else %}
|
||
<p class="text-center text-muted">Нет данных за этот период</p>
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{% endif %}
|
||
{% endfor %}
|
||
|
||
{% if metrics|length == 0 %}
|
||
<div class="col-12">
|
||
<div class="alert alert-info text-center">
|
||
<i class="fas fa-info-circle"></i> Нет данных о метриках за выбранный период
|
||
</div>
|
||
</div>
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Вкладка "Сервисы" -->
|
||
<div class="tab-pane fade" id="services" role="tabpanel">
|
||
<div class="row mb-3">
|
||
<div class="col-12">
|
||
<div class="d-flex justify-content-between align-items-center">
|
||
<div>
|
||
<h4>
|
||
<i class="fas fa-cogs"></i> Сервисы сервера
|
||
{% if allServices is defined %}
|
||
<small class="text-muted">(найдено: {{ allServices|length }})</small>
|
||
{% endif %}
|
||
</h4>
|
||
</div>
|
||
<div>
|
||
<a href="?tab=services" class="btn btn-outline-primary">
|
||
<i class="fas fa-sync-alt"></i> <span class="d-none d-sm-inline">Обновить список</span>
|
||
</a>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="row mb-3">
|
||
<div class="col-12">
|
||
<form method="post" action="/servers/{{ server.id }}/services">
|
||
<div class="card">
|
||
<div class="card-body">
|
||
<div class="form-check mb-2">
|
||
<input class="form-check-input" type="checkbox" id="selectAllServices">
|
||
<label class="form-check-label" for="selectAllServices">
|
||
Выбрать все
|
||
</label>
|
||
</div>
|
||
|
||
<hr>
|
||
|
||
<div class="row" id="servicesList">
|
||
{% if allServices is defined and allServices|length > 0 %}
|
||
{# Сортируем сервисы: сначала running, потом stopped, потом unknown, затем по имени #}
|
||
{% set runningServices = allServices|filter(s => s.status == 'running')|sort((a, b) => a.service_name|lower > b.service_name|lower) %}
|
||
{% set stoppedServices = allServices|filter(s => s.status == 'stopped')|sort((a, b) => a.service_name|lower > b.service_name|lower) %}
|
||
{% set unknownServices = allServices|filter(s => s.status != 'running' and s.status != 'stopped')|sort((a, b) => a.service_name|lower > b.service_name|lower) %}
|
||
|
||
{# Выводим все сервисы по порядку #}
|
||
{% for service in runningServices %}
|
||
<div class="col-md-4 col-lg-3 mb-2">
|
||
<div class="card border-success">
|
||
<div class="card-body p-2">
|
||
<div class="d-flex justify-content-between align-items-start">
|
||
<div class="form-check mb-0">
|
||
<input class="form-check-input service-checkbox"
|
||
type="checkbox"
|
||
id="service_{{ service.service_name }}"
|
||
name="services[]"
|
||
value="{{ service.service_name }}"
|
||
{% if service.service_name in monitorServices %}
|
||
checked
|
||
{% endif %}>
|
||
<label class="form-check-label" for="service_{{ service.service_name }}">
|
||
<i class="fas fa-check-circle text-success me-1"></i>
|
||
{{ service.service_name }}
|
||
</label>
|
||
</div>
|
||
<div class="text-end">
|
||
<span class="badge bg-success mb-1">running</span>
|
||
</div>
|
||
</div>
|
||
<div class="small text-muted mt-1">
|
||
<small>Load: {{ service.load_state|default('-') }} | Active: {{ service.active_state|default('-') }}</small>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{% endfor %}
|
||
|
||
{% for service in stoppedServices %}
|
||
<div class="col-md-4 col-lg-3 mb-2">
|
||
<div class="card border-danger">
|
||
<div class="card-body p-2">
|
||
<div class="d-flex justify-content-between align-items-start">
|
||
<div class="form-check mb-0">
|
||
<input class="form-check-input service-checkbox"
|
||
type="checkbox"
|
||
id="service_{{ service.service_name }}"
|
||
name="services[]"
|
||
value="{{ service.service_name }}"
|
||
{% if service.service_name in monitorServices %}
|
||
checked
|
||
{% endif %}>
|
||
<label class="form-check-label" for="service_{{ service.service_name }}">
|
||
<i class="fas fa-times-circle text-danger me-1"></i>
|
||
{{ service.service_name }}
|
||
</label>
|
||
</div>
|
||
<div class="text-end">
|
||
<span class="badge bg-danger mb-1">stopped</span>
|
||
</div>
|
||
</div>
|
||
<div class="small text-muted mt-1">
|
||
<small>Load: {{ service.load_state|default('-') }} | Active: {{ service.active_state|default('-') }}</small>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{% endfor %}
|
||
|
||
{% for service in unknownServices %}
|
||
<div class="col-md-4 col-lg-3 mb-2">
|
||
<div class="card border-warning">
|
||
<div class="card-body p-2">
|
||
<div class="d-flex justify-content-between align-items-start">
|
||
<div class="form-check mb-0">
|
||
<input class="form-check-input service-checkbox"
|
||
type="checkbox"
|
||
id="service_{{ service.service_name }}"
|
||
name="services[]"
|
||
value="{{ service.service_name }}"
|
||
{% if service.service_name in monitorServices %}
|
||
checked
|
||
{% endif %}>
|
||
<label class="form-check-label" for="service_{{ service.service_name }}">
|
||
<i class="fas fa-question-circle text-warning me-1"></i>
|
||
{{ service.service_name }}
|
||
</label>
|
||
</div>
|
||
<div class="text-end">
|
||
<span class="badge bg-warning mb-1">unknown</span>
|
||
</div>
|
||
</div>
|
||
<div class="small text-muted mt-1">
|
||
<small>Load: {{ service.load_state|default('-') }} | Active: {{ service.active_state|default('-') }}</small>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{% endfor %}
|
||
{% else %}
|
||
<div class="col-12">
|
||
<div class="alert alert-warning text-center">
|
||
<i class="fas fa-exclamation-triangle"></i> Агент не отправил список сервисов или не установлен
|
||
</div>
|
||
</div>
|
||
<div class="col-12 text-center mt-3">
|
||
<button type="button" class="btn btn-outline-primary" onclick="requestServices()">
|
||
<i class="fas fa-download"></i> Запросить список сервисов
|
||
</button>
|
||
</div>
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="d-grid gap-2 d-md-flex justify-content-md-end mt-3">
|
||
<button type="submit" class="btn btn-primary">
|
||
<i class="fas fa-save"></i> Сохранить конфигурацию
|
||
</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Вкладка "Пороги" -->
|
||
<div class="tab-pane fade" id="thresholds" role="tabpanel">
|
||
<h4>Настройка порогов</h4>
|
||
<div class="row">
|
||
<div class="col-md-8">
|
||
<form method="post" action="/servers/{{ server.id }}/thresholds">
|
||
{% for metricType in allMetricTypes %}
|
||
<div class="card mb-3">
|
||
<div class="card-header">
|
||
<h6 class="mb-0">
|
||
{{ metricType.name|replace({'_': ' ', 'load': 'загрузка', 'used': 'использование'})|title }}
|
||
{% if metricType.unit %}<small class="text-muted">({{ metricType.unit }})</small>{% endif %}
|
||
</h6>
|
||
</div>
|
||
<div class="card-body">
|
||
<div class="row">
|
||
<div class="col-md-6">
|
||
<label class="form-label">Порог предупреждения</label>
|
||
<input type="number" class="form-control"
|
||
name="{{ metricType.name }}_warning"
|
||
step="0.01"
|
||
{% if existingThresholds[metricType.name].warning is defined %}
|
||
value="{{ existingThresholds[metricType.name].warning }}"
|
||
{% endif %}
|
||
placeholder="80.00">
|
||
</div>
|
||
<div class="col-md-6">
|
||
<label class="form-label">Порог критический</label>
|
||
<input type="number" class="form-control"
|
||
name="{{ metricType.name }}_critical"
|
||
step="0.01"
|
||
{% if existingThresholds[metricType.name].critical is defined %}
|
||
value="{{ existingThresholds[metricType.name].critical }}"
|
||
{% endif %}
|
||
placeholder="90.00">
|
||
</div>
|
||
</div>
|
||
<div class="row mt-3">
|
||
<div class="col-md-12">
|
||
<label class="form-label">
|
||
<i class="fas fa-clock"></i> Длительность превышения (минуты)
|
||
</label>
|
||
<input type="number" class="form-control"
|
||
name="{{ metricType.name }}_duration"
|
||
min="0"
|
||
step="1"
|
||
{% if existingThresholds[metricType.name].duration is defined %}
|
||
value="{{ existingThresholds[metricType.name].duration }}"
|
||
{% endif %}
|
||
placeholder="0 - отправлять алерт сразу">
|
||
<small class="text-muted">
|
||
0 = алерт сразу при превышении, >0 = алерт только если превышено дольше указанного времени
|
||
</small>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{% endfor %}
|
||
|
||
<div class="d-grid gap-2 d-md-flex justify-content-md-end">
|
||
<button type="submit" class="btn btn-primary">
|
||
<i class="fas fa-save"></i> Сохранить пороги
|
||
</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Chart.js -->
|
||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||
<script>
|
||
|
||
// Функция для получения топ-процессов для указанного времени
|
||
function fetchProcesses(serverId, time) {
|
||
return new Promise(function(resolve) {
|
||
var fullTime = time;
|
||
if (time && time.indexOf('-') === -1) {
|
||
var now = new Date();
|
||
var year = now.getFullYear();
|
||
var month = String(now.getMonth() + 1).padStart(2, '0');
|
||
var day = String(now.getDate()).padStart(2, '0');
|
||
fullTime = year + '-' + month + '-' + day + ' ' + time;
|
||
}
|
||
fetch('/api/v1/agent/' + serverId + '/processes?time=' + encodeURIComponent(fullTime))
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
var lines = [];
|
||
|
||
// Добавляем топ-процессы CPU
|
||
if (data.top_cpu && data.top_cpu.length > 0) {
|
||
lines.push('');
|
||
lines.push('🏆 Топ CPU:');
|
||
data.top_cpu.forEach(function(proc) {
|
||
lines.push(' ' + proc.name + ': ' + proc.value + '%');
|
||
});
|
||
}
|
||
|
||
// Добавляем топ-процессы RAM
|
||
if (data.top_ram && data.top_ram.length > 0) {
|
||
lines.push('');
|
||
lines.push('💾 Топ RAM:');
|
||
data.top_ram.forEach(function(proc) {
|
||
lines.push(' ' + proc.name + ': ' + proc.value + '%');
|
||
});
|
||
}
|
||
|
||
resolve(lines);
|
||
})
|
||
.catch(function() {
|
||
resolve([]);
|
||
});
|
||
});
|
||
}
|
||
|
||
|
||
// Функция для получения списка сервисов
|
||
function requestServices() {
|
||
fetch('/api/v1/agent/{{ server.id }}/services')
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (data.services) {
|
||
location.reload();
|
||
}
|
||
})
|
||
.catch(error => {
|
||
alert('Ошибка получения списка сервисов: ' + error);
|
||
});
|
||
}
|
||
|
||
// Обработчик кнопки "Выбрать все"
|
||
document.getElementById('selectAllServices').addEventListener('change', function() {
|
||
const checkboxes = document.querySelectorAll('.service-checkbox');
|
||
checkboxes.forEach(checkbox => {
|
||
checkbox.checked = this.checked;
|
||
});
|
||
});
|
||
|
||
// Активация нужной вкладки при загрузке
|
||
document.addEventListener('DOMContentLoaded', function() {
|
||
const urlParams = new URLSearchParams(window.location.search);
|
||
const activeTab = urlParams.get('tab') || 'metrics';
|
||
|
||
// Находим кнопку нужной вкладки и кликаем
|
||
const tabButton = document.getElementById(activeTab + '-tab');
|
||
if (tabButton) {
|
||
tabButton.click();
|
||
}
|
||
});
|
||
|
||
// Графики метрик
|
||
{% for metricName, metricData in metrics %}
|
||
{% if metricName!="top_cpu_proc" and metricName!="top_ram_proc" %}
|
||
const ctx{{ metricName|replace({'-': '_', '.': '_'}) }} = document.getElementById('chart-{{ metricName }}').getContext('2d');
|
||
|
||
// Подготовка данных для графика
|
||
var labels{{ metricName }} = [];
|
||
var data{{ metricName }} = [];
|
||
|
||
{% for metric in metricData|slice(0, 400)|reverse %}
|
||
{% set time_val = metric.time_bucket|default(metric.created_at) %}
|
||
{% set time_format = metric.time_bucket and aggregation.aggregate_minutes >= 60 ? 'd.m H:i' : 'H:i' %}
|
||
labels{{ metricName }}.push('{{ time_val|date(time_format) }}');
|
||
data{{ metricName }}.push({{ metric.value|raw }});
|
||
{% endfor %}
|
||
|
||
const chart{{ metricName|replace({'-': '_', '.': '_'}) }} = new Chart(ctx{{ metricName|replace({'-': '_', '.': '_'}) }}, {
|
||
type: 'line',
|
||
data: {
|
||
labels: labels{{ metricName }},
|
||
datasets: [{
|
||
label: '{{ metricName|replace({'_': ' ', 'load': 'загрузка', 'used': 'использование'})|title }}',
|
||
data: data{{ metricName }},
|
||
borderColor: 'rgb(75, 192, 192)',
|
||
backgroundColor: 'rgba(75, 192, 192, 0.2)',
|
||
tension: 0.1
|
||
}]
|
||
},
|
||
options: {
|
||
responsive: true,
|
||
maintainAspectRatio: false,
|
||
scales: {
|
||
y: {
|
||
beginAtZero: false
|
||
}
|
||
},
|
||
plugins: {
|
||
tooltip: {
|
||
enabled: false,
|
||
mode: 'index',
|
||
intersect: false,
|
||
{% set metricType = metricName %}
|
||
|
||
external: function(context) {
|
||
// Tooltip element
|
||
var tooltipEl = document.getElementById('chartjs-tooltip-' + {{ server.id }} + '-' + '{{ metricName }}');
|
||
|
||
if (!tooltipEl) {
|
||
tooltipEl = document.createElement('div');
|
||
tooltipEl.id = 'chartjs-tooltip-' + {{ server.id }} + '-' + '{{ metricName }}';
|
||
tooltipEl.style.opacity = 0;
|
||
tooltipEl.style.position = 'absolute';
|
||
tooltipEl.style.background = 'rgba(0,0,0,0.7)';
|
||
tooltipEl.style.color = 'white';
|
||
tooltipEl.style.borderRadius = '3px';
|
||
tooltipEl.style.padding = '10px';
|
||
tooltipEl.style.fontSize = '12px';
|
||
tooltipEl.style.pointerEvents = 'none';
|
||
document.body.appendChild(tooltipEl);
|
||
}
|
||
|
||
// Прячем если курсор ушел с графика
|
||
if (!context.tooltip._active || context.tooltip._active.length === 0 || canvas{{ metricName|replace({'-': '_', '.': '_'}) }}._tooltipHidden) {
|
||
tooltipEl.style.opacity = 0;
|
||
return;
|
||
}
|
||
|
||
var dataIndex = context.tooltip._active[0].index;
|
||
var time = labels{{ metricName }}[dataIndex];
|
||
|
||
// Fetch processes
|
||
fetch('/api/v1/agent/' + {{ server.id }} + '/processes?time=' + encodeURIComponent(time))
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
var lines = [];
|
||
lines.push('Время: ' + time);
|
||
lines.push('Значение: ' + data{{ metricName }}[dataIndex]);
|
||
{% if metricName == 'cpu_load' %}
|
||
// Показываем только top_cpu
|
||
if (data.top_cpu && data.top_cpu.length > 0) {
|
||
lines.push('');
|
||
lines.push('TOP CPU:');
|
||
data.top_cpu.forEach(function(proc) {
|
||
lines.push(' ' + proc.name + ': ' + proc.value + '%');
|
||
});
|
||
}
|
||
{% elseif metricName == 'ram_used' %}
|
||
// Показываем только top_ram
|
||
if (data.top_ram && data.top_ram.length > 0) {
|
||
lines.push('');
|
||
lines.push('TOP RAM:');
|
||
data.top_ram.forEach(function(proc) {
|
||
lines.push(' ' + proc.name + ': ' + proc.value + '%');
|
||
});
|
||
}
|
||
{% endif %}
|
||
|
||
// Show tooltip
|
||
var position = context.chart.canvas.getBoundingClientRect();
|
||
tooltipEl.innerHTML = lines.join('<br>');
|
||
tooltipEl.style.visibility = 'visible';
|
||
tooltipEl.style.opacity = 1;
|
||
tooltipEl.style.left = position.left + window.pageXOffset + context.tooltip.caretX + 10 + 'px';
|
||
tooltipEl.style.top = position.top + window.pageYOffset + context.tooltip.caretY + 'px';
|
||
|
||
// Hide after 3 seconds
|
||
// setTimeout(function() {
|
||
// tooltipEl.style.opacity = 0;
|
||
// }, 3000);
|
||
});
|
||
}
|
||
}
|
||
}
|
||
}
|
||
});
|
||
|
||
// Скрывать tooltip при уходе курсора с canvas в любую сторону
|
||
var canvas{{ metricName|replace({'-': '_', '.': '_'}) }} = document.getElementById('chart-{{ metricName }}');
|
||
if (canvas{{ metricName|replace({'-': '_', '.': '_'}) }}) {
|
||
canvas{{ metricName|replace({'-': '_', '.': '_'}) }}._tooltipHidden = false;
|
||
|
||
canvas{{ metricName|replace({'-': '_', '.': '_'}) }}.addEventListener('mouseout', function() {
|
||
canvas{{ metricName|replace({'-': '_', '.': '_'}) }}._tooltipHidden = true;
|
||
var tooltipEl = document.getElementById('chartjs-tooltip-{{ server.id }}-{{ metricName }}');
|
||
if (tooltipEl) {
|
||
tooltipEl.style.opacity = '0';
|
||
}
|
||
});
|
||
|
||
canvas{{ metricName|replace({'-': '_', '.': '_'}) }}.addEventListener('mouseleave', function() {
|
||
canvas{{ metricName|replace({'-': '_', '.': '_'}) }}._tooltipHidden = true;
|
||
var tooltipEl = document.getElementById('chartjs-tooltip-{{ server.id }}-{{ metricName }}');
|
||
if (tooltipEl) {
|
||
tooltipEl.style.opacity = '0';
|
||
}
|
||
});
|
||
|
||
canvas{{ metricName|replace({'-': '_', '.': '_'}) }}.addEventListener('mousemove', function() {
|
||
canvas{{ metricName|replace({'-': '_', '.': '_'}) }}._tooltipHidden = false;
|
||
});
|
||
}
|
||
|
||
{% endif %}
|
||
{% endfor %}
|
||
</script>
|
||
<script>
|
||
// Применение пресета
|
||
function applyPreset() {
|
||
var select = document.getElementById("presetSelect");
|
||
var minutes = parseInt(select.value);
|
||
if (!minutes) return;
|
||
|
||
var now = new Date();
|
||
var startDate = new Date(now.getTime() - (minutes * 60 * 1000));
|
||
|
||
// Форматируем для datetime-local (YYYY-MM-DDTHH:MM)
|
||
function formatDateTimeLocal(date) {
|
||
var year = date.getFullYear();
|
||
var month = String(date.getMonth() + 1).padStart(2, "0");
|
||
var day = String(date.getDate()).padStart(2, "0");
|
||
var hours = String(date.getHours()).padStart(2, "0");
|
||
var mins = String(date.getMinutes()).padStart(2, "0");
|
||
return year + "-" + month + "-" + day + "T" + hours + ":" + mins;
|
||
}
|
||
|
||
document.getElementById("startDate").value = formatDateTimeLocal(startDate);
|
||
document.getElementById("endDate").value = formatDateTimeLocal(now);
|
||
}
|
||
|
||
// Автоотправка формы при изменении дат (опционально)
|
||
// document.getElementById("startDate").addEventListener("change", function() {
|
||
// document.getElementById("periodForm").submit();
|
||
// });
|
||
// document.getElementById("endDate").addEventListener("change", function() {
|
||
// document.getElementById("periodForm").submit();
|
||
// });
|
||
</script>
|
||
|
||
{% endblock %}
|