Schema-Browser: Sidebar mit Tabellen, Spalten und Indizes

SchemaView als ItemView zeigt alle Logfire-DB-Tabellen mit
aufklappbaren Details (Spaltentypen, PK, Indizes, Zeilenanzahl).
Rechtsklick kopiert SELECT-Statement.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Luca Oelfke 2026-02-12 11:24:14 +01:00
parent b9732810f9
commit 3d0519db8f

179
src/ui/schema-view.ts Normal file
View file

@ -0,0 +1,179 @@
import { ItemView, WorkspaceLeaf, Notice } from 'obsidian';
import { DatabaseManager } from '../core/database';
export const SCHEMA_VIEW_TYPE = 'logfire-schema-view';
// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------
interface TableInfo {
name: string;
columns: ColumnInfo[];
indexes: IndexInfo[];
rowCount: number;
}
interface ColumnInfo {
name: string;
type: string;
notnull: boolean;
pk: boolean;
}
interface IndexInfo {
name: string;
unique: boolean;
columns: string[];
}
// ---------------------------------------------------------------------------
// SchemaView
// ---------------------------------------------------------------------------
export class SchemaView extends ItemView {
private db: DatabaseManager;
private contentContainer!: HTMLElement;
private expandedTables = new Set<string>();
constructor(leaf: WorkspaceLeaf, db: DatabaseManager) {
super(leaf);
this.db = db;
}
getViewType(): string {
return SCHEMA_VIEW_TYPE;
}
getDisplayText(): string {
return 'Schema-Browser';
}
getIcon(): string {
return 'database';
}
async onOpen(): Promise<void> {
const container = this.containerEl.children[1] as HTMLElement;
container.empty();
container.addClass('logfire-schema-view');
const header = container.createDiv({ cls: 'logfire-schema-header' });
header.createSpan({ cls: 'logfire-schema-title', text: 'Logfire Schema' });
const refreshBtn = header.createEl('button', {
cls: 'logfire-dash-btn clickable-icon',
attr: { 'aria-label': 'Aktualisieren' },
text: '\u21bb',
});
refreshBtn.addEventListener('click', () => this.refresh());
this.contentContainer = container.createDiv({ cls: 'logfire-schema-content' });
this.refresh();
}
refresh(): void {
this.contentContainer.empty();
const tables = this.introspect();
if (tables.length === 0) {
this.contentContainer.createDiv({ cls: 'logfire-empty', text: 'Keine Tabellen gefunden.' });
return;
}
for (const table of tables) {
this.renderTableNode(table);
}
}
// ---------------------------------------------------------------------------
// Introspection
// ---------------------------------------------------------------------------
private introspect(): TableInfo[] {
const tableRows = this.db.queryReadOnly(
"SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name",
) as Array<{ name: string }>;
return tableRows.map(({ name }) => {
const columns = (this.db.queryReadOnly(`PRAGMA table_info("${name}")`) as Array<{
name: string; type: string; notnull: number; pk: number;
}>).map(c => ({
name: c.name,
type: c.type || 'ANY',
notnull: c.notnull === 1,
pk: c.pk > 0,
}));
const idxRows = this.db.queryReadOnly(`PRAGMA index_list("${name}")`) as Array<{
name: string; unique: number;
}>;
const indexes: IndexInfo[] = [];
for (const idx of idxRows) {
if (idx.name.startsWith('sqlite_')) continue;
const cols = (this.db.queryReadOnly(`PRAGMA index_info("${idx.name}")`) as Array<{
name: string;
}>).map(c => c.name);
indexes.push({ name: idx.name, unique: idx.unique === 1, columns: cols });
}
const countRow = this.db.queryReadOnly(`SELECT COUNT(*) as c FROM "${name}"`) as Array<{ c: number }>;
const rowCount = countRow[0]?.c ?? 0;
return { name, columns, indexes, rowCount };
});
}
// ---------------------------------------------------------------------------
// Rendering
// ---------------------------------------------------------------------------
private renderTableNode(table: TableInfo): void {
const node = this.contentContainer.createDiv({ cls: 'logfire-schema-table' });
const isExpanded = this.expandedTables.has(table.name);
const header = node.createDiv({ cls: 'logfire-schema-table-header' });
const arrow = header.createSpan({ cls: 'logfire-schema-arrow', text: isExpanded ? '\u25bc' : '\u25b6' });
header.createSpan({ cls: 'logfire-schema-table-name', text: table.name });
header.createSpan({ cls: 'logfire-schema-row-count', text: `(${table.rowCount})` });
header.addEventListener('click', () => {
if (isExpanded) this.expandedTables.delete(table.name);
else this.expandedTables.add(table.name);
this.refresh();
});
// Context menu: SQL kopieren
header.addEventListener('contextmenu', (e) => {
e.preventDefault();
navigator.clipboard.writeText(`SELECT * FROM "${table.name}" LIMIT 100;`);
new Notice(`SELECT auf "${table.name}" kopiert.`);
});
if (!isExpanded) return;
const details = node.createDiv({ cls: 'logfire-schema-details' });
// Columns
for (const col of table.columns) {
const row = details.createDiv({ cls: 'logfire-schema-col' });
const nameText = col.pk ? `\u{1f511} ${col.name}` : col.name;
row.createSpan({ cls: 'logfire-schema-col-name', text: nameText });
const typeText = col.type + (col.notnull ? '' : '?');
row.createSpan({ cls: 'logfire-schema-col-type', text: typeText });
}
// Indexes
if (table.indexes.length > 0) {
details.createDiv({ cls: 'logfire-schema-section-header', text: 'Indizes' });
for (const idx of table.indexes) {
const row = details.createDiv({ cls: 'logfire-schema-idx' });
row.createSpan({ text: `${idx.unique ? 'UNIQUE ' : ''}${idx.name}` });
row.createSpan({ cls: 'logfire-schema-idx-cols', text: `(${idx.columns.join(', ')})` });
}
}
}
async onClose(): Promise<void> { /* cleanup */ }
}