222 lines
6.5 KiB
TypeScript
222 lines
6.5 KiB
TypeScript
|
|
import { Component, inject, signal } from '@angular/core';
|
||
|
|
import { CommonModule } from '@angular/common';
|
||
|
|
import { FormsModule } from '@angular/forms';
|
||
|
|
import { Router } from '@angular/router';
|
||
|
|
import { ApiService } from '../../core/services/api.service';
|
||
|
|
|
||
|
|
interface EnvVar {
|
||
|
|
key: string;
|
||
|
|
value: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
@Component({
|
||
|
|
selector: 'app-service-create',
|
||
|
|
standalone: true,
|
||
|
|
imports: [CommonModule, FormsModule],
|
||
|
|
template: `
|
||
|
|
<div class="px-4 sm:px-0">
|
||
|
|
<h1 class="text-3xl font-bold text-gray-900 mb-8">Deploy New Service</h1>
|
||
|
|
|
||
|
|
<div class="card max-w-3xl">
|
||
|
|
<form (ngSubmit)="onSubmit()">
|
||
|
|
<!-- Name -->
|
||
|
|
<div class="mb-6">
|
||
|
|
<label for="name" class="label">Service Name *</label>
|
||
|
|
<input
|
||
|
|
type="text"
|
||
|
|
id="name"
|
||
|
|
[(ngModel)]="name"
|
||
|
|
name="name"
|
||
|
|
required
|
||
|
|
placeholder="myapp"
|
||
|
|
class="input"
|
||
|
|
/>
|
||
|
|
<p class="mt-1 text-sm text-gray-500">Lowercase letters, numbers, and hyphens only</p>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Image -->
|
||
|
|
<div class="mb-6">
|
||
|
|
<label for="image" class="label">Docker Image *</label>
|
||
|
|
<input
|
||
|
|
type="text"
|
||
|
|
id="image"
|
||
|
|
[(ngModel)]="image"
|
||
|
|
name="image"
|
||
|
|
required
|
||
|
|
placeholder="nginx:latest"
|
||
|
|
class="input"
|
||
|
|
/>
|
||
|
|
<p class="mt-1 text-sm text-gray-500">Format: image:tag or registry/image:tag</p>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Port -->
|
||
|
|
<div class="mb-6">
|
||
|
|
<label for="port" class="label">Container Port *</label>
|
||
|
|
<input
|
||
|
|
type="number"
|
||
|
|
id="port"
|
||
|
|
[(ngModel)]="port"
|
||
|
|
name="port"
|
||
|
|
required
|
||
|
|
placeholder="80"
|
||
|
|
class="input"
|
||
|
|
/>
|
||
|
|
<p class="mt-1 text-sm text-gray-500">Port that your application listens on</p>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Domain -->
|
||
|
|
<div class="mb-6">
|
||
|
|
<label for="domain" class="label">Domain (Optional)</label>
|
||
|
|
<input
|
||
|
|
type="text"
|
||
|
|
id="domain"
|
||
|
|
[(ngModel)]="domain"
|
||
|
|
name="domain"
|
||
|
|
placeholder="app.example.com"
|
||
|
|
class="input"
|
||
|
|
/>
|
||
|
|
<p class="mt-1 text-sm text-gray-500">Leave empty to skip automatic DNS & SSL</p>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Environment Variables -->
|
||
|
|
<div class="mb-6">
|
||
|
|
<label class="label">Environment Variables</label>
|
||
|
|
@for (env of envVars(); track $index) {
|
||
|
|
<div class="flex gap-2 mb-2">
|
||
|
|
<input
|
||
|
|
type="text"
|
||
|
|
[(ngModel)]="env.key"
|
||
|
|
[name]="'envKey' + $index"
|
||
|
|
placeholder="KEY"
|
||
|
|
class="input flex-1"
|
||
|
|
/>
|
||
|
|
<input
|
||
|
|
type="text"
|
||
|
|
[(ngModel)]="env.value"
|
||
|
|
[name]="'envValue' + $index"
|
||
|
|
placeholder="value"
|
||
|
|
class="input flex-1"
|
||
|
|
/>
|
||
|
|
<button type="button" (click)="removeEnvVar($index)" class="btn btn-danger">
|
||
|
|
Remove
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
}
|
||
|
|
<button type="button" (click)="addEnvVar()" class="btn btn-secondary mt-2">
|
||
|
|
Add Environment Variable
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Options -->
|
||
|
|
<div class="mb-6">
|
||
|
|
<div class="flex items-center mb-2">
|
||
|
|
<input
|
||
|
|
type="checkbox"
|
||
|
|
id="autoDNS"
|
||
|
|
[(ngModel)]="autoDNS"
|
||
|
|
name="autoDNS"
|
||
|
|
class="h-4 w-4 text-primary-600 focus:ring-primary-500 border-gray-300 rounded"
|
||
|
|
/>
|
||
|
|
<label for="autoDNS" class="ml-2 block text-sm text-gray-900">
|
||
|
|
Configure DNS automatically
|
||
|
|
</label>
|
||
|
|
</div>
|
||
|
|
<div class="flex items-center">
|
||
|
|
<input
|
||
|
|
type="checkbox"
|
||
|
|
id="autoSSL"
|
||
|
|
[(ngModel)]="autoSSL"
|
||
|
|
name="autoSSL"
|
||
|
|
class="h-4 w-4 text-primary-600 focus:ring-primary-500 border-gray-300 rounded"
|
||
|
|
/>
|
||
|
|
<label for="autoSSL" class="ml-2 block text-sm text-gray-900">
|
||
|
|
Obtain SSL certificate automatically
|
||
|
|
</label>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
@if (error()) {
|
||
|
|
<div class="rounded-md bg-red-50 p-4 mb-6">
|
||
|
|
<p class="text-sm text-red-800">{{ error() }}</p>
|
||
|
|
</div>
|
||
|
|
}
|
||
|
|
|
||
|
|
<!-- Actions -->
|
||
|
|
<div class="flex justify-end space-x-4">
|
||
|
|
<button type="button" (click)="cancel()" class="btn btn-secondary">
|
||
|
|
Cancel
|
||
|
|
</button>
|
||
|
|
<button type="submit" [disabled]="loading()" class="btn btn-primary">
|
||
|
|
{{ loading() ? 'Deploying...' : 'Deploy Service' }}
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
</form>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
`,
|
||
|
|
})
|
||
|
|
export class ServiceCreateComponent {
|
||
|
|
private apiService = inject(ApiService);
|
||
|
|
private router = inject(Router);
|
||
|
|
|
||
|
|
name = '';
|
||
|
|
image = '';
|
||
|
|
port = 80;
|
||
|
|
domain = '';
|
||
|
|
autoDNS = true;
|
||
|
|
autoSSL = true;
|
||
|
|
envVars = signal<EnvVar[]>([]);
|
||
|
|
loading = signal(false);
|
||
|
|
error = signal('');
|
||
|
|
|
||
|
|
addEnvVar(): void {
|
||
|
|
this.envVars.update((vars) => [...vars, { key: '', value: '' }]);
|
||
|
|
}
|
||
|
|
|
||
|
|
removeEnvVar(index: number): void {
|
||
|
|
this.envVars.update((vars) => vars.filter((_, i) => i !== index));
|
||
|
|
}
|
||
|
|
|
||
|
|
onSubmit(): void {
|
||
|
|
this.error.set('');
|
||
|
|
this.loading.set(true);
|
||
|
|
|
||
|
|
// Convert env vars to object
|
||
|
|
const envVarsObj: Record<string, string> = {};
|
||
|
|
for (const env of this.envVars()) {
|
||
|
|
if (env.key && env.value) {
|
||
|
|
envVarsObj[env.key] = env.value;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
const data = {
|
||
|
|
name: this.name,
|
||
|
|
image: this.image,
|
||
|
|
port: this.port,
|
||
|
|
domain: this.domain || undefined,
|
||
|
|
envVars: envVarsObj,
|
||
|
|
autoDNS: this.autoDNS,
|
||
|
|
autoSSL: this.autoSSL,
|
||
|
|
};
|
||
|
|
|
||
|
|
this.apiService.createService(data).subscribe({
|
||
|
|
next: (response) => {
|
||
|
|
this.loading.set(false);
|
||
|
|
if (response.success) {
|
||
|
|
this.router.navigate(['/services']);
|
||
|
|
} else {
|
||
|
|
this.error.set(response.error || 'Failed to deploy service');
|
||
|
|
}
|
||
|
|
},
|
||
|
|
error: (err) => {
|
||
|
|
this.loading.set(false);
|
||
|
|
this.error.set(err.error?.error || 'An error occurred');
|
||
|
|
},
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
cancel(): void {
|
||
|
|
this.router.navigate(['/services']);
|
||
|
|
}
|
||
|
|
}
|