2026-01-03 02:44:25 +00:00
import {
DeesElement ,
customElement ,
html ,
css ,
cssManager ,
property ,
state ,
type TemplateResult ,
} from '@design.estate/dees-element' ;
declare global {
interface HTMLElementTagNameMap {
'sz-service-create-view' : SzServiceCreateView ;
}
}
export interface IRegistry {
id : string ;
name : string ;
url : string ;
}
export interface IPortMapping {
hostPort : string ;
containerPort : string ;
protocol : 'tcp' | 'udp' ;
}
export interface IEnvVar {
key : string ;
value : string ;
}
export interface IVolumeMount {
hostPath : string ;
containerPath : string ;
readOnly : boolean ;
}
export interface IServiceConfig {
name : string ;
image : string ;
ports : IPortMapping [ ] ;
envVars : IEnvVar [ ] ;
volumes : IVolumeMount [ ] ;
cpuLimit : string ;
memoryLimit : string ;
restartPolicy : 'always' | 'on-failure' | 'never' ;
networkMode : string ;
2026-03-16 11:44:44 +00:00
enableMongoDB : boolean ;
enableS3 : boolean ;
enableClickHouse : boolean ;
2026-01-03 02:44:25 +00:00
}
@customElement ( 'sz-service-create-view' )
export class SzServiceCreateView extends DeesElement {
public static demo = ( ) = > html `
<div style="padding: 24px; max-width: 800px;">
<sz-service-create-view
.registries= ${ [
{ id : '1' , name : 'Onebox Registry' , url : 'registry.onebox.local' } ,
{ id: '2', name: 'Docker Hub', url: 'docker.io' },
]}
></sz-service-create-view>
</div>
` ;
2026-02-20 13:29:07 +00:00
public static demoGroups = [ 'Services' ] ;
2026-01-03 02:44:25 +00:00
@property ( { type : Array } )
public accessor registries : IRegistry [ ] = [ ] ;
@property ( { type : Boolean } )
public accessor loading : boolean = false ;
@state ( )
private accessor serviceName : string = '' ;
@state ( )
private accessor imageUrl : string = '' ;
@state ( )
private accessor selectedRegistry : string = '' ;
@state ( )
private accessor ports : IPortMapping [ ] = [ { hostPort : '' , containerPort : '' , protocol : 'tcp' } ] ;
@state ( )
private accessor envVars : IEnvVar [ ] = [ { key : '' , value : '' } ] ;
@state ( )
private accessor volumes : IVolumeMount [ ] = [ ] ;
@state ( )
private accessor cpuLimit : string = '' ;
@state ( )
private accessor memoryLimit : string = '' ;
@state ( )
private accessor restartPolicy : 'always' | 'on-failure' | 'never' = 'always' ;
@state ( )
private accessor networkMode : string = 'bridge' ;
@state ( )
private accessor showAdvanced : boolean = false ;
2026-03-16 11:44:44 +00:00
@state ( )
private accessor enableMongoDB : boolean = false ;
@state ( )
private accessor enableS3 : boolean = false ;
@state ( )
private accessor enableClickHouse : boolean = false ;
2026-01-03 02:44:25 +00:00
public static styles = [
cssManager . defaultStyles ,
css `
:host {
display: block;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
}
.header-title {
font-size: 20px;
font-weight: 600;
color: ${ cssManager . bdTheme ( '#18181b' , '#fafafa' ) } ;
}
.header-subtitle {
font-size: 14px;
color: ${ cssManager . bdTheme ( '#71717a' , '#a1a1aa' ) } ;
margin-top: 4px;
}
.section {
background: ${ cssManager . bdTheme ( '#ffffff' , '#09090b' ) } ;
border: 1px solid ${ cssManager . bdTheme ( '#e4e4e7' , '#27272a' ) } ;
border-radius: 8px;
padding: 20px;
margin-bottom: 16px;
}
.section-title {
font-size: 15px;
font-weight: 600;
color: ${ cssManager . bdTheme ( '#18181b' , '#fafafa' ) } ;
margin-bottom: 16px;
display: flex;
align-items: center;
gap: 8px;
}
.section-title svg {
width: 18px;
height: 18px;
color: ${ cssManager . bdTheme ( '#71717a' , '#a1a1aa' ) } ;
}
.form-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
margin-bottom: 16px;
}
.form-row.single {
grid-template-columns: 1fr;
}
.form-group {
display: flex;
flex-direction: column;
gap: 6px;
}
.form-label {
font-size: 13px;
font-weight: 500;
color: ${ cssManager . bdTheme ( '#18181b' , '#fafafa' ) } ;
}
.form-label .required {
color: ${ cssManager . bdTheme ( '#ef4444' , '#f87171' ) } ;
}
.form-hint {
font-size: 12px;
color: ${ cssManager . bdTheme ( '#71717a' , '#a1a1aa' ) } ;
}
.form-input,
.form-select {
width: 100%;
padding: 10px 12px;
background: ${ cssManager . bdTheme ( '#ffffff' , '#18181b' ) } ;
border: 1px solid ${ cssManager . bdTheme ( '#e4e4e7' , '#27272a' ) } ;
border-radius: 6px;
font-size: 14px;
color: ${ cssManager . bdTheme ( '#18181b' , '#fafafa' ) } ;
outline: none;
transition: border-color 200ms ease;
box-sizing: border-box;
}
.form-input:focus,
.form-select:focus {
border-color: ${ cssManager . bdTheme ( '#3b82f6' , '#60a5fa' ) } ;
}
.form-input::placeholder {
color: ${ cssManager . bdTheme ( '#a1a1aa' , '#52525b' ) } ;
}
.form-select {
cursor: pointer;
}
.dynamic-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.dynamic-row {
display: flex;
gap: 8px;
align-items: flex-start;
}
.dynamic-row .form-input {
flex: 1;
}
.dynamic-row .form-select {
width: 80px;
flex-shrink: 0;
}
.remove-button {
padding: 10px;
background: transparent;
border: 1px solid ${ cssManager . bdTheme ( '#e4e4e7' , '#27272a' ) } ;
border-radius: 6px;
color: ${ cssManager . bdTheme ( '#71717a' , '#a1a1aa' ) } ;
cursor: pointer;
transition: all 200ms ease;
flex-shrink: 0;
}
.remove-button:hover {
background: ${ cssManager . bdTheme ( '#fee2e2' , 'rgba(239, 68, 68, 0.2)' ) } ;
border-color: ${ cssManager . bdTheme ( '#fecaca' , 'rgba(239, 68, 68, 0.3)' ) } ;
color: ${ cssManager . bdTheme ( '#dc2626' , '#ef4444' ) } ;
}
.add-button {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 8px 12px;
background: transparent;
border: 1px dashed ${ cssManager . bdTheme ( '#e4e4e7' , '#27272a' ) } ;
border-radius: 6px;
font-size: 13px;
color: ${ cssManager . bdTheme ( '#71717a' , '#a1a1aa' ) } ;
cursor: pointer;
transition: all 200ms ease;
margin-top: 8px;
}
.add-button:hover {
background: ${ cssManager . bdTheme ( '#f4f4f5' , '#18181b' ) } ;
border-color: ${ cssManager . bdTheme ( '#a1a1aa' , '#52525b' ) } ;
color: ${ cssManager . bdTheme ( '#18181b' , '#fafafa' ) } ;
}
.add-button svg {
width: 14px;
height: 14px;
}
.toggle-advanced {
display: flex;
align-items: center;
gap: 8px;
padding: 12px 0;
font-size: 14px;
font-weight: 500;
color: ${ cssManager . bdTheme ( '#3b82f6' , '#60a5fa' ) } ;
cursor: pointer;
background: none;
border: none;
}
.toggle-advanced svg {
width: 16px;
height: 16px;
transition: transform 200ms ease;
}
.toggle-advanced.open svg {
transform: rotate(180deg);
}
.checkbox-row {
display: flex;
align-items: center;
gap: 8px;
}
.checkbox {
width: 18px;
height: 18px;
accent-color: ${ cssManager . bdTheme ( '#3b82f6' , '#60a5fa' ) } ;
}
2026-03-16 11:44:44 +00:00
.platform-toggle-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.platform-toggle-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 16px;
background: ${ cssManager . bdTheme ( '#f4f4f5' , '#18181b' ) } ;
border-radius: 8px;
transition: background 200ms ease;
}
.platform-toggle-item:has(input:checked) {
background: ${ cssManager . bdTheme ( '#eff6ff' , 'rgba(59, 130, 246, 0.1)' ) } ;
}
.platform-toggle-info {
display: flex;
align-items: center;
gap: 12px;
}
.platform-toggle-icon {
width: 36px;
height: 36px;
border-radius: 8px;
background: ${ cssManager . bdTheme ( '#ffffff' , '#27272a' ) } ;
display: flex;
align-items: center;
justify-content: center;
}
.platform-toggle-icon svg {
width: 20px;
height: 20px;
color: ${ cssManager . bdTheme ( '#18181b' , '#fafafa' ) } ;
}
.platform-toggle-name {
font-size: 14px;
font-weight: 500;
color: ${ cssManager . bdTheme ( '#18181b' , '#fafafa' ) } ;
}
.platform-toggle-desc {
font-size: 12px;
color: ${ cssManager . bdTheme ( '#71717a' , '#a1a1aa' ) } ;
margin-top: 2px;
}
.toggle-switch {
position: relative;
width: 44px;
height: 24px;
flex-shrink: 0;
}
.toggle-switch input {
opacity: 0;
width: 0;
height: 0;
}
.toggle-slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: ${ cssManager . bdTheme ( '#d4d4d8' , '#3f3f46' ) } ;
border-radius: 12px;
transition: background 200ms ease;
}
.toggle-slider::before {
content: '';
position: absolute;
height: 18px;
width: 18px;
left: 3px;
bottom: 3px;
background: white;
border-radius: 50%;
transition: transform 200ms ease;
}
.toggle-switch input:checked + .toggle-slider {
background: ${ cssManager . bdTheme ( '#3b82f6' , '#60a5fa' ) } ;
}
.toggle-switch input:checked + .toggle-slider::before {
transform: translateX(20px);
}
2026-01-03 02:44:25 +00:00
.actions {
display: flex;
justify-content: flex-end;
gap: 12px;
padding-top: 16px;
border-top: 1px solid ${ cssManager . bdTheme ( '#e4e4e7' , '#27272a' ) } ;
margin-top: 8px;
}
.button {
padding: 10px 20px;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 200ms ease;
display: inline-flex;
align-items: center;
gap: 8px;
}
.button.secondary {
background: ${ cssManager . bdTheme ( '#ffffff' , '#09090b' ) } ;
border: 1px solid ${ cssManager . bdTheme ( '#e4e4e7' , '#27272a' ) } ;
color: ${ cssManager . bdTheme ( '#18181b' , '#fafafa' ) } ;
}
.button.secondary:hover {
background: ${ cssManager . bdTheme ( '#f4f4f5' , '#18181b' ) } ;
}
.button.primary {
background: ${ cssManager . bdTheme ( '#18181b' , '#fafafa' ) } ;
border: none;
color: ${ cssManager . bdTheme ( '#fafafa' , '#18181b' ) } ;
}
.button.primary:hover:not(:disabled) {
opacity: 0.9;
}
.button.primary:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.spinner {
width: 16px;
height: 16px;
border: 2px solid transparent;
border-top-color: currentColor;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
` ,
] ;
public render ( ) : TemplateResult {
return html `
<div class="header">
<div>
<div class="header-title">Deploy New Service</div>
<div class="header-subtitle">Configure and deploy a new Docker container</div>
</div>
</div>
<!-- Basic Info Section -->
<div class="section">
<div class="section-title">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
<line x1="9" y1="9" x2="15" y2="9"></line>
<line x1="9" y1="15" x2="15" y2="15"></line>
</svg>
Basic Information
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">Service Name <span class="required">*</span></label>
<input
type="text"
class="form-input"
placeholder="my-service"
.value= ${ this . serviceName }
@input= ${ ( e : Event ) = > this . serviceName = ( e . target as HTMLInputElement ) . value }
>
<div class="form-hint">Unique name for the service (alphanumeric and hyphens)</div>
</div>
<div class="form-group">
<label class="form-label">Registry</label>
<select
class="form-select"
.value= ${ this . selectedRegistry }
@change= ${ ( e : Event ) = > this . selectedRegistry = ( e . target as HTMLSelectElement ) . value }
>
<option value="">Custom Image URL</option>
${ this . registries . map ( reg = > html `
<option value= ${ reg . id } > ${ reg . name } </option>
` ) }
</select>
</div>
</div>
<div class="form-row single">
<div class="form-group">
<label class="form-label">Image <span class="required">*</span></label>
<input
type="text"
class="form-input"
placeholder="nginx:latest or registry.example.com/image:tag"
.value= ${ this . imageUrl }
@input= ${ ( e : Event ) = > this . imageUrl = ( e . target as HTMLInputElement ) . value }
>
<div class="form-hint">Docker image to deploy (include tag)</div>
</div>
</div>
</div>
<!-- Port Configuration -->
<div class="section">
<div class="section-title">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="10"></circle>
<line x1="2" y1="12" x2="22" y2="12"></line>
<path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"></path>
</svg>
Port Configuration
</div>
<div class="dynamic-list">
${ this . ports . map ( ( port , index ) = > html `
<div class="dynamic-row">
<input
type="text"
class="form-input"
placeholder="Host Port"
.value= ${ port . hostPort }
@input= ${ ( e : Event ) = > this . updatePort ( index , 'hostPort' , ( e . target as HTMLInputElement ) . value ) }
>
<input
type="text"
class="form-input"
placeholder="Container Port"
.value= ${ port . containerPort }
@input= ${ ( e : Event ) = > this . updatePort ( index , 'containerPort' , ( e . target as HTMLInputElement ) . value ) }
>
<select
class="form-select"
.value= ${ port . protocol }
@change= ${ ( e : Event ) = > this . updatePort ( index , 'protocol' , ( e . target as HTMLSelectElement ) . value ) }
>
<option value="tcp">TCP</option>
<option value="udp">UDP</option>
</select>
${ this . ports . length > 1 ? html `
<button class="remove-button" @click= ${ ( ) = > this . removePort ( index ) } >
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
` : '' }
</div>
` ) }
</div>
<button class="add-button" @click= ${ ( ) = > this . addPort ( ) } >
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="12" y1="5" x2="12" y2="19"></line>
<line x1="5" y1="12" x2="19" y2="12"></line>
</svg>
Add Port Mapping
</button>
</div>
<!-- Environment Variables -->
<div class="section">
<div class="section-title">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="4 17 10 11 4 5"></polyline>
<line x1="12" y1="19" x2="20" y2="19"></line>
</svg>
Environment Variables
</div>
<div class="dynamic-list">
${ this . envVars . map ( ( env , index ) = > html `
<div class="dynamic-row">
<input
type="text"
class="form-input"
placeholder="KEY"
.value= ${ env . key }
@input= ${ ( e : Event ) = > this . updateEnvVar ( index , 'key' , ( e . target as HTMLInputElement ) . value ) }
>
<input
type="text"
class="form-input"
placeholder="value"
.value= ${ env . value }
@input= ${ ( e : Event ) = > this . updateEnvVar ( index , 'value' , ( e . target as HTMLInputElement ) . value ) }
>
${ this . envVars . length > 1 ? html `
<button class="remove-button" @click= ${ ( ) = > this . removeEnvVar ( index ) } >
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
` : '' }
</div>
` ) }
</div>
<button class="add-button" @click= ${ ( ) = > this . addEnvVar ( ) } >
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="12" y1="5" x2="12" y2="19"></line>
<line x1="5" y1="12" x2="19" y2="12"></line>
</svg>
Add Environment Variable
</button>
</div>
2026-03-16 11:44:44 +00:00
<!-- Platform Services -->
<div class="section">
<div class="section-title">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="2" y="2" width="20" height="8" rx="2" ry="2"></rect>
<rect x="2" y="14" width="20" height="8" rx="2" ry="2"></rect>
<line x1="6" y1="6" x2="6.01" y2="6"></line>
<line x1="6" y1="18" x2="6.01" y2="18"></line>
</svg>
Platform Services
</div>
<div class="form-hint" style="margin-bottom: 12px;">
Enable managed infrastructure services for this deployment. Resources are automatically provisioned and connection details injected as environment variables.
</div>
<div class="platform-toggle-list">
<label class="platform-toggle-item">
<div class="platform-toggle-info">
<div class="platform-toggle-icon">
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 17.93c-3.95-.49-7-3.85-7-7.93 0-.62.08-1.21.21-1.79L9 15v1c0 1.1.9 2 2 2v1.93zm6.9-2.54c-.26-.81-1-1.39-1.9-1.39h-1v-3c0-.55-.45-1-1-1H8v-2h2c.55 0 1-.45 1-1V7h2c1.1 0 2-.9 2-2v-.41c2.93 1.19 5 4.06 5 7.41 0 2.08-.8 3.97-2.1 5.39z"/></svg>
</div>
<div>
<div class="platform-toggle-name">MongoDB</div>
<div class="platform-toggle-desc">Document database with auto-provisioned credentials</div>
</div>
</div>
<div class="toggle-switch">
<input
type="checkbox"
?checked= ${ this . enableMongoDB }
@change= ${ ( e : Event ) = > this . enableMongoDB = ( e . target as HTMLInputElement ) . checked }
>
<span class="toggle-slider"></span>
</div>
</label>
<label class="platform-toggle-item">
<div class="platform-toggle-info">
<div class="platform-toggle-icon">
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M21 16.5c0 .38-.21.71-.53.88l-7.9 4.44c-.16.12-.36.18-.57.18-.21 0-.41-.06-.57-.18l-7.9-4.44A.991.991 0 0 1 3 16.5v-9c0-.38.21-.71.53-.88l7.9-4.44c.16-.12.36-.18.57-.18.21 0 .41.06.57.18l7.9 4.44c.32.17.53.5.53.88v9z"/></svg>
</div>
<div>
<div class="platform-toggle-name">S3 Storage (MinIO)</div>
<div class="platform-toggle-desc">Object storage bucket with auto-provisioned access keys</div>
</div>
</div>
<div class="toggle-switch">
<input
type="checkbox"
?checked= ${ this . enableS3 }
@change= ${ ( e : Event ) = > this . enableS3 = ( e . target as HTMLInputElement ) . checked }
>
<span class="toggle-slider"></span>
</div>
</label>
<label class="platform-toggle-item">
<div class="platform-toggle-info">
<div class="platform-toggle-icon">
<svg viewBox="0 0 24 24" fill="currentColor"><rect x="2" y="2" width="6" height="20"/><rect x="9" y="7" width="6" height="15"/><rect x="16" y="12" width="6" height="10"/></svg>
</div>
<div>
<div class="platform-toggle-name">ClickHouse</div>
<div class="platform-toggle-desc">Analytics database with auto-provisioned credentials</div>
</div>
</div>
<div class="toggle-switch">
<input
type="checkbox"
?checked= ${ this . enableClickHouse }
@change= ${ ( e : Event ) = > this . enableClickHouse = ( e . target as HTMLInputElement ) . checked }
>
<span class="toggle-slider"></span>
</div>
</label>
</div>
</div>
2026-01-03 02:44:25 +00:00
<!-- Advanced Options Toggle -->
<button
class="toggle-advanced ${ this . showAdvanced ? 'open' : '' } "
@click= ${ ( ) = > this . showAdvanced = ! this . showAdvanced }
>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="6 9 12 15 18 9"></polyline>
</svg>
Advanced Options
</button>
${ this . showAdvanced ? html `
<!-- Volumes -->
<div class="section">
<div class="section-title">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path>
</svg>
Volume Mounts
</div>
<div class="dynamic-list">
${ this . volumes . length === 0 ? html `
<div class="form-hint">No volumes configured</div>
` : this . volumes . map ( ( vol , index ) = > html `
<div class="dynamic-row">
<input
type="text"
class="form-input"
placeholder="/host/path"
.value= ${ vol . hostPath }
@input= ${ ( e : Event ) = > this . updateVolume ( index , 'hostPath' , ( e . target as HTMLInputElement ) . value ) }
>
<input
type="text"
class="form-input"
placeholder="/container/path"
.value= ${ vol . containerPath }
@input= ${ ( e : Event ) = > this . updateVolume ( index , 'containerPath' , ( e . target as HTMLInputElement ) . value ) }
>
<div class="checkbox-row">
<input
type="checkbox"
class="checkbox"
?checked= ${ vol . readOnly }
@change= ${ ( e : Event ) = > this . updateVolume ( index , 'readOnly' , ( e . target as HTMLInputElement ) . checked ) }
>
<span class="form-hint">RO</span>
</div>
<button class="remove-button" @click= ${ ( ) = > this . removeVolume ( index ) } >
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
</div>
` ) }
</div>
<button class="add-button" @click= ${ ( ) = > this . addVolume ( ) } >
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="12" y1="5" x2="12" y2="19"></line>
<line x1="5" y1="12" x2="19" y2="12"></line>
</svg>
Add Volume Mount
</button>
</div>
<!-- Resource Limits -->
<div class="section">
<div class="section-title">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="1" y="4" width="22" height="16" rx="2" ry="2"></rect>
<line x1="1" y1="10" x2="23" y2="10"></line>
</svg>
Resource Limits
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">CPU Limit</label>
<input
type="text"
class="form-input"
placeholder="e.g., 1.0 or 0.5"
.value= ${ this . cpuLimit }
@input= ${ ( e : Event ) = > this . cpuLimit = ( e . target as HTMLInputElement ) . value }
>
<div class="form-hint">Number of CPUs (leave empty for unlimited)</div>
</div>
<div class="form-group">
<label class="form-label">Memory Limit</label>
<input
type="text"
class="form-input"
placeholder="e.g., 512m or 1g"
.value= ${ this . memoryLimit }
@input= ${ ( e : Event ) = > this . memoryLimit = ( e . target as HTMLInputElement ) . value }
>
<div class="form-hint">Memory limit (leave empty for unlimited)</div>
</div>
</div>
</div>
<!-- Restart Policy & Network -->
<div class="section">
<div class="section-title">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="3"></circle>
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path>
</svg>
Container Settings
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">Restart Policy</label>
<select
class="form-select"
.value= ${ this . restartPolicy }
@change= ${ ( e : Event ) = > this . restartPolicy = ( e . target as HTMLSelectElement ) . value as any }
>
<option value="always">Always</option>
<option value="on-failure">On Failure</option>
<option value="never">Never</option>
</select>
</div>
<div class="form-group">
<label class="form-label">Network Mode</label>
<select
class="form-select"
.value= ${ this . networkMode }
@change= ${ ( e : Event ) = > this . networkMode = ( e . target as HTMLSelectElement ) . value }
>
<option value="bridge">Bridge</option>
<option value="host">Host</option>
<option value="none">None</option>
</select>
</div>
</div>
</div>
` : '' }
<div class="actions">
<button class="button secondary" @click= ${ ( ) = > this . handleCancel ( ) } >Cancel</button>
<button
class="button primary"
?disabled= ${ this . loading || ! this . isValid ( ) }
@click= ${ ( ) = > this . handleCreate ( ) }
>
${ this . loading ? html ` <div class="spinner"></div> ` : '' }
${ this . loading ? 'Deploying...' : 'Deploy Service' }
</button>
</div>
` ;
}
private isValid ( ) : boolean {
return this . serviceName . trim ( ) !== '' && this . imageUrl . trim ( ) !== '' ;
}
private addPort() {
this . ports = [ . . . this . ports , { hostPort : '' , containerPort : '' , protocol : 'tcp' } ] ;
}
private removePort ( index : number ) {
this . ports = this . ports . filter ( ( _ , i ) = > i !== index ) ;
}
private updatePort ( index : number , field : keyof IPortMapping , value : string ) {
const newPorts = [ . . . this . ports ] ;
( newPorts [ index ] as any ) [ field ] = value ;
this . ports = newPorts ;
}
private addEnvVar() {
this . envVars = [ . . . this . envVars , { key : '' , value : '' } ] ;
}
private removeEnvVar ( index : number ) {
this . envVars = this . envVars . filter ( ( _ , i ) = > i !== index ) ;
}
private updateEnvVar ( index : number , field : keyof IEnvVar , value : string ) {
const newEnvVars = [ . . . this . envVars ] ;
newEnvVars [ index ] [ field ] = value ;
this . envVars = newEnvVars ;
}
private addVolume() {
this . volumes = [ . . . this . volumes , { hostPath : '' , containerPath : '' , readOnly : false } ] ;
}
private removeVolume ( index : number ) {
this . volumes = this . volumes . filter ( ( _ , i ) = > i !== index ) ;
}
private updateVolume ( index : number , field : keyof IVolumeMount , value : string | boolean ) {
const newVolumes = [ . . . this . volumes ] ;
( newVolumes [ index ] as any ) [ field ] = value ;
this . volumes = newVolumes ;
}
private handleCancel() {
this . dispatchEvent ( new CustomEvent ( 'cancel' , { bubbles : true , composed : true } ) ) ;
}
private handleCreate() {
const config : IServiceConfig = {
name : this.serviceName.trim ( ) ,
image : this.imageUrl.trim ( ) ,
ports : this.ports.filter ( p = > p . hostPort && p . containerPort ) ,
envVars : this.envVars.filter ( e = > e . key ) ,
volumes : this.volumes.filter ( v = > v . hostPath && v . containerPath ) ,
cpuLimit : this.cpuLimit ,
memoryLimit : this.memoryLimit ,
restartPolicy : this.restartPolicy ,
networkMode : this.networkMode ,
2026-03-16 11:44:44 +00:00
enableMongoDB : this.enableMongoDB ,
enableS3 : this.enableS3 ,
enableClickHouse : this.enableClickHouse ,
2026-01-03 02:44:25 +00:00
} ;
this . dispatchEvent ( new CustomEvent ( 'create-service' , {
detail : config ,
bubbles : true ,
composed : true ,
} ) ) ;
}
public reset() {
this . serviceName = '' ;
this . imageUrl = '' ;
this . selectedRegistry = '' ;
this . ports = [ { hostPort : '' , containerPort : '' , protocol : 'tcp' } ] ;
this . envVars = [ { key : '' , value : '' } ] ;
this . volumes = [ ] ;
this . cpuLimit = '' ;
this . memoryLimit = '' ;
this . restartPolicy = 'always' ;
this . networkMode = 'bridge' ;
this . showAdvanced = false ;
2026-03-16 11:44:44 +00:00
this . enableMongoDB = false ;
this . enableS3 = false ;
this . enableClickHouse = false ;
2026-01-03 02:44:25 +00:00
}
}