Files
big-brother/web/src/app/pages/projects/project-new.ts
T
2026-10-12 14:25:22 +02:00

178 lines
5.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { ChangeDetectionStrategy, Component, computed, inject, signal } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { RouterLink } from '@angular/router';
import { Api } from '../../core/api';
import { errorMessage } from '../../core/format';
import type {
AuditStatus,
CreateProjectInput,
ProjectOptions,
ProjectType,
} from '../../core/models';
import { Icon } from '../../shared/icon';
import { JobConsole } from '../../shared/job-console';
const TYPES: { value: ProjectType; label: string; description: string }[] = [
{
value: 'node',
label: 'Node.js',
description: 'Une app Node lancée par une commande (node start.js…).',
},
{
value: 'adonis',
label: 'AdonisJS',
description: 'Compilée (node ace build) puis lancée en production.',
},
{ value: 'wordpress', label: 'WordPress', description: 'Image officielle, base MariaDB dédiée.' },
{
value: 'static',
label: 'Site statique',
description: 'Fichiers servis directement par Caddy.',
},
];
const NAME_PATTERN = /^[a-z0-9][a-z0-9-]{0,40}$/;
@Component({
selector: 'app-project-new',
imports: [FormsModule, RouterLink, Icon, JobConsole],
templateUrl: './project-new.html',
styleUrl: './project-new.scss',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ProjectNewPage {
private api = inject(Api);
protected readonly types = TYPES;
protected readonly options = signal<ProjectOptions | null>(null);
protected readonly type = signal<ProjectType>('node');
protected readonly name = signal('');
protected readonly category = signal('api');
protected readonly gitUrl = signal('');
protected readonly port = signal(3333);
protected readonly command = signal('');
protected readonly env = signal('');
protected readonly createDatabase = signal(false);
protected readonly addCaddy = signal(true);
protected readonly spa = signal(true);
/**
* Null while the domain follows the name.
*/
protected readonly customDomain = signal<string | null>(null);
protected readonly submitting = signal(false);
protected readonly error = signal<string | null>(null);
protected readonly jobId = signal<number | null>(null);
protected readonly jobStatus = signal<AuditStatus | null>(null);
protected readonly domain = computed(
() =>
this.customDomain() ??
(this.name() ? `${this.name()}.${this.options()?.baseDomain ?? ''}` : ''),
);
protected readonly isApp = computed(() => this.type() === 'node' || this.type() === 'adonis');
protected readonly nameValid = computed(() => NAME_PATTERN.test(this.name()));
protected readonly targetPath = computed(() => {
const root = this.options()?.srvRoot ?? '/srv';
const folder =
this.type() === 'static'
? 'html'
: this.type() === 'wordpress'
? 'wordpress'
: this.category();
return `${root}/${folder}/${this.name() || '<nom>'}`.replace(/\\/g, '/');
});
/**
* What the creation will do, step by step.
*/
protected readonly steps = computed(() => {
const steps: string[] = [`Créer le dossier ${this.targetPath()}`];
const type = this.type();
if (type === 'static') {
steps.push('Y mettre une page d’attente (à remplacer par ton build)');
} else {
if (this.isApp()) steps.push('Cloner le dépôt dans project/');
if (type === 'wordpress' || this.createDatabase()) {
steps.push(
`Créer la base MariaDB « ${(this.name() || '<nom>').replace(/-/g, '_')} » et son utilisateur`,
);
}
steps.push(
type === 'wordpress' ? 'Écrire le .env (identifiants de la base)' : 'Écrire project/.env',
);
steps.push('Écrire le docker-compose.yml (template de la procédure)');
steps.push('Lancer : docker compose up -d');
}
if (this.addCaddy())
steps.push(`Ajouter ${this.domain() || '<domaine>'} au Caddyfile et recharger Caddy`);
return steps;
});
constructor() {
this.api
.projectOptions()
.then((options) => {
this.options.set(options);
this.command.set(options.defaultNodeCommand);
})
.catch((error) => this.error.set(errorMessage(error)));
}
protected selectType(type: ProjectType) {
this.type.set(type);
if (type === 'wordpress') this.category.set('wordpress');
else if (this.category() === 'wordpress') this.category.set('api');
}
protected async submit() {
this.error.set(null);
if (!this.nameValid()) {
this.error.set('Le nom ne peut contenir que des minuscules, des chiffres et des tirets.');
return;
}
if (this.isApp() && !this.gitUrl().trim()) {
this.error.set('L’URL du dépôt git est obligatoire.');
return;
}
const type = this.type();
const input: CreateProjectInput = {
type,
name: this.name(),
category: type === 'wordpress' ? 'wordpress' : this.category(),
createDatabase: type === 'wordpress' || (this.isApp() && this.createDatabase()),
domain: this.domain(),
addCaddy: this.addCaddy(),
spa: this.spa(),
};
if (this.isApp()) {
input.gitUrl = this.gitUrl().trim();
input.port = Number(this.port());
input.env = this.env();
if (type === 'node') input.command = this.command().trim();
}
this.submitting.set(true);
try {
const { jobId } = await this.api.createProject(input);
this.jobStatus.set(null);
this.jobId.set(jobId);
} catch (error) {
this.error.set(errorMessage(error));
} finally {
this.submitting.set(false);
}
}
protected projectLink() {
const type = this.type();
const category =
type === 'static' ? 'html' : type === 'wordpress' ? 'wordpress' : this.category();
return ['/projects', category, this.name()];
}
}