2025-11-24 01:31:15 +00:00
import { Component , OnInit , inject , signal } from '@angular/core' ;
2025-11-18 00:03:24 +00:00
import { CommonModule } from '@angular/common' ;
import { FormsModule } from '@angular/forms' ;
2025-11-24 01:31:15 +00:00
import { Router , RouterLink } from '@angular/router' ;
2025-11-18 00:03:24 +00:00
import { ApiService } from '../../core/services/api.service' ;
interface EnvVar {
key : string ;
value : string ;
}
2025-11-24 01:31:15 +00:00
interface Domain {
domain : string ;
dnsProvider : 'cloudflare' | 'manual' | null ;
isObsolete : boolean ;
}
2025-11-18 00:03:24 +00:00
@Component ( {
selector : 'app-service-create' ,
standalone : true ,
2025-11-24 01:31:15 +00:00
imports : [ CommonModule , FormsModule , RouterLink ] ,
2025-11-18 00:03:24 +00:00
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"
2025-11-24 01:31:15 +00:00
[ required ] = "!useOneboxRegistry"
[ disabled ] = "useOneboxRegistry"
2025-11-18 00:03:24 +00:00
placeholder = "nginx:latest"
class = "input"
/ >
< p class = "mt-1 text-sm text-gray-500" > Format : image : tag or registry / image :tag < / p >
< / div >
2025-11-24 01:31:15 +00:00
<!-- 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 >
< / div >
}
< / div >
2025-11-18 00:03:24 +00:00
<!-- 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"
2025-11-24 01:31:15 +00:00
( ngModelChange ) = "onDomainChange()"
2025-11-18 00:03:24 +00:00
name = "domain"
placeholder = "app.example.com"
2025-11-24 01:31:15 +00:00
list = "domainList"
2025-11-18 00:03:24 +00:00
class = "input"
2025-11-24 01:31:15 +00:00
[ class . border - red - 300 ] = "domainWarning()"
2025-11-18 00:03:24 +00:00
/ >
2025-11-24 01:31:15 +00:00
< 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 & rarr ;
< / a >
< / div >
< / div >
< / div >
< / 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 >
}
< / p >
}
2025-11-18 00:03:24 +00:00
< / 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 >
` ,
} )
2025-11-24 01:31:15 +00:00
export class ServiceCreateComponent implements OnInit {
2025-11-18 00:03:24 +00:00
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 ( '' ) ;
2025-11-24 01:31:15 +00:00
// Onebox Registry
useOneboxRegistry = false ;
registryImageTag = 'latest' ;
autoUpdateOnPush = false ;
// Domain validation
availableDomains = signal < Domain [ ] > ( [ ] ) ;
domainWarning = signal ( false ) ;
domainWarningTitle = signal ( '' ) ;
domainWarningMessage = signal ( '' ) ;
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 ) ;
}
}
2025-11-18 00:03:24 +00:00
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 ,
2025-11-24 01:31:15 +00:00
useOneboxRegistry : this.useOneboxRegistry ,
registryImageTag : this.useOneboxRegistry ? this . registryImageTag : undefined ,
autoUpdateOnPush : this.useOneboxRegistry ? this . autoUpdateOnPush : undefined ,
2025-11-18 00:03:24 +00:00
} ;
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' ] ) ;
}
}