|
1 | 1 | import glob |
| 2 | +import textwrap |
2 | 3 | from argparse import ArgumentParser |
3 | 4 | from datetime import datetime |
4 | 5 | from pathlib import Path |
@@ -53,6 +54,163 @@ def wrap_dataset_name(name: str): |
53 | 54 | }, |
54 | 55 | } |
55 | 56 |
|
| 57 | +DATA_TABLE_TEMPLATE = textwrap.dedent( |
| 58 | + r""" |
| 59 | +<!-- jQuery + DataTables core --> |
| 60 | +<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script> |
| 61 | +<link rel="stylesheet" href="https://cdn.datatables.net/v/bm/dt-1.13.4/datatables.min.css"/> |
| 62 | +<script src="https://cdn.datatables.net/v/bm/dt-1.13.4/datatables.min.js"></script> |
| 63 | +
|
| 64 | +<!-- Buttons + SearchPanes (+ Select required by SearchPanes) --> |
| 65 | +<link rel="stylesheet" href="https://cdn.datatables.net/buttons/2.4.2/css/buttons.dataTables.min.css"> |
| 66 | +<script src="https://cdn.datatables.net/buttons/2.4.2/js/dataTables.buttons.min.js"></script> |
| 67 | +<link rel="stylesheet" href="https://cdn.datatables.net/select/1.7.0/css/select.dataTables.min.css"> |
| 68 | +<link rel="stylesheet" href="https://cdn.datatables.net/searchpanes/2.3.1/css/searchPanes.dataTables.min.css"> |
| 69 | +<script src="https://cdn.datatables.net/select/1.7.0/js/dataTables.select.min.js"></script> |
| 70 | +<script src="https://cdn.datatables.net/searchpanes/2.3.1/js/dataTables.searchPanes.min.js"></script> |
| 71 | +
|
| 72 | +<style> |
| 73 | + /* Styling for the Total row (placed in tfoot) */ |
| 74 | + table.sd-table tfoot td { |
| 75 | + font-weight: 600; |
| 76 | + border-top: 2px solid rgba(0,0,0,0.2); |
| 77 | + background: #f9fafb; |
| 78 | + /* Match body cell padding to keep perfect alignment */ |
| 79 | + padding: 8px 10px !important; |
| 80 | + vertical-align: middle; |
| 81 | + } |
| 82 | +
|
| 83 | + /* Right-align numeric-like columns (2..8) consistently for body & footer */ |
| 84 | + table.sd-table tbody td:nth-child(n+2), |
| 85 | + table.sd-table tfoot td:nth-child(n+2) { |
| 86 | + text-align: right; |
| 87 | + } |
| 88 | + /* Keep first column (Dataset/Total) left-aligned */ |
| 89 | + table.sd-table tbody td:first-child, |
| 90 | + table.sd-table tfoot td:first-child { |
| 91 | + text-align: left; |
| 92 | + } |
| 93 | +</style> |
| 94 | +
|
| 95 | +<TABLE_HTML> |
| 96 | +
|
| 97 | +<script> |
| 98 | +// Helper: robustly extract values for SearchPanes when needed |
| 99 | +function tagsArrayFromHtml(html) { |
| 100 | + if (html == null) return []; |
| 101 | + // If it's numeric or plain text, just return as a single value |
| 102 | + if (typeof html === 'number') return [String(html)]; |
| 103 | + if (typeof html === 'string' && html.indexOf('<') === -1) return [html.trim()]; |
| 104 | + // Else parse any .tag elements inside HTML |
| 105 | + const tmp = document.createElement('div'); |
| 106 | + tmp.innerHTML = html; |
| 107 | + const tags = Array.from(tmp.querySelectorAll('.tag')).map(function(el){ |
| 108 | + return (el.textContent || '').trim(); |
| 109 | + }); |
| 110 | + return tags.length ? tags : [tmp.textContent.trim()]; |
| 111 | +} |
| 112 | +
|
| 113 | +// Helper: parse human-readable sizes like "4.31 GB" into bytes (number) |
| 114 | +function parseSizeToBytes(text) { |
| 115 | + if (!text) return 0; |
| 116 | + const s = String(text).trim(); |
| 117 | + const m = s.match(/([\d,.]+)\s*(TB|GB|MB|KB|B)/i); |
| 118 | + if (!m) return 0; |
| 119 | + const value = parseFloat(m[1].replace(/,/g, '')); |
| 120 | + const unit = m[2].toUpperCase(); |
| 121 | + const factor = { B:1, KB:1024, MB:1024**2, GB:1024**3, TB:1024**4 }[unit] || 1; |
| 122 | + return value * factor; |
| 123 | +} |
| 124 | +
|
| 125 | +document.addEventListener('DOMContentLoaded', function () { |
| 126 | + const table = document.getElementById('datasets-table'); |
| 127 | + if (!table || !window.jQuery || !window.jQuery.fn || !window.jQuery.fn.DataTable) { |
| 128 | + return; |
| 129 | + } |
| 130 | +
|
| 131 | + const $table = window.jQuery(table); |
| 132 | + if (window.jQuery.fn.DataTable.isDataTable(table)) { |
| 133 | + return; |
| 134 | + } |
| 135 | +
|
| 136 | + // 1) Move the "Total" row into <tfoot> so sorting/filtering never moves it |
| 137 | + const $tbody = $table.find('tbody'); |
| 138 | + const $total = $tbody.find('tr').filter(function(){ |
| 139 | + return window.jQuery(this).find('td').eq(0).text().trim() === 'Total'; |
| 140 | + }); |
| 141 | + if ($total.length) { |
| 142 | + let $tfoot = $table.find('tfoot'); |
| 143 | + if (!$tfoot.length) $tfoot = window.jQuery('<tfoot/>').appendTo($table); |
| 144 | + $total.appendTo($tfoot); |
| 145 | + } |
| 146 | +
|
| 147 | + // 2) Initialize DataTable with SearchPanes button |
| 148 | + const FILTER_COLS = [1,2,3,4,5,6]; |
| 149 | + // Detect the index of the size column by header text |
| 150 | + const sizeIdx = (function(){ |
| 151 | + let idx = -1; |
| 152 | + $table.find('thead th').each(function(i){ |
| 153 | + const t = window.jQuery(this).text().trim().toLowerCase(); |
| 154 | + if (t === 'size on disk' || t === 'size') idx = i; |
| 155 | + }); |
| 156 | + return idx; |
| 157 | + })(); |
| 158 | +
|
| 159 | + const dataTable = $table.DataTable({ |
| 160 | + dom: 'Blfrtip', |
| 161 | + paging: false, |
| 162 | + searching: true, |
| 163 | + info: false, |
| 164 | + language: { |
| 165 | + search: 'Filter dataset:', |
| 166 | + searchPanes: { collapse: { 0: 'Filters', _: 'Filters (%d)' } } |
| 167 | + }, |
| 168 | + buttons: [{ |
| 169 | + extend: 'searchPanes', |
| 170 | + text: 'Filters', |
| 171 | + config: { cascadePanes: true, viewTotal: true, layout: 'columns-4', initCollapsed: false } |
| 172 | + }], |
| 173 | + columnDefs: (function(){ |
| 174 | + const defs = [ |
| 175 | + { searchPanes: { show: true }, targets: FILTER_COLS } |
| 176 | + ]; |
| 177 | + if (sizeIdx !== -1) { |
| 178 | + defs.push({ |
| 179 | + targets: sizeIdx, |
| 180 | + render: function(data, type) { |
| 181 | + if (type === 'sort' || type === 'type') { |
| 182 | + return parseSizeToBytes(data); |
| 183 | + } |
| 184 | + return data; |
| 185 | + } |
| 186 | + }); |
| 187 | + } |
| 188 | + return defs; |
| 189 | + })() |
| 190 | + }); |
| 191 | +
|
| 192 | + // 3) UX: click a header to open the relevant filter pane |
| 193 | + $table.find('thead th').each(function (i) { |
| 194 | + if ([1,2,3,4].indexOf(i) === -1) return; |
| 195 | + window.jQuery(this) |
| 196 | + .css('cursor','pointer') |
| 197 | + .attr('title','Click to filter this column') |
| 198 | + .on('click', function () { |
| 199 | + dataTable.button('.buttons-searchPanes').trigger(); |
| 200 | + window.setTimeout(function () { |
| 201 | + const idx = [1,2,3,4].indexOf(i); |
| 202 | + const $container = window.jQuery(dataTable.searchPanes.container()); |
| 203 | + const $pane = $container.find('.dtsp-pane').eq(idx); |
| 204 | + const $title = $pane.find('.dtsp-title'); |
| 205 | + if ($title.length) $title.trigger('click'); |
| 206 | + }, 0); |
| 207 | + }); |
| 208 | + }); |
| 209 | +}); |
| 210 | +</script> |
| 211 | +""" |
| 212 | +) |
| 213 | + |
56 | 214 |
|
57 | 215 | def _tag_normalizer(kind: str): |
58 | 216 | canonical = {k.lower(): v for k, v in DATASET_CANONICAL_MAP.get(kind, {}).items()} |
@@ -226,8 +384,9 @@ def main(source_dir: str, target_dir: str): |
226 | 384 | escape=False, |
227 | 385 | table_id="datasets-table", |
228 | 386 | ) |
| 387 | + html_table = DATA_TABLE_TEMPLATE.replace("<TABLE_HTML>", html_table) |
229 | 388 | table_path = target_dir / "dataset_summary_table.html" |
230 | | - with open(table_path, "+w", encoding="utf-8") as f: |
| 389 | + with open(table_path, "w", encoding="utf-8") as f: |
231 | 390 | f.write(html_table) |
232 | 391 | copyfile(table_path, STATIC_DATASET_DIR / table_path.name) |
233 | 392 |
|
|
0 commit comments