Merge pull request #49 from wagesj45/codex/implement-chartmanager-for-loading-and-aborting

Add chart manager for abortable fetches
This commit is contained in:
Jordan Wages 2025-07-19 18:01:36 -05:00 committed by GitHub
commit 250cce8c11
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 97 additions and 38 deletions

View file

@ -58,14 +58,17 @@ def _save_json(path: Path, data: List[Dict]) -> None:
def _copy_icons() -> None: def _copy_icons() -> None:
"""Copy vendored icons to the output directory.""" """Copy vendored icons and scripts to the output directory."""
src_dir = Path("static/icons") src_dir = Path("static/icons")
dst_dir = OUTPUT_DIR / "icons" dst_dir = OUTPUT_DIR / "icons"
if not src_dir.is_dir(): if src_dir.is_dir():
return dst_dir.mkdir(parents=True, exist_ok=True)
dst_dir.mkdir(parents=True, exist_ok=True) for icon in src_dir.glob("*.svg"):
for icon in src_dir.glob("*.svg"): shutil.copy(icon, dst_dir / icon.name)
shutil.copy(icon, dst_dir / icon.name)
js_src = Path("static/chartManager.js")
if js_src.is_file():
shutil.copy(js_src, OUTPUT_DIR / js_src.name)
def _render_snippet(report: Dict, out_dir: Path) -> None: def _render_snippet(report: Dict, out_dir: Path) -> None:

49
static/chartManager.js Normal file
View file

@ -0,0 +1,49 @@
export let currentLoad = null;
const loadInfo = new Map();
export function newLoad(container) {
if (currentLoad) {
abortLoad(currentLoad);
}
reset(container);
const controller = new AbortController();
const token = { controller, charts: new Map() };
loadInfo.set(token, token);
currentLoad = token;
return token;
}
export function abortLoad(token) {
const info = loadInfo.get(token);
if (!info) return;
info.controller.abort();
info.charts.forEach(chart => {
try {
chart.destroy();
} catch (e) {}
});
loadInfo.delete(token);
if (currentLoad === token) {
currentLoad = null;
}
}
export function registerChart(token, id, chart) {
const info = loadInfo.get(token);
if (info) {
info.charts.set(id, chart);
} else {
chart.destroy();
}
}
export function reset(container) {
if (!container) return;
container.querySelectorAll('canvas').forEach(c => {
const chart = Chart.getChart(c);
if (chart) {
chart.destroy();
}
});
container.innerHTML = '';
}

View file

