Templates: Built-in und Custom Query-Templates mit Parametern
TemplateManager mit 9 Built-in Templates fuer Logfire-Schema
(Events, Sessions, Stats, Dateien, Tags, verwaiste Notizen,
defekte Links). Parameter-Substitution via {{name:default}}.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
6c7d239108
commit
b9732810f9
1 changed files with 356 additions and 0 deletions
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(); }
|
||||
}
|
||||
Loading…
Reference in a new issue