669 lines
41 KiB
Twig
Executable File
669 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">
|
||
<!-- Отладка: period = {{ request.query.period }} -->
|
||
<!-- Отладка: period = {{ period }}, request = {{ request.period }} -->
|
||
<div class="btn-group d-flex" role="group">
|
||
<a href="?tab=metrics&period=24h" class="btn btn-outline-primary w-100 {% if period == '24h' or period is empty %}active{% endif %}">
|
||
24 часа
|
||
</a>
|
||
<a href="?tab=metrics&period=7d" class="btn btn-outline-primary w-100 {% if period == '7d' %}active{% endif %}">
|
||
7 дней
|
||
</a>
|
||
<a href="?tab=metrics&period=30d" class="btn btn-outline-primary w-100 {% if period == '30d' %}active{% endif %}">
|
||
30 дней
|
||
</a>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="row mt-2">
|
||
<div class="col-md-12">
|
||
<small class="text-muted">Масштаб:</small>
|
||
<div class="btn-group ms-2" role="group">
|
||
<a href="?tab=metrics&period={{ period }}&zoom=auto" class="btn btn-sm {% if not zoom or zoom == 'auto' %}btn-secondary{% else %}btn-outline-secondary{% endif %}">
|
||
авто
|
||
</a>
|
||
<a href="?tab=metrics&period={{ period }}&zoom=1h" class="btn btn-sm {% if zoom == '1h' %}btn-secondary{% else %}btn-outline-secondary{% endif %}">
|
||
1ч
|
||
</a>
|
||
<a href="?tab=metrics&period={{ period }}&zoom=6h" class="btn btn-sm {% if zoom == '6h' %}btn-secondary{% else %}btn-outline-secondary{% endif %}">
|
||
6ч
|
||
</a>
|
||
<a href="?tab=metrics&period={{ period }}&zoom=24h" class="btn btn-sm {% if zoom == '24h' %}btn-secondary{% else %}btn-outline-secondary{% endif %}">
|
||
24ч
|
||
</a>
|
||
<a href="?tab=metrics&period={{ period }}&zoom=7d" class="btn btn-sm {% if zoom == '7d' %}btn-secondary{% else %}btn-outline-secondary{% endif %}">
|
||
7д
|
||
</a>
|
||
<a href="?tab=metrics&period={{ period }}&zoom=30d" class="btn btn-sm {% if zoom == '30d' %}btn-secondary{% else %}btn-outline-secondary{% endif %}">
|
||
30д
|
||
</a>
|
||
</div>
|
||
</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 src="/chartjs-plugin-crosshair.min.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, 50000)|reverse %}
|
||
{% set time_val = metric.time_bucket|default(metric.created_at) %}
|
||
{% set time_format = metric.time_bucket ? '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: {
|
||
crosshair: {
|
||
line: {
|
||
color: "rgba(100, 150, 255, 0.4)",
|
||
width: 1,
|
||
dashPattern: []
|
||
},
|
||
sync: {
|
||
enabled: false
|
||
},
|
||
zoom: {
|
||
enabled: false
|
||
}
|
||
},
|
||
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) {
|
||
tooltipEl.style.opacity = 0;
|
||
return;
|
||
}
|
||
|
||
// Проверка: курсор внутри chartArea (все 4 границы)
|
||
var chartArea = context.chart.chartArea;
|
||
if (chartArea && context.tooltip.caretX !== undefined && context.tooltip.caretY !== undefined) {
|
||
if (context.tooltip.caretX < chartArea.left || context.tooltip.caretX > chartArea.right ||
|
||
context.tooltip.caretY < chartArea.top || context.tooltip.caretY > chartArea.bottom) {
|
||
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.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 в любую сторону
|
||
chart{{ metricName|replace({'-': '_', '.': '_'}) }}.canvas.addEventListener('mouseleave', function() {
|
||
var tooltipEl = document.getElementById('chartjs-tooltip-{{ server.id }}-{{ metricName }}');
|
||
if (tooltipEl) {
|
||
tooltipEl.style.opacity = 0;
|
||
}
|
||
chart{{ metricName|replace({'-': '_', '.': '_'}) }}.tooltip._active = [];
|
||
chart{{ metricName|replace({'-': '_', '.': '_'}) }}.draw();
|
||
});
|
||
|
||
{% endif %}
|
||
{% endfor %}
|
||
|
||
// Глобальный обработчик mousemove для скрытия тултипов при уходе курсора за пределы canvas
|
||
// (нужен т.к. crosshair плагин создаёт overlay canvas и перехватывает mouseleave)
|
||
document.addEventListener('mousemove', function(e) {
|
||
{% for metricName, metricData in metrics %}
|
||
{% if metricName!="top_cpu_proc" and metricName!="top_ram_proc" %}
|
||
(function() {
|
||
var canvas = chart{{ metricName|replace({'-': '_', '.': '_'}) }}.canvas;
|
||
var rect = canvas.getBoundingClientRect();
|
||
var isOverCanvas = (e.clientX >= rect.left && e.clientX <= rect.right &&
|
||
e.clientY >= rect.top && e.clientY <= rect.bottom);
|
||
if (!isOverCanvas) {
|
||
var tooltipEl = document.getElementById('chartjs-tooltip-{{ server.id }}-{{ metricName }}');
|
||
if (tooltipEl && tooltipEl.style.opacity !== '0') {
|
||
tooltipEl.style.opacity = 0;
|
||
chart{{ metricName|replace({'-': '_', '.': '_'}) }}.tooltip._active = [];
|
||
chart{{ metricName|replace({'-': '_', '.': '_'}) }}.draw();
|
||
}
|
||
}
|
||
})();
|
||
{% endif %}
|
||
{% endfor %}
|
||
});
|
||
|
||
</script>
|
||
{% endblock %}
|