@ -72,7 +72,14 @@
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script> <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script src="https://cdn.jsdelivr.net/npm/jquery@3.7.0/dist/jquery.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/jquery@3.7.0/dist/jquery.min.js"></script>
<script src="https://cdn.datatables.net/1.13.4/js/jquery.dataTables.min.js"></script> <script src="https://cdn.datatables.net/1.13.4/js/jquery.dataTables.min.js"></script>
<script> <script type="module">
import {
newLoad,
abortLoad,
registerChart,
reset,
currentLoad,
} from './chartManager.js';
const intervalSelect = document.getElementById('interval-select'); const intervalSelect = document.getElementById('interval-select');
const domainSelect = document.getElementById('domain-select'); const domainSelect = document.getElementById('domain-select');
const intervalControl = document.getElementById('interval-control'); const intervalControl = document.getElementById('interval-control');
@ -105,20 +112,22 @@
let currentDomain = domainSelect.value; let currentDomain = domainSelect.value;
let currentTab = 'overview'; let currentTab = 'overview';
function initReport(rep, base) { function initReport(token, rep, base) {
fetch(base + '/' + rep.json) fetch(base + '/' + rep.json, { signal: token.controller.signal })
.then(r => r.json()) .then(r => r.json())
.then(data => { .then(data => {
if (token !== currentLoad) return;
const bucketField = rep.bucket || 'bucket'; const bucketField = rep.bucket || 'bucket';
if (rep.chart === 'table') { if (rep.chart === 'table') {
const rows = data.map(x => [x[bucketField], x.value]); const rows = data.map(x => [x[bucketField], x.value]);
new DataTable('#table-' + rep.name, { const table = new DataTable('#table-' + rep.name, {
data: rows, data: rows,
columns: [ columns: [
{ title: rep.bucket_label || 'Bucket' }, { title: rep.bucket_label || 'Bucket' },
{ title: 'Value' } { title: 'Value' }
] ]
}); });
registerChart(token, rep.name, table);
return; return;
} }
@ -146,7 +155,7 @@
dataset.backgroundColor = 'rgba(54, 162, 235, 0.5)'; dataset.backgroundColor = 'rgba(54, 162, 235, 0.5)';
dataset.borderColor = 'rgba(54, 162, 235, 1)'; dataset.borderColor = 'rgba(54, 162, 235, 1)';
} }
new Chart(document.getElementById('chart-' + rep.name), { const chart = new Chart(document.getElementById('chart-' + rep.name), {
type: chartType, type: chartType,
data: { data: {
labels: labels, labels: labels,
@ -154,6 +163,7 @@
}, },
options: options options: options
}); });
registerChart(token, rep.name, chart);
}); });
} }
@ -171,18 +181,7 @@
}); });
} }
function destroyCharts(container) { // Reset helpers managed by chartManager
container.querySelectorAll('canvas').forEach(c => {
const chart = Chart.getChart(c);
if (chart) {
chart.destroy();
}
});
}
function destroyAllCharts() {
Object.values(containers).forEach(destroyCharts);
}
function loadReports() { function loadReports() {
let path; let path;
@ -196,27 +195,29 @@
} else { } else {
container = containers.domain; container = containers.domain;
if (!currentDomain) { if (!currentDomain) {
destroyCharts(container); reset(container);
container.innerHTML = '<p>Select a domain</p>'; container.innerHTML = '<p>Select a domain</p>';
return; return;
} }
path = 'domains/' + encodeURIComponent(currentDomain) + '/' + currentInterval; path = 'domains/' + encodeURIComponent(currentDomain) + '/' + currentInterval;
} }
fetch(path + '/reports.json') const token = newLoad(container);
.then(r => r.json())
.then(reports => { fetch(path + '/reports.json', { signal: token.controller.signal })
destroyCharts(container); .then(r => r.json())
container.innerHTML = ''; .then(reports => {
reports.forEach(rep => { if (token !== currentLoad) return;
fetch(path + '/' + rep.html) reports.forEach(rep => {
.then(r => r.text()) fetch(path + '/' + rep.html, { signal: token.controller.signal })
.then(html => { .then(r => r.text())
container.insertAdjacentHTML('beforeend', html); .then(html => {
initReport(rep, path); if (token !== currentLoad) return;
}); container.insertAdjacentHTML('beforeend', html);
}); initReport(token, rep, path);
});
}); });
});
} }
function loadAnalysis() { function loadAnalysis() {
@ -300,7 +301,8 @@
} }
function switchTab(name) { function switchTab(name) {
destroyAllCharts(); abortLoad(currentLoad);
Object.values(containers).forEach(reset);
currentTab = name; currentTab = name;
tabs.forEach(tab => { tabs.forEach(tab => {
tab.classList.toggle('is-active', tab.dataset.tab === name); tab.classList.toggle('is-active', tab.dataset.tab === name);
@ -322,11 +324,16 @@
intervalSelect.addEventListener('change', () => { intervalSelect.addEventListener('change', () => {
currentInterval = intervalSelect.value; currentInterval = intervalSelect.value;
abortLoad(currentLoad);
reset(containers.all);
reset(containers.domain);
loadReports(); loadReports();
}); });
domainSelect.addEventListener('change', () => { domainSelect.addEventListener('change', () => {
currentDomain = domainSelect.value; currentDomain = domainSelect.value;
abortLoad(currentLoad);
reset(containers.domain);
loadReports(); loadReports();
}); });