data browser wip

This commit is contained in:
2026-10-24 02:42:31 +00:00
parent c8205e474e
commit 941a9f16e1
22 changed files with 2141 additions and 6 deletions

View File

@@ -14,6 +14,24 @@
import { Input } from '#lib/components/ui/input/index.js';
import type { PageProps } from './$types';
let { data, form }: PageProps = $props();
let tableFilter = $state('');
const schemas = $derived.by(() => {
const q = tableFilter.trim().toLowerCase();
const groups = new Map<string, typeof data.tables>();
for (const t of data.tables) {
if (q && !`${t.schema}.${t.name} ${t.displayName}`.toLowerCase().includes(q)) continue;
groups.set(t.schema, [...(groups.get(t.schema) ?? []), t]);
}
// lists first, then alphabetical
return [...groups].sort(([a], [b]) => (a === 'lists' ? -1 : b === 'lists' ? 1 : a.localeCompare(b)));
});
const totalRows = $derived(data.tables.reduce((n, t) => n + (t.rows ?? 0), 0));
const badge = (access: string, relkind: string) =>
access === 'open'
? { text: 'editable', cls: 'border-emerald-600/40 text-emerald-500' }
: access === 'core'
? { text: 'app table', cls: 'border-amber-500/40 text-amber-500' }
: { text: relkind === 'v' || relkind === 'm' ? 'view' : 'read-only', cls: 'border-[var(--site-border)] text-[var(--site-muted)]' };
let uploadError = $state('');
let uploading = $state(false);
const uploadPicture: SubmitFunction = async ({ formData, cancel }) => {
@@ -137,4 +155,51 @@
</li>
{/each}
</ul>
{#if data.isOwner && data.tables.length}
<section id="database" class="mt-16">
<div class="mb-4 flex flex-wrap items-end justify-between gap-4">
<div>
<h2 class="text-2xl font-semibold tracking-tight">database</h2>
<p class="mt-1 text-sm text-[var(--site-muted)]">
{data.tables.length} tables in {new Set(data.tables.map((t) => t.schema)).size} schemas, ~{totalRows.toLocaleString('en-GB')} rows. Only you can see this. Lists are editable; app tables are read-only until you unlock them; a new list is a new table.
</p>
</div>
<input
type="search"
bind:value={tableFilter}
placeholder="find a table"
aria-label="Find a table"
class="w-56 rounded border border-[var(--site-border)] bg-[var(--site-bg)] px-3 py-1.5 text-sm"
/>
</div>
{#each schemas as [schema, tables] (schema)}
<details open class="border-t border-[var(--site-border)] py-3" id="schema-{schema}">
<summary class="cursor-pointer text-lg font-medium">
{schema}
<span class="ml-2 text-sm font-normal text-[var(--site-muted)]">{tables.length} {tables.length === 1 ? 'table' : 'tables'}</span>
</summary>
<ul class="mt-2 grid gap-x-6 sm:grid-cols-2 lg:grid-cols-3">
{#each tables as t (t.name)}
{@const b = badge(t.access, t.relkind)}
<li>
<a
href={resolve('/lists/db/[schema]/[table]', { schema: t.schema, table: t.name })}
class="flex items-center gap-2 rounded px-2 py-1.5 hover:bg-[var(--site-hover)]"
>
<span class="min-w-0 flex-1 truncate" title={t.description ?? t.displayName}>{t.name}</span>
<span class={['rounded-full border px-1.5 text-[10px]', b.cls]}>{b.text}</span>
<span class="w-16 text-right text-xs text-[var(--site-muted)] tabular-nums">
{t.rows === null ? '–' : `${t.approximate ? '~' : ''}${t.rows.toLocaleString('en-GB')}`}
</span>
</a>
</li>
{/each}
</ul>
</details>
{:else}
<p class="text-[var(--site-muted)]">no tables match</p>
{/each}
</section>
{/if}
</div>