html

tcmts.vscode.data_panel.html · #pokwx7ft · term · runs anywhere

import type * as vscode from "vscode";

export function html(webview: vscode.Webview): string {
  const nonce = Math.random().toString(36).slice(2) + Math.random().toString(36).slice(2);
  return `<!doctype html>
<html><head><meta charset="utf-8">
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src ${webview.cspSource} 'unsafe-inline'; script-src 'nonce-${nonce}';">
<style>
  body { padding: 0; margin: 0; font-family: var(--vscode-font-family); font-size: var(--vscode-font-size); color: var(--vscode-foreground); }
  header { padding: 10px 14px 6px; border-bottom: 1px solid var(--vscode-panel-border); }
  h1 { font-size: 1.25em; margin: 0 0 4px; font-weight: 600; }
  .meta { opacity: .8; } .meta a { color: var(--vscode-textLink-foreground); cursor: pointer; text-decoration: none; }
  .older { color: var(--vscode-editorWarning-foreground); }
  .bar { display: flex; gap: 8px; align-items: center; padding: 8px 14px; border-bottom: 1px solid var(--vscode-panel-border); }
  input { flex: 1; max-width: 360px; padding: 3px 6px; color: var(--vscode-input-foreground); background: var(--vscode-input-background); border: 1px solid var(--vscode-input-border, transparent); }
  button { padding: 3px 10px; color: var(--vscode-button-foreground); background: var(--vscode-button-background); border: none; cursor: pointer; }
  button.secondary { color: var(--vscode-button-secondaryForeground); background: var(--vscode-button-secondaryBackground); }
  button:disabled { opacity: .4; cursor: default; }
  .count { opacity: .75; margin-left: auto; }
  main { display: flex; height: calc(100vh - 92px); }
  .grid { flex: 1; overflow: auto; }
  table { border-collapse: collapse; width: 100%; font-family: var(--vscode-editor-font-family); font-size: var(--vscode-editor-font-size); }
  th { position: sticky; top: 0; background: var(--vscode-editorGroupHeader-tabsBackground); text-align: left; padding: 5px 10px; cursor: pointer; white-space: nowrap; border-bottom: 1px solid var(--vscode-panel-border); }
  th .type { opacity: .6; font-weight: normal; }
  th.sorted::after { content: " ▲"; } th.sorted.desc::after { content: " ▼"; }
  td { padding: 4px 10px; border-bottom: 1px solid var(--vscode-editorGroup-border); white-space: nowrap; max-width: 360px; overflow: hidden; text-overflow: ellipsis; }
  td.id { opacity: .65; }
  tr:hover td { background: var(--vscode-list-hoverBackground); }
  tr.selected td { background: var(--vscode-list-activeSelectionBackground); color: var(--vscode-list-activeSelectionForeground); }
  .badge { font-size: .8em; padding: 0 5px; border-radius: 3px; margin-left: 6px; background: var(--vscode-editorWarning-foreground); color: var(--vscode-editor-background); }
  aside { width: 38%; min-width: 260px; border-left: 1px solid var(--vscode-panel-border); overflow: auto; padding: 10px 14px; display: none; }
  aside.open { display: block; }
  aside pre { font-family: var(--vscode-editor-font-family); font-size: var(--vscode-editor-font-size); white-space: pre-wrap; margin: 6px 0 0; }
  .empty { padding: 24px 14px; opacity: .7; }
</style></head>
<body>
<header><h1 id="title"></h1><div class="meta" id="meta"></div></header>
<div class="bar">
  <input id="search" placeholder="Search rows" />
  <button class="secondary" id="prev">‹ Prev</button><button class="secondary" id="next">Next ›</button>
  <button id="migrate" style="display:none">Migrate rows</button>
  <span class="count" id="count"></span>
</div>
<main><div class="grid" id="grid"></div><aside id="detail"></aside></main>
<script nonce="${nonce}">
const vscode = acquireVsCodeApi();
const $ = (id) => document.getElementById(id);
let state = { query: {}, page: { rows: [], total: 0, fields: [] }, selected: undefined };
const send = (query) => vscode.postMessage({ type: "query", query });
const esc = (s) => String(s).replace(/[&<>"]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[c]);

let timer;
$("search").addEventListener("input", (e) => { clearTimeout(timer); timer = setTimeout(() => send({ search: e.target.value, offset: 0 }), 250); });
$("prev").onclick = () => send({ offset: Math.max(0, state.query.offset - state.query.limit) });
$("next").onclick = () => send({ offset: state.query.offset + state.query.limit });
$("migrate").onclick = () => vscode.postMessage({ type: "migrate" });

function render() {
  const { info, page, query } = state;
  $("title").textContent = info.table;
  const declared = info.path ? 'declared by <a id="def">' + esc(info.path) + "</a>" : "not declared by any definition now";
  $("meta").innerHTML = declared + " · " + info.rows + " row" + (info.rows === 1 ? "" : "s") +
    (info.older ? ' · <span class="older">' + info.older + " stored as an older row type</span>" : "");
  if ($("def")) $("def").onclick = () => vscode.postMessage({ type: "openDefinition", uri: info.uri });
  $("migrate").style.display = info.older ? "" : "none";
  const first = page.total ? query.offset + 1 : 0;
  $("count").textContent = (query.search ? page.total + " matching · " : "") + first + "–" + (query.offset + page.rows.length) + " of " + page.total;
  $("prev").disabled = query.offset === 0;
  $("next").disabled = query.offset + page.rows.length >= page.total;
  if (!page.rows.length) {
    $("grid").innerHTML = '<div class="empty">' + (query.search ? "No rows match." : "No rows yet.") + "</div>";
    return;
  }
  const cols = [{ name: "id" }, ...page.fields, { name: "updated", type: "written" }];
  const sort = query.sort || {};
  let h = "<table><thead><tr>";
  for (const c of cols) {
    const cls = sort.field === c.name ? "sorted" + (sort.desc ? " desc" : "") : "";
    h += '<th class="' + cls + '" data-field="' + esc(c.name) + '">' + esc(c.name) + (c.type ? ' <span class="type">' + esc(c.type) + "</span>" : "") + "</th>";
  }
  h += "</tr></thead><tbody>";
  for (const r of page.rows) {
    h += '<tr data-id="' + esc(r.id) + '"' + (state.selected === r.id ? ' class="selected"' : "") + ">";
    h += '<td class="id">' + esc(r.id) + (r.older ? '<span class="badge">older</span>' : "") + "</td>";
    for (const f of page.fields) h += '<td title="' + esc(r.cells[f.name] ?? "") + '">' + esc(r.cells[f.name] ?? "") + "</td>";
    h += "<td>" + esc(new Date(r.updated).toLocaleString()) + "</td></tr>";
  }
  $("grid").innerHTML = h + "</tbody></table>";
  for (const th of document.querySelectorAll("th")) th.onclick = () => {
    const field = th.dataset.field;
    send({ sort: { field, desc: sort.field === field ? !sort.desc : false }, offset: 0 });
  };
  for (const tr of document.querySelectorAll("tbody tr")) tr.onclick = () => { state.selected = tr.dataset.id; render(); };
  const sel = page.rows.find((r) => r.id === state.selected);
  $("detail").className = sel ? "open" : "";
  $("detail").innerHTML = sel ? "<strong>" + esc(sel.id) + "</strong>" + (sel.older ? '<span class="badge">older row type</span>' : "") + "<pre>" + esc(sel.detail) + "</pre>" : "";
}

window.addEventListener("message", (e) => {
  const m = e.data;
  if (m.type === "error") { $("grid").innerHTML = '<div class="empty">' + esc(m.message) + "</div>"; return; }
  state = { ...state, info: m.info, page: m.page, query: m.query };
  render();
});
</script></body></html>`;
}

used by