{#if stacks.length > 0} {/if}
e.key === 'Escape' && (searchInput = '')} class="pl-8 h-8 w-48 text-sm" />
{#if $canAccess('stacks', 'create')} {/if}
{#if selectedStacks.size > 0}
{selectedInFilter.length} selected {#if selectedStopped.length > 0 && $canAccess('stacks', 'start')} confirmBulkStart = open} > {#snippet children({ open })} Start {/snippet} {/if} {#if selectedRunning.length > 0 && $canAccess('stacks', 'restart')} confirmBulkRestart = open} > {#snippet children({ open })} Restart {/snippet} {/if} {#if selectedRunning.length > 0 && $canAccess('stacks', 'stop')} confirmBulkStop = open} > {#snippet children({ open })} Stop {/snippet} {/if} {#if selectedRunning.length > 0 && $canAccess('stacks', 'stop')} confirmBulkDown = open} > {#snippet children({ open })} Down {/snippet} {/if} {#if $canAccess('stacks', 'remove')} confirmBulkRemove = open} > {#snippet children({ open })} Remove {/snippet} {/if}
{/if} {#if !loading && ($environments.length === 0 || !$currentEnvironment)} {:else if !loading && stacks.length === 0} {:else} saveExpandedState()} sortState={{ field: sortField, direction: sortDirection }} onSortChange={(state) => { sortField = state.field as SortField; sortDirection = state.direction; }} onRowClick={(stack, e) => { const hasContainers = stack.containers && stack.containers.length > 0; if (hasContainers) { toggleExpand(stack.name); } }} rowClass={(stack) => { const isExp = expandedStacks.has(stack.name); const isSel = selectedStacks.has(stack.name); return `${isExp ? 'bg-muted/40' : ''} ${isSel ? 'bg-muted/30' : ''}`; }} > {#snippet cell(column, stack, rowState)} {@const source = getStackSource(stack.name)} {#if column.id === 'name'} {stack.name} {#if stackEnvVarCounts[stack.name]} {stackEnvVarCounts[stack.name]} {stackEnvVarCounts[stack.name]} environment variable{stackEnvVarCounts[stack.name] !== 1 ? 's' : ''} configured {/if} {:else if column.id === 'source'} {#if source.sourceType === 'git'} Git {#if source.repository} {source.repository.url} ({source.repository.branch}) {:else} Deployed from Git repository {/if} {:else if source.sourceType === 'internal'} Internal Created in Dockhand {:else} External Created outside Dockhand {/if} {:else if column.id === 'containers'}
{#if getContainerStateCounts(stack).running} {getContainerStateCounts(stack).running} {/if} {#if getContainerStateCounts(stack).exited} {getContainerStateCounts(stack).exited} {/if} {#if getContainerStateCounts(stack).paused} {getContainerStateCounts(stack).paused} {/if} {#if getContainerStateCounts(stack).restarting} {getContainerStateCounts(stack).restarting} {/if} {#if getContainerStateCounts(stack).created} {getContainerStateCounts(stack).created} {/if} {#if getContainerStateCounts(stack).dead} {getContainerStateCounts(stack).dead} {/if} {#if stack.containers.length === 0} - {/if}
{:else if column.id === 'cpu'} {@const stats = getStackStats(stack)}
{#if stats} {stats.cpuPercent.toFixed(1)}% {:else if stack.status === 'running' || stack.status === 'partial'} ... {:else} - {/if}
{:else if column.id === 'memory'} {@const stats = getStackStats(stack)}
{#if stats} {formatBytes(stats.memoryUsage)} {:else if stack.status === 'running' || stack.status === 'partial'} ... {:else} - {/if}
{:else if column.id === 'networkIO'} {@const stats = getStackStats(stack)}
{#if stats} {formatBytes(stats.networkRx, 0)} {formatBytes(stats.networkTx, 0)} {:else if stack.status === 'running' || stack.status === 'partial'} ... {:else} - {/if}
{:else if column.id === 'diskIO'} {@const stats = getStackStats(stack)}
{#if stats} r{formatBytes(stats.blockRead, 0)} w{formatBytes(stats.blockWrite, 0)} {:else if stack.status === 'running' || stack.status === 'partial'} ... {:else} - {/if}
{:else if column.id === 'networks'} {getStackNetworkCount(stack) || '-'} {:else if column.id === 'volumes'} {getStackVolumeCount(stack) || '-'} {:else if column.id === 'status'} {@const StatusIcon = getStackStatusIcon(stack.status)} {stack.status} {:else if column.id === 'actions'}
{#if operationError?.id === stack.name && operationError?.message}
{operationError.message}
{/if} {#if stack.status === 'not deployed' && source.gitStack} {#snippet children()} {/snippet} {:else} {#if source.sourceType === 'git' && source.gitStack} {#snippet children()} {/snippet} {/if} {#if $canAccess('stacks', 'edit')} {#if source.sourceType === 'internal'} {:else if source.sourceType === 'git' && source.gitStack} {/if} {/if} {#if stack.containers && stack.containers.length > 0} {/if} {#if stackActionLoading === stack.name}
{:else if stack.status === 'running' || stack.status === 'partial'} {#if $canAccess('stacks', 'restart')} restartStack(stack.name)} onOpenChange={() => {}} > {#snippet children({ open })} {/snippet} {/if} {#if $canAccess('stacks', 'stop')} stopStack(stack.name)} onOpenChange={(open) => confirmStopName = open ? stack.name : null} > {#snippet children({ open })} {/snippet} {/if} {#if $canAccess('stacks', 'stop')} downStack(stack.name)} onOpenChange={(open) => confirmDownName = open ? stack.name : null} > {#snippet children({ open })} {/snippet} {/if} {:else} {#if $canAccess('stacks', 'start')} {/if} {/if} {/if} {#if $canAccess('stacks', 'remove')} removeStack(stack.name)} onOpenChange={(open) => confirmDeleteName = open ? stack.name : null} > {#snippet children({ open })} {/snippet} {/if}
{/if} {/snippet} {#snippet expandedRow(stack, rowState)} {#if stack.containerDetails?.length > 0}
{#each stack.containerDetails as container (container.id)} {@const isLoading = containerActionLoading === container.id}
{container.service} {#if container.health} {#if container.health === 'healthy'} {:else if container.health === 'unhealthy'} {:else} {/if} {/if} {container.state}
{container.image}
{formatUptime(container.status)} {#if container.restartCount > 0} {container.restartCount} {/if}
{#if container.state === 'running'} {@const stats = containerStats.get(container.id)} {@const history = containerStatsHistory.get(container.id)} {#key statsUpdateCount}
CPU {stats?.cpuPercent?.toFixed(0) ?? '-'}%
{#if history?.cpu && history.cpu.length >= 2} {:else}
{/if}
Mem {stats ? formatBytes(stats.memoryUsage) : '-'}
{#if history?.mem && history.mem.length >= 2} {:else}
{/if}
Net {stats ? formatBytes(stats.networkRx + stats.networkTx) : '-'}
{#if history?.netRx && history.netRx.length >= 2} rx + (history.netTx[i] || 0)), 60, 16)} fill="rgba(34, 197, 94, 0.15)" /> rx + (history.netTx[i] || 0)), 60, 16)} fill="none" stroke="rgb(34, 197, 94)" stroke-width="1" /> {:else}
{/if}
Disk {stats ? formatBytes(stats.blockRead + stats.blockWrite) : '-'}
{#if history?.diskR && history.diskR.length >= 2} r + (history.diskW[i] || 0)), 60, 16)} fill="rgba(251, 146, 60, 0.15)" /> r + (history.diskW[i] || 0)), 60, 16)} fill="none" stroke="rgb(251, 146, 60)" stroke-width="1" /> {:else}
{/if}
{/key} {/if}
{#if container.ports.length > 0} {@const uniquePorts = container.ports.filter((p, i, arr) => p.publicPort && arr.findIndex(x => x.publicPort === p.publicPort) === i)} {#each uniquePorts.slice(0, 2) as port} {@const url = getPortUrl(port.publicPort)} {#if url} e.stopPropagation()} class="inline-flex items-center gap-0.5 px-1.5 py-0.5 rounded bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200 hover:bg-blue-200 dark:hover:bg-blue-800 transition-colors" title="Open {url} in new tab" > :{port.publicPort} {:else} :{port.publicPort} {/if} {/each} {#if uniquePorts.length > 2} +{uniquePorts.length - 2} {/if} {/if} {#if container.networks.length > 0} {@const ip = getContainerIp(container.networks)} {ip !== '-' ? ip : container.networks.length} {#each container.networks as net}
{net.name}: {net.ipAddress || 'no IP'}
{/each}
{/if} {#if container.volumeCount > 0} {container.volumeCount} {/if}
{#if container.state === 'running' && $canAccess('containers', 'exec')} {/if} {#if container.state === 'running' && $canAccess('containers', 'files')} {/if}
{#if operationError?.id === container.id && operationError?.message}
{operationError.message}
{/if} {#if isLoading} {:else} {#if container.state === 'running'} {#if $canAccess('containers', 'restart')} restartContainer(container.id)} onOpenChange={(open) => confirmRestartContainerId = open ? container.id : null} > {#snippet children({ open })} {/snippet} {/if} {#if $canAccess('containers', 'pause')} pauseContainer(container.id)} onOpenChange={(open) => confirmPauseContainerId = open ? container.id : null} > {#snippet children({ open })} {/snippet} {/if} {#if $canAccess('containers', 'stop')} stopContainer(container.id)} onOpenChange={(open) => confirmStopContainerId = open ? container.id : null} > {#snippet children({ open })} {/snippet} {/if} {:else if container.state === 'paused'} {#if $canAccess('containers', 'unpause')} {/if} {:else} {#if $canAccess('containers', 'start')} {/if} {/if} {/if}
{/each}
{/if} {/snippet}
{/if}
showCreateModal = false} onSuccess={fetchStacks} /> { showEditModal = false; editingStackName = ''; }} onSuccess={fetchStacks} /> { showGitModal = false; editingGitStack = null; }} onSaved={fetchStacks} /> showFileBrowserModal = false} /> showBatchOpModal = false} onComplete={handleBatchComplete} />