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; } @customElement('sz-service-create-view') export class SzServiceCreateView extends DeesElement { public static demo = () => html`
`; public static demoGroups = ['Services']; @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; 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')}; } .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`
Deploy New Service
Configure and deploy a new Docker container
Basic Information
this.serviceName = (e.target as HTMLInputElement).value} >
Unique name for the service (alphanumeric and hyphens)
this.imageUrl = (e.target as HTMLInputElement).value} >
Docker image to deploy (include tag)
Port Configuration
${this.ports.map((port, index) => html`
this.updatePort(index, 'hostPort', (e.target as HTMLInputElement).value)} > this.updatePort(index, 'containerPort', (e.target as HTMLInputElement).value)} > ${this.ports.length > 1 ? html` ` : ''}
`)}
Environment Variables
${this.envVars.map((env, index) => html`
this.updateEnvVar(index, 'key', (e.target as HTMLInputElement).value)} > this.updateEnvVar(index, 'value', (e.target as HTMLInputElement).value)} > ${this.envVars.length > 1 ? html` ` : ''}
`)}
${this.showAdvanced ? html`
Volume Mounts
${this.volumes.length === 0 ? html`
No volumes configured
` : this.volumes.map((vol, index) => html`
this.updateVolume(index, 'hostPath', (e.target as HTMLInputElement).value)} > this.updateVolume(index, 'containerPath', (e.target as HTMLInputElement).value)} >
this.updateVolume(index, 'readOnly', (e.target as HTMLInputElement).checked)} > RO
`)}
Resource Limits
this.cpuLimit = (e.target as HTMLInputElement).value} >
Number of CPUs (leave empty for unlimited)
this.memoryLimit = (e.target as HTMLInputElement).value} >
Memory limit (leave empty for unlimited)
Container Settings
` : ''}
`; } 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, }; 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; } }