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>
179 lines
5.7 KiB
TypeScript
179 lines
5.7 KiB
TypeScript
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 */ }
|
|
}
|