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