ui rebuild
This commit is contained in:
@@ -1,354 +1,317 @@
|
||||
import { Component, OnInit, inject, signal } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { Component, inject, signal, OnInit } from '@angular/core';
|
||||
import { Router, RouterLink } from '@angular/router';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { ApiService } from '../../core/services/api.service';
|
||||
import { ToastService } from '../../core/services/toast.service';
|
||||
import { IServiceCreate, IDomainDetail } from '../../core/types/api.types';
|
||||
import {
|
||||
CardComponent,
|
||||
CardHeaderComponent,
|
||||
CardTitleComponent,
|
||||
CardDescriptionComponent,
|
||||
CardContentComponent,
|
||||
CardFooterComponent,
|
||||
} from '../../ui/card/card.component';
|
||||
import { ButtonComponent } from '../../ui/button/button.component';
|
||||
import { InputComponent } from '../../ui/input/input.component';
|
||||
import { LabelComponent } from '../../ui/label/label.component';
|
||||
import { CheckboxComponent } from '../../ui/checkbox/checkbox.component';
|
||||
import { AlertComponent, AlertDescriptionComponent } from '../../ui/alert/alert.component';
|
||||
import { SeparatorComponent } from '../../ui/separator/separator.component';
|
||||
|
||||
interface EnvVar {
|
||||
key: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
interface Domain {
|
||||
domain: string;
|
||||
dnsProvider: 'cloudflare' | 'manual' | null;
|
||||
isObsolete: boolean;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-service-create',
|
||||
standalone: true,
|
||||
imports: [CommonModule, FormsModule, RouterLink],
|
||||
imports: [
|
||||
FormsModule,
|
||||
RouterLink,
|
||||
CardComponent,
|
||||
CardHeaderComponent,
|
||||
CardTitleComponent,
|
||||
CardDescriptionComponent,
|
||||
CardContentComponent,
|
||||
CardFooterComponent,
|
||||
ButtonComponent,
|
||||
InputComponent,
|
||||
LabelComponent,
|
||||
CheckboxComponent,
|
||||
AlertComponent,
|
||||
AlertDescriptionComponent,
|
||||
SeparatorComponent,
|
||||
],
|
||||
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="max-w-2xl mx-auto space-y-6">
|
||||
<!-- Header -->
|
||||
<div>
|
||||
<a routerLink="/services" class="text-sm text-muted-foreground hover:text-foreground inline-flex items-center gap-1 mb-2">
|
||||
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15 19l-7-7 7-7" />
|
||||
</svg>
|
||||
Back to Services
|
||||
</a>
|
||||
<h1 class="text-3xl font-bold tracking-tight">Deploy Service</h1>
|
||||
<p class="text-muted-foreground">Deploy a new Docker service</p>
|
||||
</div>
|
||||
|
||||
<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]="!useOneboxRegistry"
|
||||
[disabled]="useOneboxRegistry"
|
||||
placeholder="nginx:latest"
|
||||
class="input"
|
||||
/>
|
||||
<p class="mt-1 text-sm text-gray-500">Format: image:tag or registry/image:tag</p>
|
||||
</div>
|
||||
|
||||
<!-- Onebox Registry Option -->
|
||||
<div class="mb-6 bg-blue-50 border border-blue-200 rounded-lg p-4">
|
||||
<div class="flex items-center mb-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="useOneboxRegistry"
|
||||
[(ngModel)]="useOneboxRegistry"
|
||||
name="useOneboxRegistry"
|
||||
class="h-4 w-4 text-primary-600 focus:ring-primary-500 border-gray-300 rounded"
|
||||
/>
|
||||
<label for="useOneboxRegistry" class="ml-2 block text-sm font-medium text-gray-900">
|
||||
Use Onebox Registry
|
||||
</label>
|
||||
</div>
|
||||
<p class="text-sm text-gray-600 mb-3">
|
||||
Store your container image in the local Onebox registry instead of using an external image.
|
||||
</p>
|
||||
|
||||
@if (useOneboxRegistry) {
|
||||
<div class="space-y-3">
|
||||
<div>
|
||||
<label for="registryImageTag" class="label text-sm">Image Tag</label>
|
||||
<input
|
||||
type="text"
|
||||
id="registryImageTag"
|
||||
[(ngModel)]="registryImageTag"
|
||||
name="registryImageTag"
|
||||
placeholder="latest"
|
||||
class="input text-sm"
|
||||
/>
|
||||
<p class="mt-1 text-xs text-gray-500">Tag to use (e.g., latest, v1.0, develop)</p>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="autoUpdateOnPush"
|
||||
[(ngModel)]="autoUpdateOnPush"
|
||||
name="autoUpdateOnPush"
|
||||
class="h-4 w-4 text-primary-600 focus:ring-primary-500 border-gray-300 rounded"
|
||||
/>
|
||||
<label for="autoUpdateOnPush" class="ml-2 block text-sm text-gray-700">
|
||||
Auto-restart on new image push
|
||||
</label>
|
||||
</div>
|
||||
<p class="text-xs text-gray-500 ml-6">
|
||||
Automatically pull and restart the service when a new image is pushed to the registry
|
||||
</p>
|
||||
<form (ngSubmit)="onSubmit()">
|
||||
<ui-card>
|
||||
<ui-card-header class="flex flex-col space-y-1.5">
|
||||
<ui-card-title>Service Configuration</ui-card-title>
|
||||
<ui-card-description>Configure your service settings</ui-card-description>
|
||||
</ui-card-header>
|
||||
<ui-card-content class="space-y-6">
|
||||
<!-- Basic Configuration -->
|
||||
<div class="grid gap-4">
|
||||
<div class="space-y-2">
|
||||
<label uiLabel for="name">Service Name</label>
|
||||
<input
|
||||
uiInput
|
||||
id="name"
|
||||
[(ngModel)]="form.name"
|
||||
name="name"
|
||||
placeholder="my-service"
|
||||
required
|
||||
pattern="[a-z0-9-]+"
|
||||
/>
|
||||
<p class="text-xs text-muted-foreground">Lowercase letters, numbers, and hyphens only</p>
|
||||
</div>
|
||||
}
|
||||
</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"
|
||||
(ngModelChange)="onDomainChange()"
|
||||
name="domain"
|
||||
placeholder="app.example.com"
|
||||
list="domainList"
|
||||
class="input"
|
||||
[class.border-red-300]="domainWarning()"
|
||||
/>
|
||||
<datalist id="domainList">
|
||||
@for (domain of availableDomains(); track domain.domain) {
|
||||
<option [value]="domain.domain">{{ domain.domain }}</option>
|
||||
}
|
||||
</datalist>
|
||||
|
||||
@if (domainWarning()) {
|
||||
<div class="mt-2 rounded-md bg-yellow-50 p-3">
|
||||
<div class="flex">
|
||||
<div class="flex-shrink-0">
|
||||
<svg class="h-5 w-5 text-yellow-400" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<p class="text-sm text-yellow-800">
|
||||
<strong>{{ domainWarningTitle() }}</strong>
|
||||
</p>
|
||||
<p class="mt-1 text-sm text-yellow-700">{{ domainWarningMessage() }}</p>
|
||||
<div class="mt-2">
|
||||
<a routerLink="/domains" class="text-sm font-medium text-yellow-800 hover:text-yellow-900 underline">
|
||||
View domains →
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<label uiLabel for="image">Docker Image</label>
|
||||
<input
|
||||
uiInput
|
||||
id="image"
|
||||
[(ngModel)]="form.image"
|
||||
name="image"
|
||||
placeholder="nginx:latest"
|
||||
required
|
||||
/>
|
||||
<p class="text-xs text-muted-foreground">e.g., nginx:latest, registry.example.com/image:tag</p>
|
||||
</div>
|
||||
} @else {
|
||||
<p class="mt-1 text-sm text-gray-500">
|
||||
Leave empty to skip automatic DNS & SSL.
|
||||
@if (availableDomains().length > 0) {
|
||||
<span>Or select from {{ availableDomains().length }} available domain(s).</span>
|
||||
|
||||
<div class="space-y-2">
|
||||
<label uiLabel for="port">Container Port</label>
|
||||
<input
|
||||
uiInput
|
||||
id="port"
|
||||
type="number"
|
||||
[(ngModel)]="form.port"
|
||||
name="port"
|
||||
placeholder="80"
|
||||
required
|
||||
min="1"
|
||||
max="65535"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<label uiLabel for="domain">Domain (optional)</label>
|
||||
<input
|
||||
uiInput
|
||||
id="domain"
|
||||
[(ngModel)]="form.domain"
|
||||
name="domain"
|
||||
placeholder="app.example.com"
|
||||
list="domains-list"
|
||||
/>
|
||||
<datalist id="domains-list">
|
||||
@for (d of domains(); track d.domain.domain) {
|
||||
<option [value]="d.domain.domain">{{ d.domain.domain }}</option>
|
||||
}
|
||||
</datalist>
|
||||
@if (domainWarning()) {
|
||||
<ui-alert variant="warning" class="mt-2">
|
||||
<ui-alert-description>{{ domainWarning() }}</ui-alert-description>
|
||||
</ui-alert>
|
||||
}
|
||||
</p>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</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
|
||||
<ui-separator />
|
||||
|
||||
<!-- Environment Variables -->
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h3 class="text-sm font-medium">Environment Variables</h3>
|
||||
<p class="text-xs text-muted-foreground">Configure environment variables for your service</p>
|
||||
</div>
|
||||
<button uiButton variant="outline" size="sm" type="button" (click)="addEnvVar()">
|
||||
<svg class="h-4 w-4 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4v16m8-8H4" />
|
||||
</svg>
|
||||
Add
|
||||
</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>
|
||||
@if (envVars().length > 0) {
|
||||
<div class="space-y-2">
|
||||
@for (env of envVars(); track $index; let i = $index) {
|
||||
<div class="flex gap-2">
|
||||
<input
|
||||
uiInput
|
||||
[(ngModel)]="env.key"
|
||||
[name]="'env-key-' + i"
|
||||
placeholder="KEY"
|
||||
class="flex-1"
|
||||
/>
|
||||
<input
|
||||
uiInput
|
||||
[(ngModel)]="env.value"
|
||||
[name]="'env-value-' + i"
|
||||
placeholder="value"
|
||||
class="flex-1"
|
||||
/>
|
||||
<button
|
||||
uiButton
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
type="button"
|
||||
(click)="removeEnvVar(i)"
|
||||
>
|
||||
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</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>
|
||||
}
|
||||
<ui-separator />
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="flex justify-end space-x-4">
|
||||
<button type="button" (click)="cancel()" class="btn btn-secondary">
|
||||
Cancel
|
||||
<!-- Onebox Registry -->
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<ui-checkbox
|
||||
[checked]="form.useOneboxRegistry ?? false"
|
||||
(checkedChange)="form.useOneboxRegistry = $event"
|
||||
/>
|
||||
<div>
|
||||
<label uiLabel class="cursor-pointer">Use Onebox Registry</label>
|
||||
<p class="text-xs text-muted-foreground">Push images directly to this Onebox instance</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (form.useOneboxRegistry) {
|
||||
<div class="pl-7 space-y-4">
|
||||
<div class="space-y-2">
|
||||
<label uiLabel for="registryImageTag">Image Tag</label>
|
||||
<input
|
||||
uiInput
|
||||
id="registryImageTag"
|
||||
[(ngModel)]="form.registryImageTag"
|
||||
name="registryImageTag"
|
||||
placeholder="latest"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-3">
|
||||
<ui-checkbox
|
||||
[checked]="form.autoUpdateOnPush ?? false"
|
||||
(checkedChange)="form.autoUpdateOnPush = $event"
|
||||
/>
|
||||
<label uiLabel class="cursor-pointer">Auto-restart on push</label>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</ui-card-content>
|
||||
<ui-card-footer class="flex justify-between">
|
||||
<a routerLink="/services">
|
||||
<button uiButton variant="outline" type="button">Cancel</button>
|
||||
</a>
|
||||
<button uiButton type="submit" [disabled]="loading()">
|
||||
@if (loading()) {
|
||||
<svg class="animate-spin h-4 w-4 mr-2" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path>
|
||||
</svg>
|
||||
Deploying...
|
||||
} @else {
|
||||
Deploy Service
|
||||
}
|
||||
</button>
|
||||
<button type="submit" [disabled]="loading()" class="btn btn-primary">
|
||||
{{ loading() ? 'Deploying...' : 'Deploy Service' }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</ui-card-footer>
|
||||
</ui-card>
|
||||
</form>
|
||||
</div>
|
||||
`,
|
||||
})
|
||||
export class ServiceCreateComponent implements OnInit {
|
||||
private apiService = inject(ApiService);
|
||||
private api = inject(ApiService);
|
||||
private router = inject(Router);
|
||||
private toast = inject(ToastService);
|
||||
|
||||
form: IServiceCreate = {
|
||||
name: '',
|
||||
image: '',
|
||||
port: 80,
|
||||
domain: '',
|
||||
useOneboxRegistry: false,
|
||||
registryImageTag: 'latest',
|
||||
autoUpdateOnPush: false,
|
||||
};
|
||||
|
||||
name = '';
|
||||
image = '';
|
||||
port = 80;
|
||||
domain = '';
|
||||
autoDNS = true;
|
||||
autoSSL = true;
|
||||
envVars = signal<EnvVar[]>([]);
|
||||
domains = signal<IDomainDetail[]>([]);
|
||||
loading = signal(false);
|
||||
error = signal('');
|
||||
|
||||
// Onebox Registry
|
||||
useOneboxRegistry = false;
|
||||
registryImageTag = 'latest';
|
||||
autoUpdateOnPush = false;
|
||||
|
||||
// Domain validation
|
||||
availableDomains = signal<Domain[]>([]);
|
||||
domainWarning = signal(false);
|
||||
domainWarningTitle = signal('');
|
||||
domainWarningMessage = signal('');
|
||||
domainWarning = signal<string | null>(null);
|
||||
|
||||
ngOnInit(): void {
|
||||
this.loadDomains();
|
||||
}
|
||||
|
||||
loadDomains(): void {
|
||||
this.apiService.getDomains().subscribe({
|
||||
next: (response) => {
|
||||
if (response.success && response.data) {
|
||||
const domains: Domain[] = response.data.map((d: any) => ({
|
||||
domain: d.domain.domain,
|
||||
dnsProvider: d.domain.dnsProvider,
|
||||
isObsolete: d.domain.isObsolete,
|
||||
}));
|
||||
this.availableDomains.set(domains);
|
||||
}
|
||||
},
|
||||
error: () => {
|
||||
// Silently fail - domains list not critical
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
onDomainChange(): void {
|
||||
if (!this.domain) {
|
||||
this.domainWarning.set(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Extract base domain from entered domain
|
||||
const parts = this.domain.split('.');
|
||||
if (parts.length < 2) {
|
||||
// Not a valid domain format
|
||||
this.domainWarning.set(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const baseDomain = parts.slice(-2).join('.');
|
||||
|
||||
// Check if base domain exists in available domains
|
||||
const matchingDomain = this.availableDomains().find(
|
||||
(d) => d.domain === baseDomain
|
||||
);
|
||||
|
||||
if (!matchingDomain) {
|
||||
this.domainWarning.set(true);
|
||||
this.domainWarningTitle.set('Domain not found');
|
||||
this.domainWarningMessage.set(
|
||||
`The base domain "${baseDomain}" is not in the Domain table. The service will deploy, but certificate management may not work. Sync your Cloudflare domains or manually add the domain first.`
|
||||
);
|
||||
} else if (matchingDomain.isObsolete) {
|
||||
this.domainWarning.set(true);
|
||||
this.domainWarningTitle.set('Domain is obsolete');
|
||||
this.domainWarningMessage.set(
|
||||
`The domain "${baseDomain}" is marked as obsolete (likely removed from Cloudflare). Certificate management may not work properly.`
|
||||
);
|
||||
} else {
|
||||
this.domainWarning.set(false);
|
||||
async loadDomains(): Promise<void> {
|
||||
try {
|
||||
const response = await this.api.getDomains();
|
||||
if (response.success && response.data) {
|
||||
this.domains.set(response.data);
|
||||
}
|
||||
} catch {
|
||||
// Silent fail - domain autocomplete is optional
|
||||
}
|
||||
}
|
||||
|
||||
addEnvVar(): void {
|
||||
this.envVars.update((vars) => [...vars, { key: '', value: '' }]);
|
||||
this.envVars.update(vars => [...vars, { key: '', value: '' }]);
|
||||
}
|
||||
|
||||
removeEnvVar(index: number): void {
|
||||
this.envVars.update((vars) => vars.filter((_, i) => i !== index));
|
||||
this.envVars.update(vars => vars.filter((_, i) => i !== index));
|
||||
}
|
||||
|
||||
onSubmit(): void {
|
||||
this.error.set('');
|
||||
validateDomain(): void {
|
||||
if (!this.form.domain) {
|
||||
this.domainWarning.set(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const domain = this.domains().find(d => d.domain.domain === this.form.domain);
|
||||
if (!domain) {
|
||||
this.domainWarning.set('This domain is not in your domain list. DNS and SSL may not be configured automatically.');
|
||||
} else if (domain.domain.isObsolete) {
|
||||
this.domainWarning.set('This domain is marked as obsolete.');
|
||||
} else {
|
||||
this.domainWarning.set(null);
|
||||
}
|
||||
}
|
||||
|
||||
async onSubmit(): Promise<void> {
|
||||
if (!this.form.name || !this.form.image || !this.form.port) {
|
||||
this.toast.error('Please fill in all required fields');
|
||||
return;
|
||||
}
|
||||
|
||||
this.loading.set(true);
|
||||
|
||||
// Convert env vars to object
|
||||
// Build env vars object
|
||||
const envVarsObj: Record<string, string> = {};
|
||||
for (const env of this.envVars()) {
|
||||
if (env.key && env.value) {
|
||||
@@ -356,36 +319,23 @@ export class ServiceCreateComponent implements OnInit {
|
||||
}
|
||||
}
|
||||
|
||||
const data = {
|
||||
name: this.name,
|
||||
image: this.image,
|
||||
port: this.port,
|
||||
domain: this.domain || undefined,
|
||||
envVars: envVarsObj,
|
||||
autoDNS: this.autoDNS,
|
||||
autoSSL: this.autoSSL,
|
||||
useOneboxRegistry: this.useOneboxRegistry,
|
||||
registryImageTag: this.useOneboxRegistry ? this.registryImageTag : undefined,
|
||||
autoUpdateOnPush: this.useOneboxRegistry ? this.autoUpdateOnPush : undefined,
|
||||
const data: IServiceCreate = {
|
||||
...this.form,
|
||||
envVars: Object.keys(envVarsObj).length > 0 ? envVarsObj : undefined,
|
||||
};
|
||||
|
||||
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']);
|
||||
try {
|
||||
const response = await this.api.createService(data);
|
||||
if (response.success) {
|
||||
this.toast.success(`Service "${this.form.name}" deployed successfully`);
|
||||
this.router.navigate(['/services']);
|
||||
} else {
|
||||
this.toast.error(response.error || 'Failed to deploy service');
|
||||
}
|
||||
} catch {
|
||||
this.toast.error('Failed to deploy service');
|
||||
} finally {
|
||||
this.loading.set(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user