128 lines
4.3 KiB
TypeScript
128 lines
4.3 KiB
TypeScript
import { ChangeDetectionStrategy, Component, computed, inject, signal } from '@angular/core';
|
||
import { HttpErrorResponse } from '@angular/common/http';
|
||
import { FormsModule } from '@angular/forms';
|
||
import { Api } from '../../core/api';
|
||
import { Feedback } from '../../core/feedback';
|
||
import { errorMessage, formatDateTime } from '../../core/format';
|
||
import type { CaddyInfo, CaddyVersion } from '../../core/models';
|
||
import { Icon } from '../../shared/icon';
|
||
|
||
@Component({
|
||
selector: 'app-caddy',
|
||
imports: [FormsModule, Icon],
|
||
templateUrl: './caddy.html',
|
||
styleUrl: './caddy.scss',
|
||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||
})
|
||
export class CaddyPage {
|
||
private api = inject(Api);
|
||
private feedback = inject(Feedback);
|
||
|
||
protected readonly info = signal<CaddyInfo | null>(null);
|
||
protected readonly content = signal('');
|
||
protected readonly reason = signal('');
|
||
protected readonly saving = signal(false);
|
||
protected readonly error = signal<string | null>(null);
|
||
protected readonly output = signal<{ ok: boolean; text: string } | null>(null);
|
||
protected readonly versions = signal<CaddyVersion[]>([]);
|
||
protected readonly viewed = signal<CaddyVersion | null>(null);
|
||
|
||
protected readonly dirty = computed(
|
||
() => this.info() !== null && this.content() !== this.info()!.content,
|
||
);
|
||
protected readonly lineCount = computed(() => this.content().split('\n').length);
|
||
protected readonly dateTime = formatDateTime;
|
||
|
||
constructor() {
|
||
this.load();
|
||
}
|
||
|
||
protected async load() {
|
||
try {
|
||
const [info, versions] = await Promise.all([this.api.caddy(), this.api.caddyVersions()]);
|
||
this.info.set(info);
|
||
this.content.set(info.content);
|
||
this.versions.set(versions);
|
||
this.error.set(null);
|
||
} catch (error) {
|
||
this.error.set(errorMessage(error));
|
||
}
|
||
}
|
||
|
||
protected async save() {
|
||
if (!this.dirty()) return;
|
||
const ok = await this.feedback.confirm({
|
||
title: 'Enregistrer et recharger Caddy ?',
|
||
message:
|
||
'Si Caddy refuse la nouvelle configuration, l’ancien fichier est remis en place automatiquement.',
|
||
confirmLabel: 'Enregistrer et recharger',
|
||
});
|
||
if (!ok) return;
|
||
await this.run(
|
||
() => this.api.saveCaddy(this.content(), this.reason().trim()),
|
||
'Caddyfile enregistré et Caddy rechargé.',
|
||
);
|
||
}
|
||
|
||
protected async reload() {
|
||
await this.run(() => this.api.reloadCaddy(), 'Caddy rechargé.');
|
||
}
|
||
|
||
protected reset() {
|
||
this.content.set(this.info()?.content ?? '');
|
||
this.output.set(null);
|
||
}
|
||
|
||
protected async view(version: CaddyVersion) {
|
||
try {
|
||
this.viewed.set(await this.api.caddyVersion(version.id));
|
||
} catch (error) {
|
||
this.feedback.error(errorMessage(error));
|
||
}
|
||
}
|
||
|
||
protected async restore(version: CaddyVersion) {
|
||
const ok = await this.feedback.confirm({
|
||
title: `Restaurer la version #${version.id} ?`,
|
||
message: `Version enregistrée le ${formatDateTime(version.createdAt)}. Le fichier actuel est lui-même gardé dans l’historique.`,
|
||
confirmLabel: 'Restaurer',
|
||
});
|
||
if (!ok) return;
|
||
this.viewed.set(null);
|
||
await this.run(() => this.api.restoreCaddy(version.id), `Version #${version.id} restaurée.`);
|
||
}
|
||
|
||
/**
|
||
* Tab inserts a tab character (Caddyfile indentation) instead of
|
||
* leaving the field.
|
||
*/
|
||
protected onKeydown(event: KeyboardEvent) {
|
||
if (event.key !== 'Tab' || event.shiftKey) return;
|
||
event.preventDefault();
|
||
const textarea = event.target as HTMLTextAreaElement;
|
||
const { selectionStart, selectionEnd, value } = textarea;
|
||
const next = `${value.slice(0, selectionStart)}\t${value.slice(selectionEnd)}`;
|
||
this.content.set(next);
|
||
textarea.value = next;
|
||
textarea.selectionStart = textarea.selectionEnd = selectionStart + 1;
|
||
}
|
||
|
||
private async run(action: () => Promise<{ output: string }>, success: string) {
|
||
this.saving.set(true);
|
||
this.output.set(null);
|
||
try {
|
||
const result = await action();
|
||
this.output.set({ ok: true, text: result.output || 'OK' });
|
||
this.feedback.success(success);
|
||
this.reason.set('');
|
||
await this.load();
|
||
} catch (error) {
|
||
const body = error instanceof HttpErrorResponse ? error.error : null;
|
||
this.output.set({ ok: false, text: body?.output || errorMessage(error) });
|
||
this.feedback.error(errorMessage(error));
|
||
} finally {
|
||
this.saving.set(false);
|
||
}
|
||
}
|
||
}
|