776 lines
24 KiB
TypeScript
776 lines
24 KiB
TypeScript
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`
|
|
<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>
|
|
`;
|
|
|
|
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`
|
|
<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>
|
|
|
|
<!-- 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,
|
|
};
|
|
|
|
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;
|
|
}
|
|
}
|