Spreadsheet-UX op een echte relationele engine

main

Deze repository wordt als momentopname gepubliceerd: een clone bevat de volledige inhoud, maar geen commitgeschiedenis.

3.4 kB Ruw bestand
import { useState, type ReactNode } from 'react';
import { useRows, useSummary } from '../api/hooks';
import { useI18n } from '../i18n';
import { useWorkspace } from '../state/workspace';
import { useToasts } from '../state/toasts';

/**
 * The signature: the statement the server actually ran for the current view,
 * with literals inlined. Not an illustration — this is the query, echoed back
 * from the response that produced the rows above it.
 *
 * Collapsed by default. Shown once on request it is a proof that the grid is a
 * real query; shown always it was a truncated ribbon nobody could read.
 */
export function SqlBar(): ReactNode {
  const ws = useWorkspace();
  const toasts = useToasts();
  const { t, locale } = useI18n();
  const [open, setOpen] = useState(false);

  const { sql, tokens, total } = useRows({
    table: ws.tableName,
    viewId: ws.viewId,
    filters: ws.adHocFilters,
    sort: ws.sortOverride,
    hidden: ws.hiddenOverride,
    search: ws.search,
    enabled: Boolean(ws.tableName),
  });

  // The aggregate row is a second statement, so it gets a second line. Hiding
  // it would make the footer the one number on this screen with no working out
  // shown — which is the opposite of what this bar is for.
  const summary = useSummary({
    table: ws.tableName,
    viewId: ws.viewId,
    filters: ws.adHocFilters,
    hidden: ws.hiddenOverride,
    search: ws.search,
    summaries: ws.summaries,
    enabled: Boolean(ws.tableName),
  });

  if (!sql) return null;

  return (
    <div className={'sqlbar' + (open ? ' open' : '')}>
      {/* The whole line toggles — a thin dark strip whose only clickable part
          is one small label invites misses. The buttons stop propagation so
          they keep their own meaning. */}
      <div className="sqlbar-line" onClick={() => setOpen(!open)}>
        <span className="lbl">{t('grid.sqlRowCount', total, locale)}</span>
        <button
          className="disclose"
          onClick={(e) => {
            e.stopPropagation();
            setOpen(!open);
          }}
          aria-expanded={open}
        >
          {open ? t('grid.hideSql') : t('grid.viewSql')}
        </button>
        {open && (
          <button
            className="copy"
            onClick={(e) => {
              e.stopPropagation();
              navigator.clipboard
                .writeText(sql)
                .then(() => toasts.push(t('grid.sqlCopied'), 'success'))
                .catch(() => toasts.push(t('grid.clipboardRefused'), 'error'));
            }}
          >
            {t('grid.copySql')}
          </button>
        )}
      </div>

      {open && (
        // One scroll container around both statements: two panes that scroll
        // apart would make the pair read as two bars instead of one proof.
        <div className="stmts">
          <code className="stmt" aria-label={t('grid.sqlForView')}>
            {tokens.map((token, i) => (
              <span key={i} className={token.c}>
                {token.s}
              </span>
            ))}
          </code>

          {summary.sql && (
            <code className="stmt" aria-label={t('grid.sqlForSummary')}>
              {summary.tokens.map((token, i) => (
                <span key={i} className={token.c}>
                  {token.s}
                </span>
              ))}
            </code>
          )}
        </div>
      )}
    </div>
  );
}