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(); 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 { 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 { /* cleanup */ } }