Merge feature/query-management: History, Favoriten, Templates, Schema
Query-History mit Metriken, Favoriten mit Kategorien/Tags, 9 Built-in Templates fuer Logfire-Schema, Schema-Browser Sidebar. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
commit
c30320a7db
7 changed files with 1224 additions and 2 deletions
61
src/main.ts
61
src/main.ts
|
|
@ -17,6 +17,10 @@ import { registerLogfireBlock, registerLogfireSqlBlock, registerLogfireDashboard
|
|||
import { QueryModal } from './query/query-modal';
|
||||
import { VirtualTableManager } from './query/virtual-tables';
|
||||
import { DashboardView, DASHBOARD_VIEW_TYPE } from './viz/dashboard';
|
||||
import { HistoryManager } from './management/history';
|
||||
import { FavoritesManager, SaveFavoriteModal } from './management/favorites';
|
||||
import { TemplateManager, TemplatePickerModal } from './management/templates';
|
||||
import { SchemaView, SCHEMA_VIEW_TYPE } from './ui/schema-view';
|
||||
|
||||
export default class LogfirePlugin extends Plugin {
|
||||
settings!: LogfireSettings;
|
||||
|
|
@ -24,6 +28,9 @@ export default class LogfirePlugin extends Plugin {
|
|||
eventBus!: EventBus;
|
||||
sessionManager!: SessionManager;
|
||||
contentAnalyzer!: ContentAnalyzer;
|
||||
historyManager!: HistoryManager;
|
||||
favoritesManager!: FavoritesManager;
|
||||
templateManager!: TemplateManager;
|
||||
|
||||
private fileCollector!: FileCollector;
|
||||
private contentCollector!: ContentCollector;
|
||||
|
|
@ -53,6 +60,11 @@ export default class LogfirePlugin extends Plugin {
|
|||
const vaultName = this.app.vault.getName();
|
||||
this.sessionManager = new SessionManager(this.eventBus, this.db, vaultName);
|
||||
|
||||
// Query management
|
||||
this.historyManager = new HistoryManager();
|
||||
this.favoritesManager = new FavoritesManager();
|
||||
this.templateManager = new TemplateManager();
|
||||
|
||||
// Content analyzer
|
||||
this.contentAnalyzer = new ContentAnalyzer();
|
||||
if (this.db.hasBaseline()) {
|
||||
|
|
@ -97,6 +109,12 @@ export default class LogfirePlugin extends Plugin {
|
|||
(leaf) => new DashboardView(leaf, this),
|
||||
);
|
||||
|
||||
// UI: Schema-Browser view
|
||||
this.registerView(
|
||||
SCHEMA_VIEW_TYPE,
|
||||
(leaf) => new SchemaView(leaf, this.db),
|
||||
);
|
||||
|
||||
// UI: Status bar
|
||||
this.statusBar = new StatusBar(this);
|
||||
this.statusBar.start();
|
||||
|
|
@ -321,7 +339,31 @@ export default class LogfirePlugin extends Plugin {
|
|||
id: 'open-query',
|
||||
name: 'Query-Editor \u00f6ffnen',
|
||||
callback: () => {
|
||||
new QueryModal(this.app, this.db).open();
|
||||
new QueryModal(this.app, this.db, this.historyManager).open();
|
||||
},
|
||||
});
|
||||
|
||||
this.addCommand({
|
||||
id: 'show-schema',
|
||||
name: 'Schema-Browser anzeigen',
|
||||
callback: () => this.activateSchema(),
|
||||
});
|
||||
|
||||
this.addCommand({
|
||||
id: 'show-templates',
|
||||
name: 'Query-Templates anzeigen',
|
||||
callback: () => {
|
||||
new TemplatePickerModal(this, this.templateManager, (sql) => {
|
||||
new QueryModal(this.app, this.db, this.historyManager, sql).open();
|
||||
}).open();
|
||||
},
|
||||
});
|
||||
|
||||
this.addCommand({
|
||||
id: 'save-favorite',
|
||||
name: 'Aktuelle Query als Favorit speichern',
|
||||
callback: () => {
|
||||
new SaveFavoriteModal(this, this.favoritesManager, '').open();
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -376,6 +418,23 @@ export default class LogfirePlugin extends Plugin {
|
|||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Schema view
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
private async activateSchema(): Promise<void> {
|
||||
const existing = this.app.workspace.getLeavesOfType(SCHEMA_VIEW_TYPE);
|
||||
if (existing.length > 0) {
|
||||
this.app.workspace.revealLeaf(existing[0]);
|
||||
return;
|
||||
}
|
||||
const leaf = this.app.workspace.getRightLeaf(false);
|
||||
if (leaf) {
|
||||
await leaf.setViewState({ type: SCHEMA_VIEW_TYPE, active: true });
|
||||
this.app.workspace.revealLeaf(leaf);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Settings
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
|
|||
209
src/management/favorites.ts
Normal file
209
src/management/favorites.ts
Normal file
|
|
@ -0,0 +1,209 @@
|
|||
import { Modal, Setting, Notice } from 'obsidian';
|
||||
import type LogfirePlugin from '../main';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Types
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export interface Favorite {
|
||||
id: string;
|
||||
name: string;
|
||||
sql: string;
|
||||
category: string;
|
||||
tags: string[];
|
||||
createdAt: string;
|
||||
lastUsedAt?: string;
|
||||
usageCount: number;
|
||||
}
|
||||
|
||||
export interface FavoriteCategory {
|
||||
id: string;
|
||||
name: string;
|
||||
color: string;
|
||||
}
|
||||
|
||||
const STORAGE_KEY = 'logfire-favorites';
|
||||
|
||||
const DEFAULT_CATEGORIES: FavoriteCategory[] = [
|
||||
{ id: 'allgemein', name: 'Allgemein', color: '#4C78A8' },
|
||||
{ id: 'analyse', name: 'Analyse', color: '#F58518' },
|
||||
{ id: 'wartung', name: 'Wartung', color: '#E45756' },
|
||||
];
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// FavoritesManager
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export class FavoritesManager {
|
||||
private favorites = new Map<string, Favorite>();
|
||||
private categories = new Map<string, FavoriteCategory>();
|
||||
private listeners = new Set<() => void>();
|
||||
|
||||
constructor() {
|
||||
this.load();
|
||||
}
|
||||
|
||||
private load(): void {
|
||||
const raw = localStorage.getItem(STORAGE_KEY);
|
||||
if (raw) {
|
||||
try {
|
||||
const data = JSON.parse(raw) as { favorites: Favorite[]; categories: FavoriteCategory[] };
|
||||
for (const f of data.favorites) this.favorites.set(f.id, f);
|
||||
for (const c of data.categories) this.categories.set(c.id, c);
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
if (this.categories.size === 0) {
|
||||
for (const c of DEFAULT_CATEGORIES) this.categories.set(c.id, c);
|
||||
this.save();
|
||||
}
|
||||
}
|
||||
|
||||
private save(): void {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify({
|
||||
favorites: Array.from(this.favorites.values()),
|
||||
categories: Array.from(this.categories.values()),
|
||||
}));
|
||||
for (const fn of this.listeners) fn();
|
||||
}
|
||||
|
||||
onChange(fn: () => void): () => void {
|
||||
this.listeners.add(fn);
|
||||
return () => this.listeners.delete(fn);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Favorites CRUD
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
add(name: string, sql: string, category = 'allgemein', tags: string[] = []): Favorite {
|
||||
const fav: Favorite = {
|
||||
id: `fav-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
||||
name,
|
||||
sql,
|
||||
category,
|
||||
tags,
|
||||
createdAt: new Date().toISOString(),
|
||||
usageCount: 0,
|
||||
};
|
||||
this.favorites.set(fav.id, fav);
|
||||
this.save();
|
||||
return fav;
|
||||
}
|
||||
|
||||
getAll(): Favorite[] {
|
||||
return Array.from(this.favorites.values()).sort((a, b) => {
|
||||
if (b.usageCount !== a.usageCount) return b.usageCount - a.usageCount;
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
}
|
||||
|
||||
getByCategory(categoryId: string): Favorite[] {
|
||||
return this.getAll().filter(f => f.category === categoryId);
|
||||
}
|
||||
|
||||
search(term: string): Favorite[] {
|
||||
const t = term.toLowerCase();
|
||||
return this.getAll().filter(
|
||||
f => f.name.toLowerCase().includes(t) || f.sql.toLowerCase().includes(t)
|
||||
|| f.tags.some(tag => tag.toLowerCase().includes(t)),
|
||||
);
|
||||
}
|
||||
|
||||
incrementUsage(id: string): void {
|
||||
const f = this.favorites.get(id);
|
||||
if (f) {
|
||||
f.usageCount++;
|
||||
f.lastUsedAt = new Date().toISOString();
|
||||
this.save();
|
||||
}
|
||||
}
|
||||
|
||||
update(id: string, updates: Partial<Favorite>): void {
|
||||
const f = this.favorites.get(id);
|
||||
if (f) { Object.assign(f, updates); this.save(); }
|
||||
}
|
||||
|
||||
delete(id: string): void {
|
||||
this.favorites.delete(id);
|
||||
this.save();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Categories
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
getCategories(): FavoriteCategory[] {
|
||||
return Array.from(this.categories.values());
|
||||
}
|
||||
|
||||
addCategory(name: string, color: string): FavoriteCategory {
|
||||
const id = name.toLowerCase().replace(/\s+/g, '-');
|
||||
const cat: FavoriteCategory = { id, name, color };
|
||||
this.categories.set(id, cat);
|
||||
this.save();
|
||||
return cat;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// SaveFavoriteModal
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export class SaveFavoriteModal extends Modal {
|
||||
private plugin: LogfirePlugin;
|
||||
private manager: FavoritesManager;
|
||||
private sql: string;
|
||||
private name = '';
|
||||
private category = 'allgemein';
|
||||
private tags: string[] = [];
|
||||
|
||||
constructor(plugin: LogfirePlugin, manager: FavoritesManager, sql: string) {
|
||||
super(plugin.app);
|
||||
this.plugin = plugin;
|
||||
this.manager = manager;
|
||||
this.sql = sql;
|
||||
}
|
||||
|
||||
onOpen(): void {
|
||||
const { contentEl } = this;
|
||||
contentEl.addClass('logfire-save-fav-modal');
|
||||
contentEl.createEl('h3', { text: 'Als Favorit speichern' });
|
||||
|
||||
new Setting(contentEl).setName('Name').addText(text =>
|
||||
text.setPlaceholder('Meine Query').onChange(v => { this.name = v; }),
|
||||
);
|
||||
|
||||
new Setting(contentEl).setName('Kategorie').addDropdown(dd => {
|
||||
for (const cat of this.manager.getCategories()) dd.addOption(cat.id, cat.name);
|
||||
dd.setValue(this.category);
|
||||
dd.onChange(v => { this.category = v; });
|
||||
});
|
||||
|
||||
new Setting(contentEl).setName('Tags').setDesc('Komma-getrennt').addText(text =>
|
||||
text.setPlaceholder('select, events').onChange(v => {
|
||||
this.tags = v.split(',').map(s => s.trim()).filter(Boolean);
|
||||
}),
|
||||
);
|
||||
|
||||
contentEl.createEl('h4', { text: 'SQL-Vorschau' });
|
||||
contentEl.createEl('pre', {
|
||||
cls: 'logfire-sql-preview',
|
||||
text: this.sql.length > 200 ? this.sql.slice(0, 200) + '...' : this.sql,
|
||||
});
|
||||
|
||||
const btns = contentEl.createDiv({ cls: 'logfire-modal-buttons' });
|
||||
const saveBtn = btns.createEl('button', { text: 'Speichern', cls: 'mod-cta' });
|
||||
saveBtn.addEventListener('click', () => this.doSave());
|
||||
const cancelBtn = btns.createEl('button', { text: 'Abbrechen' });
|
||||
cancelBtn.addEventListener('click', () => this.close());
|
||||
}
|
||||
|
||||
private doSave(): void {
|
||||
if (!this.name.trim()) { new Notice('Bitte einen Namen eingeben.'); return; }
|
||||
this.manager.add(this.name.trim(), this.sql, this.category, this.tags);
|
||||
new Notice(`Favorit "${this.name}" gespeichert.`);
|
||||
this.close();
|
||||
}
|
||||
|
||||
onClose(): void { this.contentEl.empty(); }
|
||||
}
|
||||
126
src/management/history.ts
Normal file
126
src/management/history.ts
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
// ---------------------------------------------------------------------------
|
||||
// Query History — automatisches Tracking aller ausgefuehrten Queries
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export interface HistoryEntry {
|
||||
id: string;
|
||||
sql: string;
|
||||
executedAt: string;
|
||||
executionTimeMs?: number;
|
||||
rowCount?: number;
|
||||
isFavorite: boolean;
|
||||
name?: string;
|
||||
}
|
||||
|
||||
const STORAGE_KEY = 'logfire-query-history';
|
||||
const MAX_ENTRIES = 200;
|
||||
|
||||
export class HistoryManager {
|
||||
private entries = new Map<string, HistoryEntry>();
|
||||
private listeners = new Set<() => void>();
|
||||
|
||||
constructor() {
|
||||
this.load();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Persistence
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
private load(): void {
|
||||
const raw = localStorage.getItem(STORAGE_KEY);
|
||||
if (!raw) return;
|
||||
try {
|
||||
const arr: HistoryEntry[] = JSON.parse(raw);
|
||||
for (const e of arr) this.entries.set(e.id, e);
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
|
||||
private save(): void {
|
||||
localStorage.setItem(
|
||||
STORAGE_KEY,
|
||||
JSON.stringify(Array.from(this.entries.values())),
|
||||
);
|
||||
for (const fn of this.listeners) fn();
|
||||
}
|
||||
|
||||
onChange(fn: () => void): () => void {
|
||||
this.listeners.add(fn);
|
||||
return () => this.listeners.delete(fn);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// CRUD
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
addEntry(sql: string, executionTimeMs?: number, rowCount?: number): HistoryEntry {
|
||||
// Deduplizieren: gleiche SQL aktualisiert vorhandenen Eintrag
|
||||
for (const entry of this.entries.values()) {
|
||||
if (entry.sql === sql) {
|
||||
entry.executedAt = new Date().toISOString();
|
||||
entry.executionTimeMs = executionTimeMs;
|
||||
entry.rowCount = rowCount;
|
||||
this.save();
|
||||
return entry;
|
||||
}
|
||||
}
|
||||
|
||||
const entry: HistoryEntry = {
|
||||
id: `h-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
||||
sql,
|
||||
executedAt: new Date().toISOString(),
|
||||
executionTimeMs,
|
||||
rowCount,
|
||||
isFavorite: false,
|
||||
};
|
||||
this.entries.set(entry.id, entry);
|
||||
|
||||
// Limit: aelteste nicht-favorisierte loeschen
|
||||
if (this.entries.size > MAX_ENTRIES) {
|
||||
const sorted = this.getAll();
|
||||
for (const e of sorted.slice(MAX_ENTRIES)) {
|
||||
if (!e.isFavorite) this.entries.delete(e.id);
|
||||
}
|
||||
}
|
||||
|
||||
this.save();
|
||||
return entry;
|
||||
}
|
||||
|
||||
getAll(searchTerm?: string): HistoryEntry[] {
|
||||
let results = Array.from(this.entries.values());
|
||||
|
||||
if (searchTerm) {
|
||||
const term = searchTerm.toLowerCase();
|
||||
results = results.filter(
|
||||
e => e.sql.toLowerCase().includes(term) || e.name?.toLowerCase().includes(term),
|
||||
);
|
||||
}
|
||||
|
||||
return results.sort(
|
||||
(a, b) => new Date(b.executedAt).getTime() - new Date(a.executedAt).getTime(),
|
||||
);
|
||||
}
|
||||
|
||||
toggleFavorite(id: string): void {
|
||||
const e = this.entries.get(id);
|
||||
if (e) { e.isFavorite = !e.isFavorite; this.save(); }
|
||||
}
|
||||
|
||||
setName(id: string, name: string): void {
|
||||
const e = this.entries.get(id);
|
||||
if (e) { e.name = name || undefined; this.save(); }
|
||||
}
|
||||
|
||||
delete(id: string): void {
|
||||
this.entries.delete(id);
|
||||
this.save();
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
for (const [id, entry] of this.entries) {
|
||||
if (!entry.isFavorite) this.entries.delete(id);
|
||||
}
|
||||
this.save();
|
||||
}
|
||||
}
|
||||
356
src/management/templates.ts
Normal file
356
src/management/templates.ts
Normal file
|
|
@ -0,0 +1,356 @@
|
|||
import { Modal, Setting, Notice } from 'obsidian';
|
||||
import type LogfirePlugin from '../main';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Types
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export interface QueryTemplate {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
sql: string;
|
||||
parameters: TemplateParameter[];
|
||||
builtIn: boolean;
|
||||
}
|
||||
|
||||
export interface TemplateParameter {
|
||||
name: string;
|
||||
label: string;
|
||||
defaultValue: string;
|
||||
type: 'text' | 'number' | 'date';
|
||||
}
|
||||
|
||||
const STORAGE_KEY = 'logfire-templates';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// TemplateManager
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export class TemplateManager {
|
||||
private templates = new Map<string, QueryTemplate>();
|
||||
|
||||
constructor() {
|
||||
this.load();
|
||||
}
|
||||
|
||||
private load(): void {
|
||||
const raw = localStorage.getItem(STORAGE_KEY);
|
||||
if (raw) {
|
||||
try {
|
||||
const arr: QueryTemplate[] = JSON.parse(raw);
|
||||
for (const t of arr) this.templates.set(t.id, t);
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
// Immer Built-in Templates sicherstellen
|
||||
for (const t of BUILTIN_TEMPLATES) {
|
||||
if (!this.templates.has(t.id)) this.templates.set(t.id, t);
|
||||
}
|
||||
this.save();
|
||||
}
|
||||
|
||||
private save(): void {
|
||||
localStorage.setItem(
|
||||
STORAGE_KEY,
|
||||
JSON.stringify(Array.from(this.templates.values())),
|
||||
);
|
||||
}
|
||||
|
||||
getAll(): QueryTemplate[] {
|
||||
return Array.from(this.templates.values());
|
||||
}
|
||||
|
||||
get(id: string): QueryTemplate | undefined {
|
||||
return this.templates.get(id);
|
||||
}
|
||||
|
||||
addCustom(name: string, description: string, sql: string): QueryTemplate {
|
||||
const t: QueryTemplate = {
|
||||
id: `tpl-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
||||
name,
|
||||
description,
|
||||
sql,
|
||||
parameters: parseParameters(sql),
|
||||
builtIn: false,
|
||||
};
|
||||
this.templates.set(t.id, t);
|
||||
this.save();
|
||||
return t;
|
||||
}
|
||||
|
||||
delete(id: string): void {
|
||||
const t = this.templates.get(id);
|
||||
if (t && !t.builtIn) {
|
||||
this.templates.delete(id);
|
||||
this.save();
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Parameter handling
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static applyParameters(sql: string, values: Record<string, string>): string {
|
||||
return sql.replace(/\{\{(\w+)(?::([^}]*))?\}\}/g, (_match, name, defaultVal) => {
|
||||
const v = values[name];
|
||||
if (v !== undefined && v !== '') return v;
|
||||
return defaultVal ?? '';
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Parameter detection
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function parseParameters(sql: string): TemplateParameter[] {
|
||||
const params: TemplateParameter[] = [];
|
||||
const seen = new Set<string>();
|
||||
const re = /\{\{(\w+)(?::([^}]*))?\}\}/g;
|
||||
let m: RegExpExecArray | null;
|
||||
|
||||
while ((m = re.exec(sql)) !== null) {
|
||||
if (seen.has(m[1])) continue;
|
||||
seen.add(m[1]);
|
||||
params.push({
|
||||
name: m[1],
|
||||
label: m[1].charAt(0).toUpperCase() + m[1].slice(1).replace(/_/g, ' '),
|
||||
defaultValue: m[2] ?? '',
|
||||
type: 'text',
|
||||
});
|
||||
}
|
||||
return params;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Built-in Templates (angepasst an Logfire-Schema)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const BUILTIN_TEMPLATES: QueryTemplate[] = [
|
||||
{
|
||||
id: 'builtin-events-today',
|
||||
name: 'Events heute',
|
||||
description: 'Alle Events seit Mitternacht',
|
||||
sql: `SELECT type, source, datetime(timestamp/1000, 'unixepoch', 'localtime') as zeit
|
||||
FROM events
|
||||
WHERE timestamp > (strftime('%s', 'now', 'start of day') * 1000)
|
||||
ORDER BY timestamp DESC
|
||||
LIMIT {{limit:100}}`,
|
||||
parameters: [{ name: 'limit', label: 'Limit', defaultValue: '100', type: 'number' }],
|
||||
builtIn: true,
|
||||
},
|
||||
{
|
||||
id: 'builtin-active-files',
|
||||
name: 'Aktivste Dateien',
|
||||
description: 'Dateien mit den meisten Events',
|
||||
sql: `SELECT source as datei, COUNT(*) as events
|
||||
FROM events
|
||||
WHERE source IS NOT NULL
|
||||
GROUP BY source
|
||||
ORDER BY events DESC
|
||||
LIMIT {{limit:20}}`,
|
||||
parameters: [{ name: 'limit', label: 'Limit', defaultValue: '20', type: 'number' }],
|
||||
builtIn: true,
|
||||
},
|
||||
{
|
||||
id: 'builtin-session-overview',
|
||||
name: 'Sessions',
|
||||
description: 'Letzte Sessions mit Dauer',
|
||||
sql: `SELECT
|
||||
id,
|
||||
datetime(start_time/1000, 'unixepoch', 'localtime') as start,
|
||||
CASE WHEN end_time IS NOT NULL
|
||||
THEN ROUND((end_time - start_time) / 60000.0, 1) || ' min'
|
||||
ELSE 'aktiv'
|
||||
END as dauer
|
||||
FROM sessions
|
||||
ORDER BY start_time DESC
|
||||
LIMIT {{limit:10}}`,
|
||||
parameters: [{ name: 'limit', label: 'Limit', defaultValue: '10', type: 'number' }],
|
||||
builtIn: true,
|
||||
},
|
||||
{
|
||||
id: 'builtin-daily-stats',
|
||||
name: 'Tagesstatistik',
|
||||
description: 'Aggregierte Statistiken pro Tag',
|
||||
sql: `SELECT date as tag, SUM(events_count) as events,
|
||||
SUM(words_added) as woerter_hinzu, SUM(words_removed) as woerter_entfernt
|
||||
FROM daily_stats
|
||||
GROUP BY date
|
||||
ORDER BY date DESC
|
||||
LIMIT {{limit:14}}`,
|
||||
parameters: [{ name: 'limit', label: 'Tage', defaultValue: '14', type: 'number' }],
|
||||
builtIn: true,
|
||||
},
|
||||
{
|
||||
id: 'builtin-recent-files',
|
||||
name: 'Zuletzt geaenderte Dateien',
|
||||
description: 'Dateien nach Aenderungsdatum',
|
||||
sql: `SELECT name, extension,
|
||||
datetime(modified/1000, 'unixepoch', 'localtime') as geaendert,
|
||||
folder
|
||||
FROM _files
|
||||
WHERE extension = 'md'
|
||||
ORDER BY modified DESC
|
||||
LIMIT {{limit:20}}`,
|
||||
parameters: [{ name: 'limit', label: 'Limit', defaultValue: '20', type: 'number' }],
|
||||
builtIn: true,
|
||||
},
|
||||
{
|
||||
id: 'builtin-tag-stats',
|
||||
name: 'Tag-Statistiken',
|
||||
description: 'Notizen pro Tag zaehlen',
|
||||
sql: `SELECT tag, COUNT(DISTINCT path) as notizen
|
||||
FROM _tags
|
||||
GROUP BY tag
|
||||
ORDER BY notizen DESC
|
||||
LIMIT {{limit:30}}`,
|
||||
parameters: [{ name: 'limit', label: 'Limit', defaultValue: '30', type: 'number' }],
|
||||
builtIn: true,
|
||||
},
|
||||
{
|
||||
id: 'builtin-orphan-notes',
|
||||
name: 'Verwaiste Notizen',
|
||||
description: 'Notizen ohne eingehende Links',
|
||||
sql: `SELECT f.path, f.name,
|
||||
datetime(f.modified/1000, 'unixepoch', 'localtime') as geaendert
|
||||
FROM _files f
|
||||
WHERE f.extension = 'md'
|
||||
AND f.path NOT IN (SELECT DISTINCT to_path FROM _links)
|
||||
ORDER BY f.modified DESC`,
|
||||
parameters: [],
|
||||
builtIn: true,
|
||||
},
|
||||
{
|
||||
id: 'builtin-broken-links',
|
||||
name: 'Defekte Links',
|
||||
description: 'Links zu nicht-existierenden Notizen',
|
||||
sql: `SELECT l.from_path as von, l.to_path as nach, l.display_text
|
||||
FROM _links l
|
||||
WHERE l.to_path NOT IN (SELECT path FROM _files)
|
||||
AND l.link_type = 'link'`,
|
||||
parameters: [],
|
||||
builtIn: true,
|
||||
},
|
||||
{
|
||||
id: 'builtin-event-types',
|
||||
name: 'Event-Typen',
|
||||
description: 'Verteilung der Event-Typen',
|
||||
sql: `SELECT type as typ, category as kategorie, COUNT(*) as anzahl
|
||||
FROM events
|
||||
GROUP BY type, category
|
||||
ORDER BY anzahl DESC`,
|
||||
parameters: [],
|
||||
builtIn: true,
|
||||
},
|
||||
];
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// TemplatePickerModal
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export class TemplatePickerModal extends Modal {
|
||||
private manager: TemplateManager;
|
||||
private onSelect: (sql: string) => void;
|
||||
|
||||
constructor(plugin: LogfirePlugin, manager: TemplateManager, onSelect: (sql: string) => void) {
|
||||
super(plugin.app);
|
||||
this.manager = manager;
|
||||
this.onSelect = onSelect;
|
||||
}
|
||||
|
||||
onOpen(): void {
|
||||
const { contentEl } = this;
|
||||
contentEl.addClass('logfire-template-picker');
|
||||
contentEl.createEl('h2', { text: 'Query-Templates' });
|
||||
|
||||
const list = contentEl.createDiv({ cls: 'logfire-template-list' });
|
||||
|
||||
for (const tpl of this.manager.getAll()) {
|
||||
const item = list.createDiv({ cls: 'logfire-template-item' });
|
||||
|
||||
const header = item.createDiv({ cls: 'logfire-template-header' });
|
||||
header.createSpan({ cls: 'logfire-template-name', text: tpl.name });
|
||||
if (tpl.builtIn) {
|
||||
header.createSpan({ cls: 'logfire-template-badge', text: 'Built-in' });
|
||||
}
|
||||
|
||||
if (tpl.description) {
|
||||
item.createDiv({ cls: 'logfire-template-desc', text: tpl.description });
|
||||
}
|
||||
|
||||
item.createEl('pre', {
|
||||
cls: 'logfire-template-preview',
|
||||
text: tpl.sql.length > 120 ? tpl.sql.slice(0, 120) + '...' : tpl.sql,
|
||||
});
|
||||
|
||||
const actions = item.createDiv({ cls: 'logfire-template-actions' });
|
||||
const useBtn = actions.createEl('button', { text: 'Verwenden', cls: 'mod-cta' });
|
||||
useBtn.addEventListener('click', () => {
|
||||
if (tpl.parameters.length > 0) {
|
||||
this.close();
|
||||
new ParameterModal(this.app, tpl, values => {
|
||||
this.onSelect(TemplateManager.applyParameters(tpl.sql, values));
|
||||
}).open();
|
||||
} else {
|
||||
this.onSelect(tpl.sql);
|
||||
this.close();
|
||||
}
|
||||
});
|
||||
|
||||
if (!tpl.builtIn) {
|
||||
const delBtn = actions.createEl('button', { text: 'Entfernen' });
|
||||
delBtn.addEventListener('click', () => {
|
||||
this.manager.delete(tpl.id);
|
||||
item.remove();
|
||||
new Notice('Template entfernt.');
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onClose(): void { this.contentEl.empty(); }
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ParameterModal
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
class ParameterModal extends Modal {
|
||||
private template: QueryTemplate;
|
||||
private onSubmit: (values: Record<string, string>) => void;
|
||||
private values: Record<string, string> = {};
|
||||
|
||||
constructor(
|
||||
app: import('obsidian').App,
|
||||
template: QueryTemplate,
|
||||
onSubmit: (values: Record<string, string>) => void,
|
||||
) {
|
||||
super(app);
|
||||
this.template = template;
|
||||
this.onSubmit = onSubmit;
|
||||
for (const p of template.parameters) this.values[p.name] = p.defaultValue;
|
||||
}
|
||||
|
||||
onOpen(): void {
|
||||
const { contentEl } = this;
|
||||
contentEl.createEl('h3', { text: `Parameter: ${this.template.name}` });
|
||||
|
||||
for (const param of this.template.parameters) {
|
||||
new Setting(contentEl).setName(param.label).addText(text => {
|
||||
text.setValue(param.defaultValue).setPlaceholder(param.defaultValue)
|
||||
.onChange(v => { this.values[param.name] = v; });
|
||||
if (param.type === 'number') text.inputEl.type = 'number';
|
||||
if (param.type === 'date') text.inputEl.type = 'date';
|
||||
});
|
||||
}
|
||||
|
||||
const btns = contentEl.createDiv({ cls: 'logfire-modal-buttons' });
|
||||
const runBtn = btns.createEl('button', { text: 'Ausfuehren', cls: 'mod-cta' });
|
||||
runBtn.addEventListener('click', () => { this.onSubmit(this.values); this.close(); });
|
||||
const cancelBtn = btns.createEl('button', { text: 'Abbrechen' });
|
||||
cancelBtn.addEventListener('click', () => this.close());
|
||||
}
|
||||
|
||||
onClose(): void { this.contentEl.empty(); }
|
||||
}
|
||||
|
|
@ -3,15 +3,20 @@ import { QueryConfig, TimeRange, EventType } from '../types';
|
|||
import { buildQuery } from '../core/query-builder';
|
||||
import { DatabaseManager } from '../core/database';
|
||||
import { renderTable, toMarkdownTable } from '../viz/table-renderer';
|
||||
import { HistoryManager } from '../management/history';
|
||||
|
||||
export class QueryModal extends Modal {
|
||||
private editorEl!: HTMLTextAreaElement;
|
||||
private resultEl!: HTMLElement;
|
||||
private modeToggle!: HTMLButtonElement;
|
||||
private mode: 'shorthand' | 'sql' = 'shorthand';
|
||||
private historyManager?: HistoryManager;
|
||||
private initialSql?: string;
|
||||
|
||||
constructor(app: App, private db: DatabaseManager) {
|
||||
constructor(app: App, private db: DatabaseManager, historyManager?: HistoryManager, initialSql?: string) {
|
||||
super(app);
|
||||
this.historyManager = historyManager;
|
||||
this.initialSql = initialSql;
|
||||
}
|
||||
|
||||
onOpen(): void {
|
||||
|
|
@ -76,6 +81,14 @@ export class QueryModal extends Modal {
|
|||
|
||||
// Results
|
||||
this.resultEl = contentEl.createDiv({ cls: 'logfire-qm-results' });
|
||||
|
||||
// Initial SQL setzen
|
||||
if (this.initialSql) {
|
||||
this.mode = 'sql';
|
||||
this.modeToggle.textContent = 'Modus: SQL';
|
||||
this.editorEl.value = this.initialSql;
|
||||
this.editorEl.placeholder = 'SELECT * FROM events\nWHERE type = \'file:create\'\nORDER BY timestamp DESC\nLIMIT 50';
|
||||
}
|
||||
}
|
||||
|
||||
onClose(): void {
|
||||
|
|
@ -91,8 +104,11 @@ export class QueryModal extends Modal {
|
|||
|
||||
this.resultEl.empty();
|
||||
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
let rows: Record<string, unknown>[];
|
||||
let executedSql = input;
|
||||
|
||||
if (this.mode === 'sql') {
|
||||
// Raw SQL mode
|
||||
|
|
@ -109,9 +125,13 @@ export class QueryModal extends Modal {
|
|||
// Shorthand mode
|
||||
const config = parseShorthand(input);
|
||||
const { sql, params } = buildQuery(config);
|
||||
executedSql = sql;
|
||||
rows = this.db.queryReadOnly(sql, params) as Record<string, unknown>[];
|
||||
}
|
||||
|
||||
const elapsed = Date.now() - startTime;
|
||||
this.historyManager?.addEntry(executedSql, elapsed, rows.length);
|
||||
|
||||
this.lastRows = rows;
|
||||
this.lastKeys = rows.length > 0 ? Object.keys(rows[0]) : [];
|
||||
|
||||
|
|
|
|||
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 */ }
|
||||
}
|
||||
273
styles.css
273
styles.css
|
|
@ -924,3 +924,276 @@
|
|||
padding: 0 0 8px 0;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════════════════
|
||||
Schema Browser — Database Introspection Sidebar
|
||||
═══════════════════════════════════════════════════════════════════════════ */
|
||||
|
||||
.logfire-schema-view {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
background: var(--background-primary);
|
||||
}
|
||||
|
||||
.logfire-schema-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
padding: 8px 10px;
|
||||
border-bottom: 1px solid var(--background-modifier-border);
|
||||
background: var(--background-secondary);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.logfire-schema-title {
|
||||
font-family: var(--font-monospace);
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--text-normal);
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
.logfire-schema-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 4px 0;
|
||||
font-family: var(--font-monospace);
|
||||
font-size: 11.5px;
|
||||
}
|
||||
|
||||
.logfire-schema-content::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.logfire-schema-content::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.logfire-schema-content::-webkit-scrollbar-thumb {
|
||||
background: var(--background-modifier-border);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------------
|
||||
Schema — Table Node
|
||||
--------------------------------------------------------------------------- */
|
||||
|
||||
.logfire-schema-table {
|
||||
border-bottom: 1px solid color-mix(in srgb, var(--background-modifier-border) 40%, transparent);
|
||||
}
|
||||
|
||||
.logfire-schema-table-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
padding: 4px 10px;
|
||||
cursor: pointer;
|
||||
transition: background 60ms ease;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.logfire-schema-table-header:hover {
|
||||
background: var(--background-secondary);
|
||||
}
|
||||
|
||||
.logfire-schema-arrow {
|
||||
color: var(--text-faint);
|
||||
font-size: 9px;
|
||||
width: 10px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.logfire-schema-table-name {
|
||||
font-weight: 600;
|
||||
color: var(--text-accent);
|
||||
}
|
||||
|
||||
.logfire-schema-row-count {
|
||||
color: var(--text-faint);
|
||||
font-size: 10px;
|
||||
margin-left: auto;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------------
|
||||
Schema — Column & Index Details
|
||||
--------------------------------------------------------------------------- */
|
||||
|
||||
.logfire-schema-details {
|
||||
padding: 2px 0 4px 20px;
|
||||
}
|
||||
|
||||
.logfire-schema-col {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 1px 10px;
|
||||
color: var(--text-normal);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.logfire-schema-col:hover {
|
||||
background: color-mix(in srgb, var(--background-secondary) 40%, transparent);
|
||||
}
|
||||
|
||||
.logfire-schema-col-name {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.logfire-schema-col-type {
|
||||
color: var(--text-faint);
|
||||
font-size: 10px;
|
||||
margin-left: auto;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.logfire-schema-section-header {
|
||||
padding: 4px 10px 1px;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.03em;
|
||||
text-transform: uppercase;
|
||||
color: var(--text-muted);
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.logfire-schema-idx {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 1px 10px;
|
||||
color: var(--text-muted);
|
||||
font-size: 10.5px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.logfire-schema-idx-cols {
|
||||
color: var(--text-faint);
|
||||
font-size: 10px;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════════════════
|
||||
Template Picker & Favorite Modals
|
||||
═══════════════════════════════════════════════════════════════════════════ */
|
||||
|
||||
.logfire-template-picker h2,
|
||||
.logfire-save-fav-modal h3 {
|
||||
font-family: var(--font-monospace);
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.01em;
|
||||
margin-bottom: 10px;
|
||||
color: var(--text-normal);
|
||||
}
|
||||
|
||||
.logfire-template-list {
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.logfire-template-item {
|
||||
padding: 8px 10px;
|
||||
border-bottom: 1px solid var(--background-modifier-border);
|
||||
transition: background 60ms ease;
|
||||
}
|
||||
|
||||
.logfire-template-item:hover {
|
||||
background: color-mix(in srgb, var(--background-secondary) 40%, transparent);
|
||||
}
|
||||
|
||||
.logfire-template-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.logfire-template-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.logfire-template-name {
|
||||
font-family: var(--font-monospace);
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--text-normal);
|
||||
}
|
||||
|
||||
.logfire-template-badge {
|
||||
font-family: var(--font-monospace);
|
||||
font-size: 9px;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
padding: 1px 5px;
|
||||
border-radius: 3px;
|
||||
background: color-mix(in srgb, var(--interactive-accent) 15%, transparent);
|
||||
color: var(--interactive-accent);
|
||||
}
|
||||
|
||||
.logfire-template-desc {
|
||||
font-family: var(--font-monospace);
|
||||
font-size: 11px;
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.logfire-template-preview {
|
||||
font-family: var(--font-monospace);
|
||||
font-size: 10.5px;
|
||||
line-height: 1.4;
|
||||
color: var(--text-faint);
|
||||
background: var(--background-secondary);
|
||||
border-radius: 3px;
|
||||
padding: 4px 8px;
|
||||
margin: 4px 0;
|
||||
overflow: hidden;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.logfire-template-actions {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.logfire-template-actions button {
|
||||
font-family: var(--font-monospace);
|
||||
font-size: 11px;
|
||||
letter-spacing: 0.02em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------------
|
||||
Modal Shared — Buttons & Preview
|
||||
--------------------------------------------------------------------------- */
|
||||
|
||||
.logfire-modal-buttons {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.logfire-modal-buttons button {
|
||||
font-family: var(--font-monospace);
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
.logfire-sql-preview {
|
||||
font-family: var(--font-monospace);
|
||||
font-size: 11px;
|
||||
line-height: 1.4;
|
||||
color: var(--text-muted);
|
||||
background: var(--background-secondary);
|
||||
border-radius: 4px;
|
||||
padding: 6px 10px;
|
||||
margin: 4px 0;
|
||||
white-space: pre-wrap;
|
||||
max-height: 120px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue