Skip to content

Commit e03161e

Browse files
committed
analytics: Show event details
Signed-off-by: Denys Fedoryshchenko <denys.f@collabora.com>
1 parent 24ff43e commit e03161e

1 file changed

Lines changed: 272 additions & 18 deletions

File tree

api/templates/analytics.html

Lines changed: 272 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,43 @@
6464
.nav-links a { color: rgba(255,255,255,0.8); text-decoration: none; margin-left: 16px; }
6565
.nav-links a:hover { color: white; }
6666
.chart-container { position: relative; height: 300px; }
67+
.anomaly-alert { cursor: pointer; }
68+
.anomaly-alert:hover { opacity: 0.9; }
69+
.stats-clickable tr { cursor: pointer; }
70+
.stats-clickable tr:hover { background-color: #e9ecef; }
71+
.detail-panel {
72+
background: white;
73+
border-radius: 12px;
74+
box-shadow: 0 4px 20px rgba(0,0,0,0.15);
75+
padding: 24px;
76+
margin-bottom: 24px;
77+
border-left: 4px solid #667eea;
78+
}
79+
.detail-panel .detail-header {
80+
display: flex;
81+
justify-content: space-between;
82+
align-items: center;
83+
margin-bottom: 16px;
84+
}
85+
.detail-panel .detail-header h5 { margin: 0; }
86+
.event-card {
87+
background: #f8f9fa;
88+
border-radius: 8px;
89+
padding: 14px;
90+
margin-bottom: 10px;
91+
border: 1px solid #e9ecef;
92+
}
93+
.event-card:hover { border-color: #667eea; }
94+
.event-field { margin-bottom: 4px; }
95+
.event-field .field-name {
96+
font-weight: 600;
97+
color: #6c757d;
98+
font-size: 0.8rem;
99+
text-transform: uppercase;
100+
}
101+
.event-field .field-value { font-size: 0.9rem; }
102+
.node-link { color: #667eea; text-decoration: underline; cursor: pointer; }
103+
.node-link:hover { color: #764ba2; }
67104
</style>
68105
</head>
69106
<body>
@@ -311,6 +348,26 @@ <h5><i class="bi bi-heart-pulse"></i> Runtime Health Overview</h5>
311348
</div>
312349
</div>
313350

351+
<!-- Event Detail Drill-down Panel -->
352+
<div class="container-fluid px-4" id="detailContainer" style="display:none;">
353+
<div class="detail-panel" id="detailPanel">
354+
<div class="detail-header">
355+
<h5><i class="bi bi-list-ul"></i> <span id="detailTitle">Event Details</span></h5>
356+
<div>
357+
<span class="text-muted me-3" id="detailCount"></span>
358+
<button class="btn btn-sm btn-outline-secondary" id="detailClose">
359+
<i class="bi bi-x-lg"></i> Close
360+
</button>
361+
</div>
362+
</div>
363+
<div id="detailLoading" class="text-center py-3" style="display:none;">
364+
<div class="spinner-border spinner-border-sm text-primary"></div>
365+
<span class="ms-2">Loading events...</span>
366+
</div>
367+
<div id="detailContent"></div>
368+
</div>
369+
</div>
370+
314371
<!-- Loading Overlay -->
315372
<div class="loading-overlay" id="loadingOverlay" style="display:none;">
316373
<div class="spinner-border text-primary" style="width:3rem;height:3rem;" role="status">
@@ -503,8 +560,156 @@ <h5><i class="bi bi-heart-pulse"></i> Runtime Health Overview</h5>
503560
$('#skipCount').text(formatNumber(totals.skip));
504561
}
505562

563+
function escapeHtml(str) {
564+
if (!str) return '';
565+
return str.replace(/&/g, '&amp;').replace(/</g, '&lt;')
566+
.replace(/>/g, '&gt;').replace(/"/g, '&quot;');
567+
}
568+
569+
function resultBadge(result) {
570+
var colors = {
571+
pass: 'bg-success', fail: 'bg-danger',
572+
incomplete: 'bg-info', skip: 'bg-secondary'
573+
};
574+
var cls = colors[result] || 'bg-secondary';
575+
return '<span class="badge ' + cls + '">' + (result || 'N/A') + '</span>';
576+
}
577+
578+
function renderEventCard(evt) {
579+
var ts = evt.ts ? new Date(evt.ts).toLocaleString() : 'N/A';
580+
var html = '<div class="event-card">';
581+
html += '<div class="row">';
582+
583+
// Left column: key fields
584+
html += '<div class="col-md-8">';
585+
html += '<div class="d-flex align-items-center mb-2">';
586+
html += '<span class="badge bg-secondary me-2">' + escapeHtml(evt.kind) + '</span>';
587+
html += resultBadge(evt.result);
588+
if (evt.is_infra_error) {
589+
html += ' <span class="badge ms-1" style="background:#fd7e14">infra error</span>';
590+
}
591+
html += '<small class="text-muted ms-auto">' + ts + '</small>';
592+
html += '</div>';
593+
594+
var fields = [
595+
['Runtime', evt.runtime],
596+
['Device Type', evt.device_type],
597+
['Device ID', evt.device_id],
598+
['Job Name', evt.job_name],
599+
['Test Name', evt.test_name],
600+
['Tree', evt.tree],
601+
['Branch', evt.branch],
602+
['Arch', evt.arch],
603+
];
604+
fields.forEach(function(f) {
605+
if (f[1]) {
606+
html += '<div class="event-field d-inline-block me-4">';
607+
html += '<span class="field-name">' + f[0] + ': </span>';
608+
html += '<span class="field-value">' + escapeHtml(f[1]) + '</span>';
609+
html += '</div>';
610+
}
611+
});
612+
html += '</div>';
613+
614+
// Right column: IDs, errors, links
615+
html += '<div class="col-md-4">';
616+
if (evt.node_id) {
617+
html += '<div class="event-field">';
618+
html += '<span class="field-name">Node: </span>';
619+
html += '<a class="node-link" href="/viewer?node_id=' +
620+
encodeURIComponent(evt.node_id) +
621+
'" target="_blank">' + escapeHtml(evt.node_id) + '</a>';
622+
html += '</div>';
623+
}
624+
if (evt.job_id) {
625+
html += '<div class="event-field">';
626+
html += '<span class="field-name">Job ID: </span>';
627+
html += '<span class="field-value">' + escapeHtml(evt.job_id) + '</span>';
628+
html += '</div>';
629+
}
630+
if (evt.error_type) {
631+
html += '<div class="event-field">';
632+
html += '<span class="field-name">Error Type: </span>';
633+
html += '<span class="badge" style="background:#fd7e14">' +
634+
escapeHtml(evt.error_type) + '</span>';
635+
html += '</div>';
636+
}
637+
if (evt.error_msg) {
638+
html += '<div class="event-field">';
639+
html += '<span class="field-name">Error Msg: </span>';
640+
html += '<span class="field-value text-danger">' +
641+
escapeHtml(evt.error_msg) + '</span>';
642+
html += '</div>';
643+
}
644+
if (evt.retry > 0) {
645+
html += '<div class="event-field">';
646+
html += '<span class="field-name">Retry: </span>';
647+
html += '<span class="field-value">#' + evt.retry + '</span>';
648+
html += '</div>';
649+
}
650+
html += '</div>';
651+
652+
html += '</div></div>';
653+
return html;
654+
}
655+
656+
function showDetail(title, filters) {
657+
var container = $('#detailContainer');
658+
var content = $('#detailContent');
659+
var loading = $('#detailLoading');
660+
$('#detailTitle').text(title);
661+
content.empty();
662+
container.show();
663+
loading.show();
664+
$('#detailCount').text('');
665+
666+
var time = getTimeRange();
667+
var url = apiurl + '/latest/telemetry?since=' +
668+
encodeURIComponent(time.since) + '&limit=50';
669+
for (var key in filters) {
670+
if (filters[key] !== null && filters[key] !== undefined && filters[key] !== '') {
671+
url += '&' + encodeURIComponent(key) + '=' +
672+
encodeURIComponent(filters[key]);
673+
}
674+
}
675+
676+
$.ajax({
677+
url: url,
678+
method: 'GET',
679+
dataType: 'json'
680+
}).then(function(data) {
681+
loading.hide();
682+
var items = data.items || [];
683+
var total = data.total || items.length;
684+
$('#detailCount').text(
685+
items.length + ' shown' +
686+
(total > items.length ? ' of ' + total + ' total' : '')
687+
);
688+
if (items.length === 0) {
689+
content.html(
690+
'<div class="text-muted text-center py-3">' +
691+
'No events found matching these filters.</div>'
692+
);
693+
return;
694+
}
695+
items.forEach(function(evt) {
696+
content.append(renderEventCard(evt));
697+
});
698+
// Scroll to the detail panel
699+
container[0].scrollIntoView({ behavior: 'smooth', block: 'start' });
700+
}).catch(function(err) {
701+
loading.hide();
702+
content.html(
703+
'<div class="alert alert-danger">Error loading events: ' +
704+
escapeHtml(String(err.statusText || err)) + '</div>'
705+
);
706+
});
707+
}
708+
506709
function renderStatsTable(tableId, data, groupField) {
507-
var tbody = $('#' + tableId + ' tbody');
710+
var table = $('#' + tableId);
711+
table.addClass('stats-clickable');
712+
var tbody = table.find('tbody');
508713
tbody.empty();
509714
if (!data || data.length === 0) {
510715
tbody.append('<tr><td colspan="6" class="text-muted text-center">No data available</td></tr>');
@@ -517,16 +722,22 @@ <h5><i class="bi bi-heart-pulse"></i> Runtime Health Overview</h5>
517722
var pass = row.pass || 0;
518723
var fail = row.fail || 0;
519724
var infra = row.infra_error || 0;
520-
tbody.append(
521-
'<tr>' +
522-
'<td><strong>' + name + '</strong></td>' +
725+
var tr = $(
726+
'<tr title="Click to see individual events">' +
727+
'<td><strong>' + escapeHtml(name) + '</strong></td>' +
523728
'<td>' + total + '</td>' +
524729
'<td><span class="badge badge-pass bg-success">' + pass + '</span></td>' +
525730
'<td><span class="badge badge-fail bg-danger">' + fail + '</span></td>' +
526731
'<td><span class="badge badge-infra" style="background:#fd7e14">' + infra + '</span></td>' +
527732
'<td>' + healthBar(pass, total) + '</td>' +
528733
'</tr>'
529734
);
735+
tr.on('click', function() {
736+
var filters = {};
737+
filters[groupField] = name;
738+
showDetail('Events for ' + groupField + ': ' + name, filters);
739+
});
740+
tbody.append(tr);
530741
});
531742
}
532743

@@ -553,37 +764,62 @@ <h5><i class="bi bi-heart-pulse"></i> Runtime Health Overview</h5>
553764
var icon = severity === 'high' ? 'bi-exclamation-octagon text-danger' :
554765
severity === 'medium' ? 'bi-exclamation-triangle text-warning' :
555766
'bi-info-circle text-success';
556-
section.append(
557-
'<div class="anomaly-alert anomaly-' + severity + '">' +
767+
var alertEl = $(
768+
'<div class="anomaly-alert anomaly-' + severity +
769+
'" title="Click to see individual events">' +
558770
'<div class="d-flex justify-content-between align-items-center">' +
559771
'<div>' +
560772
'<i class="bi ' + icon + '"></i> ' +
561-
'<strong>' + (a.runtime || 'N/A') + '</strong>' +
562-
(a.device_type ? ' / ' + a.device_type : '') +
773+
'<strong>' + escapeHtml(a.runtime || 'N/A') + '</strong>' +
774+
(a.device_type ? ' / ' + escapeHtml(a.device_type) : '') +
563775
'</div>' +
564776
'<div>' +
565777
'<span class="badge bg-danger me-1">Fail: ' + (a.fail_rate * 100).toFixed(1) + '%</span>' +
566778
'<span class="badge" style="background:#fd7e14">Infra: ' + (a.infra_rate * 100).toFixed(1) + '%</span>' +
779+
' <i class="bi bi-arrow-right-circle ms-2"></i>' +
567780
'</div>' +
568781
'</div>' +
569782
'<small class="text-muted">' + a.total + ' events | ' +
570783
a.fail + ' fail | ' + a.incomplete + ' incomplete | ' +
571784
a.infra_error + ' infra errors</small>' +
572785
'</div>'
573786
);
787+
(function(anomaly) {
788+
alertEl.on('click', function() {
789+
var filters = { runtime: anomaly.runtime };
790+
if (anomaly.device_type) filters.device_type = anomaly.device_type;
791+
var label = escapeHtml(anomaly.runtime || 'N/A');
792+
if (anomaly.device_type) label += ' / ' + escapeHtml(anomaly.device_type);
793+
showDetail('Anomaly events: ' + label, filters);
794+
});
795+
})(a);
796+
section.append(alertEl);
574797
});
575798

576799
if (errorAnomalies.length > 0) {
577800
section.append('<h6 class="mt-3 mb-2">Submission / Connectivity Errors</h6>');
578801
errorAnomalies.forEach(function(a) {
579-
section.append(
580-
'<div class="anomaly-alert anomaly-medium">' +
802+
var alertEl = $(
803+
'<div class="anomaly-alert anomaly-medium" title="Click to see individual events">' +
581804
'<i class="bi bi-wifi-off text-warning"></i> ' +
582-
'<strong>' + (a.runtime || 'N/A') + '</strong>' +
583-
' - ' + (a.error_type || 'unknown') +
805+
'<strong>' + escapeHtml(a.runtime || 'N/A') + '</strong>' +
806+
' - ' + escapeHtml(a.error_type || 'unknown') +
584807
' <span class="badge bg-warning text-dark">' + a.count + ' occurrences</span>' +
808+
' <i class="bi bi-arrow-right-circle ms-2"></i>' +
585809
'</div>'
586810
);
811+
(function(anomaly) {
812+
alertEl.on('click', function() {
813+
var filters = { runtime: anomaly.runtime };
814+
if (anomaly.error_type) filters.error_type = anomaly.error_type;
815+
showDetail(
816+
'Error events: ' + escapeHtml(anomaly.runtime || 'N/A') +
817+
' - ' + escapeHtml(anomaly.error_type || 'unknown'),
818+
filters
819+
);
820+
});
821+
})(a);
822+
section.append(alertEl);
587823
});
588824
}
589825
}
@@ -600,15 +836,29 @@ <h5><i class="bi bi-heart-pulse"></i> Runtime Health Overview</h5>
600836
var ts = evt.ts ? new Date(evt.ts).toLocaleString() : 'N/A';
601837
var msg = evt.error_msg || '';
602838
if (msg.length > 80) msg = msg.substring(0, 80) + '...';
603-
tbody.append(
604-
'<tr class="error-row">' +
839+
var tr = $(
840+
'<tr class="error-row" title="Click to see full event details">' +
605841
'<td><small>' + ts + '</small></td>' +
606-
'<td><span class="badge bg-secondary">' + (evt.kind || '') + '</span></td>' +
607-
'<td>' + (evt.runtime || 'N/A') + '</td>' +
608-
'<td><span class="badge" style="background:#fd7e14">' + (evt.error_type || 'N/A') + '</span></td>' +
609-
'<td><small>' + msg + '</small></td>' +
842+
'<td><span class="badge bg-secondary">' + escapeHtml(evt.kind || '') + '</span></td>' +
843+
'<td>' + escapeHtml(evt.runtime || 'N/A') + '</td>' +
844+
'<td><span class="badge" style="background:#fd7e14">' + escapeHtml(evt.error_type || 'N/A') + '</span></td>' +
845+
'<td><small>' + escapeHtml(msg) + '</small></td>' +
610846
'</tr>'
611847
);
848+
(function(event) {
849+
tr.on('click', function() {
850+
// Show this single event in the detail panel
851+
$('#detailContainer').show();
852+
$('#detailTitle').text('Event Detail');
853+
$('#detailCount').text('');
854+
$('#detailLoading').hide();
855+
var content = $('#detailContent');
856+
content.empty();
857+
content.append(renderEventCard(event));
858+
$('#detailContainer')[0].scrollIntoView({ behavior: 'smooth', block: 'start' });
859+
});
860+
})(evt);
861+
tbody.append(tr);
612862
});
613863
}
614864

@@ -737,6 +987,10 @@ <h5><i class="bi bi-heart-pulse"></i> Runtime Health Overview</h5>
737987
$('#filterTree').change(loadAll);
738988
$('#filterKind').change(loadAll);
739989
$('#anomalyThreshold').change(loadAll);
990+
$('#detailClose').click(function() {
991+
$('#detailContainer').hide();
992+
$('#detailContent').empty();
993+
});
740994
loadAll();
741995
});
742996
</script>

0 commit comments

Comments
 (0)