diff --git a/src/ui/schema-view.ts b/src/ui/schema-view.ts new file mode 100644 index 0000000..a2b844a --- /dev/null +++ b/src/ui/schema-view.ts @@ -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(); + + 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 */ } +}