Event-Stream-View: Echtzeit-Sidebar mit Filtern
Sidebar-View mit Live-Event-Liste, Suche nach Quelle, Kategorie-Filter-Checkboxen, Pause/Leeren-Buttons, max 500 sichtbare Einträge, Payload-Zusammenfassung je Event-Typ. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
afc8c8281d
commit
50af0eb802
1 changed files with 150 additions and 0 deletions
150
src/ui/event-stream-view.ts
Normal file
150
src/ui/event-stream-view.ts
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
import { ItemView, WorkspaceLeaf } from 'obsidian';
|
||||
import { LogfireEvent, EventCategory } from '../types';
|
||||
import { EventBus } from '../core/event-bus';
|
||||
|
||||
export const EVENT_STREAM_VIEW_TYPE = 'logfire-event-stream';
|
||||
const MAX_VISIBLE_ENTRIES = 500;
|
||||
|
||||
export class EventStreamView extends ItemView {
|
||||
private entries: LogfireEvent[] = [];
|
||||
private unsubscribe: (() => void) | null = null;
|
||||
private listEl!: HTMLElement;
|
||||
private viewPaused = false;
|
||||
private filterText = '';
|
||||
private enabledCategories = new Set<EventCategory>([
|
||||
'file', 'content', 'navigation', 'editor', 'vault', 'plugin', 'system',
|
||||
]);
|
||||
|
||||
constructor(leaf: WorkspaceLeaf, private eventBus: EventBus) {
|
||||
super(leaf);
|
||||
}
|
||||
|
||||
getViewType(): string {
|
||||
return EVENT_STREAM_VIEW_TYPE;
|
||||
}
|
||||
|
||||
getDisplayText(): string {
|
||||
return 'Logfire Events';
|
||||
}
|
||||
|
||||
getIcon(): string {
|
||||
return 'activity';
|
||||
}
|
||||
|
||||
async onOpen(): Promise<void> {
|
||||
const container = this.containerEl.children[1] as HTMLElement;
|
||||
container.empty();
|
||||
container.addClass('logfire-event-stream');
|
||||
|
||||
// Toolbar
|
||||
const toolbar = container.createDiv({ cls: 'logfire-stream-toolbar' });
|
||||
|
||||
const searchInput = toolbar.createEl('input', {
|
||||
type: 'text',
|
||||
placeholder: 'Nach Quelle filtern...',
|
||||
cls: 'logfire-stream-search',
|
||||
});
|
||||
searchInput.addEventListener('input', () => {
|
||||
this.filterText = searchInput.value.toLowerCase();
|
||||
this.renderList();
|
||||
});
|
||||
|
||||
const btnGroup = toolbar.createDiv({ cls: 'logfire-stream-btn-group' });
|
||||
|
||||
const pauseBtn = btnGroup.createEl('button', { text: 'Pause' });
|
||||
pauseBtn.addEventListener('click', () => {
|
||||
this.viewPaused = !this.viewPaused;
|
||||
pauseBtn.textContent = this.viewPaused ? 'Weiter' : 'Pause';
|
||||
pauseBtn.toggleClass('is-active', this.viewPaused);
|
||||
});
|
||||
|
||||
const clearBtn = btnGroup.createEl('button', { text: 'Leeren' });
|
||||
clearBtn.addEventListener('click', () => {
|
||||
this.entries = [];
|
||||
this.renderList();
|
||||
});
|
||||
|
||||
// Category toggles
|
||||
const categoryBar = container.createDiv({ cls: 'logfire-stream-categories' });
|
||||
|
||||
const categories: EventCategory[] = ['file', 'content', 'navigation', 'editor', 'vault', 'plugin', 'system'];
|
||||
for (const cat of categories) {
|
||||
const label = categoryBar.createEl('label', { cls: 'logfire-cat-toggle' });
|
||||
const cb = label.createEl('input', { type: 'checkbox' }) as HTMLInputElement;
|
||||
cb.checked = true;
|
||||
label.createSpan({ text: cat });
|
||||
cb.addEventListener('change', () => {
|
||||
if (cb.checked) {
|
||||
this.enabledCategories.add(cat);
|
||||
} else {
|
||||
this.enabledCategories.delete(cat);
|
||||
}
|
||||
this.renderList();
|
||||
});
|
||||
}
|
||||
|
||||
// Event list
|
||||
this.listEl = container.createDiv({ cls: 'logfire-stream-list' });
|
||||
|
||||
// Subscribe to events
|
||||
this.unsubscribe = this.eventBus.onEvent('*', (event) => {
|
||||
if (this.viewPaused) return;
|
||||
this.entries.unshift(event);
|
||||
if (this.entries.length > MAX_VISIBLE_ENTRIES) {
|
||||
this.entries.length = MAX_VISIBLE_ENTRIES;
|
||||
}
|
||||
this.renderList();
|
||||
});
|
||||
}
|
||||
|
||||
async onClose(): Promise<void> {
|
||||
this.unsubscribe?.();
|
||||
this.unsubscribe = null;
|
||||
}
|
||||
|
||||
private renderList(): void {
|
||||
this.listEl.empty();
|
||||
|
||||
const filtered = this.entries.filter(e => {
|
||||
if (!this.enabledCategories.has(e.category)) return false;
|
||||
if (this.filterText && !e.source.toLowerCase().includes(this.filterText)) return false;
|
||||
return true;
|
||||
});
|
||||
|
||||
for (const event of filtered.slice(0, MAX_VISIBLE_ENTRIES)) {
|
||||
const row = this.listEl.createDiv({ cls: 'logfire-stream-row' });
|
||||
|
||||
const time = new Date(event.timestamp);
|
||||
const timeStr = `${String(time.getHours()).padStart(2, '0')}:${String(time.getMinutes()).padStart(2, '0')}:${String(time.getSeconds()).padStart(2, '0')}`;
|
||||
|
||||
const payloadSummary = this.summarizePayload(event);
|
||||
|
||||
row.createSpan({ text: timeStr, cls: 'logfire-stream-time' });
|
||||
row.createSpan({ text: event.type, cls: 'logfire-stream-type' });
|
||||
row.createSpan({ text: event.source || '', cls: 'logfire-stream-source' });
|
||||
if (payloadSummary) {
|
||||
row.createSpan({ text: payloadSummary, cls: 'logfire-stream-payload' });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private summarizePayload(event: LogfireEvent): string {
|
||||
const p = event.payload;
|
||||
switch (event.type) {
|
||||
case 'content:words-changed':
|
||||
return `+${p.wordsAdded ?? 0}/-${p.wordsRemoved ?? 0} Wörter`;
|
||||
case 'editor:change':
|
||||
return `+${p.insertedChars ?? 0}/-${p.deletedChars ?? 0} Zeichen`;
|
||||
case 'nav:file-close':
|
||||
return typeof p.duration === 'number' ? `${Math.round(p.duration / 1000)}s` : '';
|
||||
case 'file:rename':
|
||||
return `→ ${p.newName ?? ''}`;
|
||||
case 'file:move':
|
||||
return `→ ${p.newFolder ?? ''}`;
|
||||
case 'plugin:command-executed':
|
||||
return String(p.commandName ?? '');
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue