/** * The CDClient Browser page: a raw table viewer. Everything is addressed by the hash, so views can be linked and the * back button works: * #/ the list of tables * #/table/?... a table's rows (start, order, dir, q, filters as JSON [{column, op, value}]) * #// shorthand for the target table filtered to that ID (#/object/1727 is Objects id = 1727) * Which columns link where comes from /api/cdclient/schema; a linked value opens the target table filtered to it. */ (function () { 'use strict'; var PAGE = 50; var OPS = ['=', '!=', '<', '<=', '>', '>=', 'contains', 'starts', 'null', 'notnull']; var nf = new Intl.NumberFormat(); var view = document.getElementById('view'); var schema = null; function api404(d) { return d && d.success === false; } // ---- links ---- function tableHash(table, params) { var query = []; Object.keys(params || {}).forEach(function (k) { var v = params[k]; if (v === undefined || v === null || v === '' || (Array.isArray(v) && !v.length)) return; query.push(k + '=' + encodeURIComponent(Array.isArray(v) ? JSON.stringify(v) : v)); }); return '#/table/' + encodeURIComponent(table) + (query.length ? '?' + query.join('&') : ''); } // The rows a link points at: its target table filtered to the ID function hashFor(link, id) { var target = schema && schema.links[link]; if (!target) return null; return tableHash(target.table, { filters: [{ column: target.column, op: '=', value: String(id) }] }); } function componentTable(type) { return schema.componentTypes.filter(function (c) { return c.type === type; })[0] || null; } // One cell: linked when its column points somewhere function cell(value, column, row, table) { if (value === null || value === undefined) return 'NULL'; // REAL columns hold 32-bit floats: show the digits they actually have (0.4, not 0.4000000059604645) var text = esc(column && column.type === 'REAL' && typeof value === 'number' ? +value.toPrecision(7) : value); if (column && column.link && value !== 0 && value !== -1) { var href = hashFor(column.link, value); if (href) return '' + text + ''; } // ComponentsRegistry: the type by name, and the component's row in its table if (table === 'ComponentsRegistry' && column) { if (column.name === 'component_type') { var type = componentTable(value); return text + (type ? ' ' + esc(type.name) + '' : ''); } if (column.name === 'component_id') { var owner = componentTable(row.component_type); if (owner && owner.table) return '' + text + ''; } } return text; } function card(title, body) { return '
' + title + '
' + body + '
'; } function header(title, subtitle) { return '

' + title + '

' + (subtitle ? '
' + subtitle + '
' : '') + '
'; } function showError(message) { view.innerHTML = '
' + esc(message) + '
'; } // ---- views ---- function home() { var tables = schema.tables; view.innerHTML = card('Tables ' + tables.length + '', '' + ''); var filter = document.getElementById('tableFilter'); filter.addEventListener('input', function () { var text = filter.value.trim().toLowerCase(); document.querySelectorAll('#tableLinks a').forEach(function (a) { a.classList.toggle('d-none', text && a.dataset.name.indexOf(text) === -1); }); }); // Enter opens the only table left filter.addEventListener('keydown', function (e) { if (e.key !== 'Enter') return; var shown = document.querySelectorAll('#tableLinks a:not(.d-none)'); if (shown.length === 1) window.location.hash = shown[0].getAttribute('href'); }); filter.focus(); } function tableView(name, params) { var start = parseInt(params.start || '0', 10) || 0; var filters = []; try { filters = params.filters ? JSON.parse(params.filters) : []; } catch (e) { filters = []; } var url = '/api/cdclient/tables/' + encodeURIComponent(name) + '/rows?start=' + start + '&length=' + PAGE + (params.order ? '&order=' + encodeURIComponent(params.order) + '&dir=' + (params.dir || 'asc') : '') + (params.q ? '&q=' + encodeURIComponent(params.q) : '') + (filters.length ? '&filters=' + encodeURIComponent(JSON.stringify(filters)) : ''); api.get(url).then(function (d) { if (api404(d)) return showError(d.error); var state = { filters: filters, q: params.q || '', order: params.order || '', dir: params.dir || 'asc' }; var go = function (changes) { window.location.hash = tableHash(name, Object.assign({ q: state.q, filters: state.filters, order: state.order, dir: state.order ? state.dir : '', start: 0 }, changes)); }; var columnOptions = d.columns.map(function (c) { return ''; }).join(''); var filterChips = filters.map(function (f, i) { return '' + esc(f.column) + ' ' + esc(f.op) + ' ' + esc(f.value || '') + ' '; }).join(''); var pages = Math.max(1, Math.ceil(d.total / PAGE)), page = Math.floor(start / PAGE) + 1; var head = '' + d.columns.map(function (c) { var arrow = state.order === c.name ? (state.dir === 'desc' ? ' ↓' : ' ↑') : ''; return '' + esc(c.name) + arrow + ''; }).join('') + ''; view.innerHTML = header('Tables / ' + esc(name), nf.format(d.total) + ' matching rows') + '
' + '
' + '
' + '
' + '
' + '
' + '
' + '
' + filterChips + '
' + '
' + head + '' + d.rows.map(function (row) { return '' + d.columns.map(function (c) { return ''; }).join('') + ''; }).join('') + '
' + cell(row[c.name], c, row, name) + '
'; document.getElementById('filterForm').addEventListener('submit', function (e) { e.preventDefault(); var search = document.getElementById('fSearch'); if (document.activeElement === search) return go({ q: search.value.trim() }); var filter = { column: document.getElementById('fColumn').value, op: document.getElementById('fOp').value, value: document.getElementById('fValue').value }; go({ filters: state.filters.concat([filter]) }); }); view.querySelectorAll('[data-remove]').forEach(function (b) { b.addEventListener('click', function () { go({ filters: state.filters.filter(function (f, i) { return i !== parseInt(b.dataset.remove, 10); }) }); }); }); view.querySelectorAll('[data-order]').forEach(function (a) { a.addEventListener('click', function (e) { e.preventDefault(); var column = a.dataset.order; go({ order: column, dir: state.order === column && state.dir === 'asc' ? 'desc' : 'asc' }); }); }); document.getElementById('prevPage').addEventListener('click', function () { go({ start: Math.max(0, start - PAGE) }); }); document.getElementById('nextPage').addEventListener('click', function () { go({ start: start + PAGE }); }); }); } // ---- routing ---- function route() { var hash = window.location.hash.replace(/^#\/?/, ''); var queryAt = hash.indexOf('?'); var path = (queryAt === -1 ? hash : hash.slice(0, queryAt)).split('/').map(decodeURIComponent); var params = {}; if (queryAt !== -1) hash.slice(queryAt + 1).split('&').forEach(function (p) { var eq = p.indexOf('='); if (eq > 0) params[p.slice(0, eq)] = decodeURIComponent(p.slice(eq + 1)); }); view.innerHTML = '
Loading…
'; window.scrollTo(0, 0); if (path[0] === '') return home(); if (path[0] === 'table') return tableView(path[1], params); // #/object/1727 and the like: the rows it names, without adding a history entry var id = parseInt(path[1], 10); var target = !isNaN(id) && hashFor(path[0], id); if (target) return window.location.replace(target); showError('Nothing here.'); } window.addEventListener('hashchange', route); api.get('/api/cdclient/schema').then(function (d) { if (api404(d)) return showError(d.error); schema = d; route(); }); })();