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:
parent
b9732810f9
commit
3d0519db8f
1 changed files with 179 additions and 0 deletions
179
src/ui/schema-view.ts
Normal file
179
src/ui/schema-view.ts
Normal 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 */ }
|
||||
}
|
||||
Loading…
Reference in a new issue