| | | 1 | | @using Services.HangfireService.Models |
| | | 2 | | @model IEnumerable<JobDto> |
| | | 3 | | |
| | | 4 | | @{ |
| | 0 | 5 | | var methodColors = new Dictionary<string, string>(); |
| | 0 | 6 | | var colorPalette = new[] { |
| | 0 | 7 | | "#4e79a7", "#f28e2c", "#e15759", "#76b7b2", "#59a14f", |
| | 0 | 8 | | "#edc948", "#b07aa1", "#ff9da7", "#9c755f", "#bab0ab" |
| | 0 | 9 | | }; |
| | 0 | 10 | | int colorIndex = 0; |
| | | 11 | | |
| | 0 | 12 | | foreach (var method in Model |
| | 0 | 13 | | .Where(j => !string.IsNullOrEmpty(j.MethodName)) |
| | 0 | 14 | | .Select(j => j.MethodName) |
| | 0 | 15 | | .Distinct()) |
| | | 16 | | { |
| | 0 | 17 | | methodColors[method!] = colorPalette[colorIndex % colorPalette.Length]; |
| | 0 | 18 | | colorIndex++; |
| | | 19 | | } |
| | | 20 | | |
| | 0 | 21 | | var chartData = Model |
| | 0 | 22 | | .Where(j => j.SucceededAt.HasValue && j.Duration.HasValue) |
| | 0 | 23 | | .OrderBy(j => j.SucceededAt) |
| | 0 | 24 | | .Select(j => new { |
| | 0 | 25 | | time = j.SucceededAt!.Value.ToString("yyyy-MM-ddTHH:mm:ss"), // ISO формат для корректной обработки в JS |
| | 0 | 26 | | durationMs = j.Duration!.Value, |
| | 0 | 27 | | durationMin = j.Duration!.Value / 60000.0, |
| | 0 | 28 | | method = j.MethodName, |
| | 0 | 29 | | color = methodColors[j.MethodName!] |
| | 0 | 30 | | }) |
| | 0 | 31 | | .ToList(); |
| | | 32 | | |
| | 0 | 33 | | var jsonData = System.Text.Json.JsonSerializer.Serialize(chartData); |
| | | 34 | | } |
| | | 35 | | |
| | 0 | 36 | | @if (chartData.Any()) |
| | | 37 | | { |
| | | 38 | | <div class="hq-chart-wrap"> |
| | | 39 | | <canvas id="jobChart" style="height: 400px !important; width: 100%;"></canvas> |
| | | 40 | | </div> |
| | | 41 | | |
| | 0 | 42 | | @if (methodColors.Any()) |
| | | 43 | | { |
| | | 44 | | <div class="mt-2"> |
| | | 45 | | <div class="small text-secondary fw-semibold text-uppercase" style="letter-spacing:.04em;">Легенда методов</div> |
| | | 46 | | <ul class="hq-legend-list"> |
| | 0 | 47 | | @foreach (var kv in methodColors) |
| | | 48 | | { |
| | | 49 | | <li> |
| | | 50 | | <span class="hq-legend-swatch" style="background:@kv.Value;"></span> |
| | 0 | 51 | | <span>@kv.Key</span> |
| | | 52 | | </li> |
| | | 53 | | } |
| | | 54 | | </ul> |
| | | 55 | | </div> |
| | | 56 | | } |
| | | 57 | | |
| | | 58 | | <script src="https://cdn.jsdelivr.net/npm/chart.js"></script> |
| | | 59 | | <script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-zoom@2.0.0"></script> |
| | | 60 | | |
| | | 61 | | <script> |
| | 0 | 62 | | const rawData = @Html.Raw(jsonData); |
| | | 63 | | |
| | | 64 | | const labels = rawData.map(d => new Date(d.time).toLocaleString()); |
| | | 65 | | const data = { |
| | | 66 | | labels: labels, |
| | | 67 | | datasets: [{ |
| | | 68 | | label: 'Длительность (мин)', |
| | | 69 | | data: rawData.map(d => d.durationMin), |
| | | 70 | | backgroundColor: rawData.map(d => d.color), |
| | | 71 | | hoverOffset: 4 |
| | | 72 | | }] |
| | | 73 | | }; |
| | | 74 | | |
| | | 75 | | const config = { |
| | | 76 | | type: 'bar', |
| | | 77 | | data: data, |
| | | 78 | | maintainAspectRatio: false, |
| | | 79 | | options: { |
| | | 80 | | responsive: true, |
| | | 81 | | plugins: { |
| | | 82 | | tooltip: { |
| | | 83 | | callbacks: { |
| | | 84 | | label: function (context) { |
| | | 85 | | const index = context.dataIndex; |
| | | 86 | | const method = rawData[index].method; |
| | | 87 | | const durationMin = rawData[index].durationMin.toFixed(2); |
| | | 88 | | const durationMs = rawData[index].durationMs; |
| | | 89 | | return `${method}: ${durationMin} мин (${durationMs} мс)`; |
| | | 90 | | } |
| | | 91 | | } |
| | | 92 | | }, |
| | | 93 | | zoom: { |
| | | 94 | | zoom: { |
| | | 95 | | wheel: { enabled: true }, |
| | | 96 | | pinch: { enabled: true }, |
| | | 97 | | mode: 'x', |
| | | 98 | | }, |
| | | 99 | | pan: { |
| | | 100 | | enabled: true, |
| | | 101 | | mode: 'x', |
| | | 102 | | }, |
| | | 103 | | limits: { |
| | | 104 | | x: { min: 0, max: rawData.length - 1 }, |
| | | 105 | | } |
| | | 106 | | }, |
| | | 107 | | legend: { |
| | | 108 | | display: false |
| | | 109 | | } |
| | | 110 | | }, |
| | | 111 | | scales: { |
| | | 112 | | x: { |
| | | 113 | | title: { |
| | | 114 | | display: true, |
| | | 115 | | text: 'Время завершения' |
| | | 116 | | }, |
| | | 117 | | ticks: { |
| | | 118 | | maxRotation: 45, |
| | | 119 | | minRotation: 45 |
| | | 120 | | } |
| | | 121 | | }, |
| | | 122 | | y: { |
| | | 123 | | title: { |
| | | 124 | | display: true, |
| | | 125 | | text: 'Длительность (минуты)' |
| | | 126 | | } |
| | | 127 | | } |
| | | 128 | | } |
| | | 129 | | } |
| | | 130 | | }; |
| | | 131 | | |
| | | 132 | | const ctx = document.getElementById('jobChart').getContext('2d'); |
| | | 133 | | new Chart(ctx, config); |
| | | 134 | | </script> |
| | | 135 | | } |
| | | 136 | | else |
| | | 137 | | { |
| | | 138 | | <div class="hq-empty-state"> |
| | | 139 | | <i class="fas fa-chart-bar text-secondary"></i> |
| | | 140 | | <span>Недостаточно данных для графика — нужны succeeded-задачи с длительностью.</span> |
| | | 141 | | </div> |
| | | 142 | | } |