Skip to content

Commit 555d585

Browse files
authored
Feature: Download of reports from UI (#6)
* Cleanup UI, add download buttons
1 parent 6e0feb3 commit 555d585

File tree

2 files changed

+172
-85
lines changed

2 files changed

+172
-85
lines changed

web/server/main.go

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,11 @@ type Config struct {
3232
}
3333

3434
type AnalyzeResponse struct {
35-
Markdown string `json:"markdown"`
36-
Console string `json:"console"`
37-
Error string `json:"error,omitempty"`
35+
Markdown string `json:"markdown"`
36+
Console string `json:"console"`
37+
ClusterName string `json:"clusterName,omitempty"`
38+
AnalysisTimestamp string `json:"analysisTimestamp,omitempty"`
39+
Error string `json:"error,omitempty"`
3840
}
3941

4042
type VersionResponse struct {
@@ -183,6 +185,8 @@ func handleAnalyzeBoth(cfg *Config) http.HandlerFunc {
183185
if err != nil {
184186
response.Error = fmt.Sprintf("Analysis failed: %v", err)
185187
} else {
188+
response.ClusterName = report.ClusterName
189+
response.AnalysisTimestamp = report.Timestamp.Format(time.RFC3339)
186190
response.Console = reporter.GenerateConsole(report)
187191
markdown, mdErr := reporter.GenerateMarkdown(report, cfg.TemplatePath)
188192
if mdErr != nil {

web/static/index.html

Lines changed: 165 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -156,42 +156,6 @@
156156
display: block;
157157
}
158158

159-
.tabs {
160-
display: flex;
161-
border-bottom: 2px solid #ecf0f1;
162-
margin-bottom: 20px;
163-
}
164-
165-
.tab {
166-
padding: 12px 24px;
167-
cursor: pointer;
168-
border: none;
169-
background: none;
170-
color: #7f8c8d;
171-
font-size: 16px;
172-
transition: color 0.3s;
173-
margin: 0;
174-
border-radius: 0;
175-
}
176-
177-
.tab:hover {
178-
color: #3498db;
179-
}
180-
181-
.tab.active {
182-
color: #3498db;
183-
border-bottom: 2px solid #3498db;
184-
margin-bottom: -2px;
185-
}
186-
187-
.tab-content {
188-
display: none;
189-
}
190-
191-
.tab-content.active {
192-
display: block;
193-
}
194-
195159
.markdown-rendered {
196160
background: #ffffff;
197161
color: #2c3e50;
@@ -227,6 +191,23 @@
227191
overflow-x: auto;
228192
}
229193

194+
.report-actions {
195+
display: flex;
196+
gap: 10px;
197+
margin-bottom: 14px;
198+
}
199+
200+
.report-download-btn {
201+
margin-top: 0;
202+
padding: 10px 16px;
203+
font-size: 14px;
204+
background: #16a085;
205+
}
206+
207+
.report-download-btn:hover:not(:disabled) {
208+
background: #138d75;
209+
}
210+
230211
pre {
231212
background: #2c3e50;
232213
color: #ecf0f1;
@@ -275,7 +256,7 @@ <h1>Sensor Metrics Analyzer</h1>
275256
Privacy: Uploaded files are analyzed and not retained. Temporary files are deleted immediately after processing, and metric data is not stored.
276257
</div>
277258
<div class="disclaimer-note">
278-
Disclaimer: This project is AI-generated and only a small fraction of the code and metric rules were verified by a human. Analysis results may be inaccurate and, in extreme cases, totally wrong.
259+
Disclaimer: The code of this project is AI-generated and only a fraction of the code and metric rules were verified by a human. Analysis results may be inaccurate and - in extreme cases - totally wrong.
279260
</div>
280261

281262
<div class="upload-section" id="uploadSection">
@@ -293,17 +274,11 @@ <h1>Sensor Metrics Analyzer</h1>
293274
</div>
294275

295276
<div class="results" id="results">
296-
<div class="tabs">
297-
<button class="tab" data-tab="console">Console Output</button>
298-
<button class="tab active" data-tab="markdown">Markdown Report</button>
299-
</div>
300-
<div class="tab-content" id="consoleTab">
301-
<pre id="consoleOutput"></pre>
302-
</div>
303-
<div class="tab-content active" id="markdownTab">
304-
<div id="markdownRendered" class="markdown-rendered"></div>
305-
<pre id="markdownOutput" style="margin-top: 16px;"></pre>
277+
<div class="report-actions">
278+
<button id="downloadMdBtn" class="report-download-btn" disabled>Download Markdown</button>
279+
<button id="downloadHtmlBtn" class="report-download-btn" disabled>Download HTML</button>
306280
</div>
281+
<div id="markdownRendered" class="markdown-rendered"></div>
307282
</div>
308283
</div>
309284

@@ -316,14 +291,15 @@ <h1>Sensor Metrics Analyzer</h1>
316291
const errorDiv = document.getElementById('error');
317292
const loadingDiv = document.getElementById('loading');
318293
const resultsDiv = document.getElementById('results');
319-
const consoleOutput = document.getElementById('consoleOutput');
320-
const markdownOutput = document.getElementById('markdownOutput');
321294
const markdownRendered = document.getElementById('markdownRendered');
322-
const tabs = document.querySelectorAll('.tab');
323-
const tabContents = document.querySelectorAll('.tab-content');
295+
const downloadMdBtn = document.getElementById('downloadMdBtn');
296+
const downloadHtmlBtn = document.getElementById('downloadHtmlBtn');
324297
const versionValue = document.getElementById('versionValue');
325298
const lastUpdateValue = document.getElementById('lastUpdateValue');
326299
const releaseLink = document.getElementById('releaseLink');
300+
let currentMarkdownReport = '';
301+
let currentRenderedReportHtml = '';
302+
let currentReportBaseName = 'sensor-metrics-report';
327303

328304
const releasesUrl = 'https://github.com/stackrox/sensor-metrics-analyzer/releases';
329305
releaseLink.href = releasesUrl;
@@ -406,6 +382,10 @@ <h1>Sensor Metrics Analyzer</h1>
406382
resultsDiv.classList.remove('show');
407383
loadingDiv.style.display = 'block';
408384
analyzeBtn.disabled = true;
385+
setDownloadButtonsEnabled(false);
386+
currentMarkdownReport = '';
387+
currentRenderedReportHtml = '';
388+
currentReportBaseName = 'sensor-metrics-report';
409389

410390
const formData = new FormData();
411391
formData.append('file', file);
@@ -421,28 +401,20 @@ <h1>Sensor Metrics Analyzer</h1>
421401
loadingDiv.style.display = 'none';
422402
analyzeBtn.disabled = false;
423403

424-
if (data.error && !data.console && !data.markdown) {
404+
if (data.error && !data.markdown) {
425405
showError(data.error);
426406
return;
427407
}
428408

429-
// Show results
430-
if (data.console) {
431-
consoleOutput.textContent = data.console;
432-
} else {
433-
consoleOutput.textContent = 'No console output available';
434-
}
435-
436409
if (data.markdown) {
437-
if (window.marked && typeof window.marked.parse === 'function') {
438-
markdownRendered.innerHTML = window.marked.parse(data.markdown);
439-
} else {
440-
markdownRendered.textContent = data.markdown;
441-
}
442-
markdownOutput.textContent = data.markdown;
410+
currentMarkdownReport = data.markdown;
411+
currentRenderedReportHtml = renderMarkdownToHtml(data.markdown);
412+
markdownRendered.innerHTML = currentRenderedReportHtml;
413+
currentReportBaseName = buildReportBaseName(data.clusterName, data.analysisTimestamp);
414+
setDownloadButtonsEnabled(true);
443415
} else {
444416
markdownRendered.textContent = 'No markdown output available';
445-
markdownOutput.textContent = 'No markdown output available';
417+
setDownloadButtonsEnabled(false);
446418
}
447419

448420
if (data.error) {
@@ -457,29 +429,140 @@ <h1>Sensor Metrics Analyzer</h1>
457429
}
458430
});
459431

460-
// Tab switching
461-
tabs.forEach(tab => {
462-
tab.addEventListener('click', () => {
463-
const targetTab = tab.dataset.tab;
464-
465-
// Update active tab
466-
tabs.forEach(t => t.classList.remove('active'));
467-
tab.classList.add('active');
432+
downloadMdBtn.addEventListener('click', () => {
433+
if (!currentMarkdownReport) {
434+
return;
435+
}
436+
downloadTextFile(currentMarkdownReport, `${currentReportBaseName}.md`, 'text/markdown;charset=utf-8');
437+
});
468438

469-
// Update active content
470-
tabContents.forEach(content => {
471-
content.classList.remove('active');
472-
if (content.id === targetTab + 'Tab') {
473-
content.classList.add('active');
474-
}
475-
});
476-
});
439+
downloadHtmlBtn.addEventListener('click', () => {
440+
if (!currentRenderedReportHtml) {
441+
return;
442+
}
443+
const htmlDocument = buildHtmlReportDocument(currentRenderedReportHtml, currentReportBaseName);
444+
downloadTextFile(htmlDocument, `${currentReportBaseName}.html`, 'text/html;charset=utf-8');
477445
});
478446

479447
function showError(message) {
480448
errorDiv.textContent = message;
481449
errorDiv.style.display = 'block';
482450
}
451+
452+
function setDownloadButtonsEnabled(isEnabled) {
453+
downloadMdBtn.disabled = !isEnabled;
454+
downloadHtmlBtn.disabled = !isEnabled;
455+
}
456+
457+
function renderMarkdownToHtml(markdown) {
458+
if (window.marked && typeof window.marked.parse === 'function') {
459+
return window.marked.parse(markdown);
460+
}
461+
return `<pre>${escapeHtml(markdown)}</pre>`;
462+
}
463+
464+
function buildReportBaseName(clusterName, analysisTimestamp) {
465+
const sanitizedCluster = sanitizeFilePart(clusterName || 'cluster');
466+
const formattedDate = formatDateForFilename(analysisTimestamp);
467+
return `sensor-metrics-${sanitizedCluster}-${formattedDate}`;
468+
}
469+
470+
function formatDateForFilename(rawTimestamp) {
471+
const date = rawTimestamp ? new Date(rawTimestamp) : new Date();
472+
if (Number.isNaN(date.getTime())) {
473+
return 'unknown-date';
474+
}
475+
const year = date.getUTCFullYear();
476+
const month = String(date.getUTCMonth() + 1).padStart(2, '0');
477+
const day = String(date.getUTCDate()).padStart(2, '0');
478+
const hours = String(date.getUTCHours()).padStart(2, '0');
479+
const minutes = String(date.getUTCMinutes()).padStart(2, '0');
480+
const seconds = String(date.getUTCSeconds()).padStart(2, '0');
481+
return `${year}${month}${day}-${hours}${minutes}${seconds}`;
482+
}
483+
484+
function sanitizeFilePart(value) {
485+
return String(value)
486+
.toLowerCase()
487+
.replace(/[^a-z0-9-]+/g, '-')
488+
.replace(/^-+|-+$/g, '')
489+
.slice(0, 64) || 'cluster';
490+
}
491+
492+
function downloadTextFile(content, filename, mimeType) {
493+
const blob = new Blob([content], { type: mimeType });
494+
const url = URL.createObjectURL(blob);
495+
const link = document.createElement('a');
496+
link.href = url;
497+
link.download = filename;
498+
document.body.appendChild(link);
499+
link.click();
500+
document.body.removeChild(link);
501+
URL.revokeObjectURL(url);
502+
}
503+
504+
function buildHtmlReportDocument(renderedHtml, reportBaseName) {
505+
const escapedTitle = escapeHtml(reportBaseName);
506+
return `<!DOCTYPE html>
507+
<html lang="en">
508+
<head>
509+
<meta charset="UTF-8">
510+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
511+
<title>${escapedTitle}</title>
512+
<style>
513+
body {
514+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
515+
background: #f7f9fb;
516+
color: #2c3e50;
517+
margin: 0;
518+
padding: 24px;
519+
}
520+
.report {
521+
max-width: 1000px;
522+
margin: 0 auto;
523+
background: #fff;
524+
border: 1px solid #e4ebf0;
525+
border-radius: 8px;
526+
padding: 24px;
527+
}
528+
.report h1, .report h2, .report h3, .report h4 {
529+
margin: 16px 0 8px;
530+
}
531+
.report p {
532+
margin: 8px 0;
533+
}
534+
.report code {
535+
background: #f2f4f6;
536+
padding: 2px 4px;
537+
border-radius: 3px;
538+
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
539+
font-size: 13px;
540+
}
541+
.report pre {
542+
background: #2c3e50;
543+
color: #ecf0f1;
544+
padding: 16px;
545+
border-radius: 4px;
546+
overflow-x: auto;
547+
}
548+
</style>
549+
</head>
550+
<body>
551+
<article class="report">
552+
${renderedHtml}
553+
</article>
554+
</body>
555+
</html>`;
556+
}
557+
558+
function escapeHtml(text) {
559+
return String(text)
560+
.replace(/&/g, '&amp;')
561+
.replace(/</g, '&lt;')
562+
.replace(/>/g, '&gt;')
563+
.replace(/"/g, '&quot;')
564+
.replace(/'/g, '&#39;');
565+
}
483566
</script>
484567
</body>
485568
</html>

0 commit comments

Comments
 (0)