refactor(docker): remove deprecated guest URL edit functionality and align table consistency

This commit is contained in:
rcourtman 2026-02-03 22:40:12 +00:00
parent a3436fbde5
commit 5e0cdfc163
3 changed files with 32 additions and 387 deletions

View file

@ -1,7 +1,6 @@
import { Component, For, Show, createMemo, createSignal } from 'solid-js';
import type { DockerHost } from '@/types/api';
import { Card } from '@/components/shared/Card';
import { renderDockerStatusBadge } from './DockerStatusBadge';
import { resolveHostRuntime } from './runtimeDisplay';
import { formatUptime } from '@/utils/format';
import { ScrollableTable } from '@/components/shared/ScrollableTable';
@ -12,10 +11,7 @@ import { useBreakpoint } from '@/hooks/useBreakpoint';
import { ResponsiveMetricCell } from '@/components/shared/responsive';
import { EnhancedCPUBar } from '@/components/Dashboard/EnhancedCPUBar';
import { isAgentOutdated, getAgentVersionTooltip } from '@/utils/agentVersion';
import { DockerHostMetadataAPI, type DockerHostMetadata } from '@/api/dockerHostMetadata';
import { UrlEditPopover, createUrlEditState } from '@/components/shared/UrlEditPopover';
import { showSuccess, showError } from '@/utils/toast';
import { logger } from '@/utils/logger';
export interface DockerHostSummary {
host: DockerHost;
@ -38,8 +34,6 @@ interface DockerHostSummaryTableProps {
summaries: () => DockerHostSummary[];
selectedHostId: () => string | null;
onSelect: (hostId: string) => void;
dockerHostMetadata?: Record<string, DockerHostMetadata>;
onHostCustomUrlUpdate?: (hostId: string, url: string) => void;
}
type SortKey = 'name' | 'uptime' | 'cpu' | 'memory' | 'disk' | 'running' | 'lastSeen' | 'agent';
@ -60,9 +54,6 @@ export const DockerHostSummaryTable: Component<DockerHostSummaryTableProps> = (p
const [sortDirection, setSortDirection] = createSignal<SortDirection>('asc');
const { isMobile } = useBreakpoint();
// URL editing state using shared hook
const urlEdit = createUrlEditState();
const handleSort = (key: SortKey) => {
if (sortKey() === key) {
setSortDirection(sortDirection() === 'asc' ? 'desc' : 'asc');
@ -77,8 +68,6 @@ export const DockerHostSummaryTable: Component<DockerHostSummaryTableProps> = (p
return lastSeen;
};
const sortedSummaries = createMemo(() => {
const list = [...props.summaries()];
const key = sortKey();
@ -133,68 +122,6 @@ export const DockerHostSummaryTable: Component<DockerHostSummaryTableProps> = (p
return list;
});
// URL editing functions
const getHostCustomUrl = (hostId: string) => {
return props.dockerHostMetadata?.[hostId]?.customUrl;
};
const handleStartEditingUrl = (hostId: string, event: MouseEvent) => {
const currentUrl = getHostCustomUrl(hostId) || '';
urlEdit.startEditing(hostId, currentUrl, event);
};
const handleSaveUrl = async () => {
const hostId = urlEdit.editingId();
if (!hostId) return;
const newUrl = urlEdit.editingValue().trim();
urlEdit.setIsSaving(true);
try {
await DockerHostMetadataAPI.updateMetadata(hostId, { customUrl: newUrl });
if (props.onHostCustomUrlUpdate) {
props.onHostCustomUrlUpdate(hostId, newUrl);
}
if (newUrl) {
showSuccess('Host URL saved');
} else {
showSuccess('Host URL cleared');
}
urlEdit.finishEditing();
} catch (err: unknown) {
const message = err instanceof Error ? err.message : 'Failed to save host URL';
logger.error('Failed to save host URL:', err);
showError(message);
urlEdit.setIsSaving(false);
}
};
const handleDeleteUrl = async () => {
const hostId = urlEdit.editingId();
if (!hostId) return;
urlEdit.setIsSaving(true);
try {
await DockerHostMetadataAPI.updateMetadata(hostId, { customUrl: '' });
if (props.onHostCustomUrlUpdate) {
props.onHostCustomUrlUpdate(hostId, '');
}
showSuccess('Host URL removed');
urlEdit.finishEditing();
} catch (err: unknown) {
const message = err instanceof Error ? err.message : 'Failed to remove host URL';
logger.error('Failed to remove host URL:', err);
showError(message);
urlEdit.setIsSaving(false);
}
};
const renderSortIndicator = (key: SortKey) => {
if (sortKey() !== key) return null;
return sortDirection() === 'asc' ? '▲' : '▼';
@ -204,7 +131,7 @@ export const DockerHostSummaryTable: Component<DockerHostSummaryTableProps> = (p
return (
<>
<Card padding="none" tone="glass" class={`mb-4 ${urlEdit.isEditing() ? 'overflow-visible' : 'overflow-hidden'}`}>
<Card padding="none" tone="glass" class="mb-4 overflow-hidden">
<ScrollableTable persistKey="docker-host-summary" minWidth={isMobile() ? '100%' : '800px'}>
<table class="w-full border-collapse whitespace-nowrap" style={{ "table-layout": "fixed" }}>
<thead>
@ -221,10 +148,11 @@ export const DockerHostSummaryTable: Component<DockerHostSummaryTableProps> = (p
Host {renderSortIndicator('name')}
</th>
<th
class="px-2 py-1 text-center text-[11px] sm:text-xs font-medium uppercase tracking-wider whitespace-nowrap"
style={{ width: "70px" }}
class="px-2 py-1 text-center text-[11px] sm:text-xs font-medium uppercase tracking-wider cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600 whitespace-nowrap"
style={{ width: "80px" }}
onClick={() => handleSort('uptime')}
>
Status
Uptime {renderSortIndicator('uptime')}
</th>
<th
class="px-2 py-1 text-center text-[11px] sm:text-xs font-medium uppercase tracking-wider cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600 whitespace-nowrap"
@ -254,13 +182,6 @@ export const DockerHostSummaryTable: Component<DockerHostSummaryTableProps> = (p
>
Containers {renderSortIndicator('running')}
</th>
<th
class="px-2 py-1 text-center text-[11px] sm:text-xs font-medium uppercase tracking-wider cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600 whitespace-nowrap"
style={{ width: "80px" }}
onClick={() => handleSort('uptime')}
>
Uptime {renderSortIndicator('uptime')}
</th>
<th
class="px-2 py-1 text-center text-[11px] sm:text-xs font-medium uppercase tracking-wider cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600 whitespace-nowrap"
style={{ width: "80px" }}
@ -338,32 +259,6 @@ export const DockerHostSummaryTable: Component<DockerHostSummaryTableProps> = (p
<span class="font-medium text-[11px] text-gray-900 dark:text-gray-100 truncate" title={getDisplayName(summary.host)}>
{getDisplayName(summary.host)}
</span>
{/* URL icon and link */}
<Show when={getHostCustomUrl(summary.host.id)}>
<a
href={getHostCustomUrl(summary.host.id)}
target="_blank"
rel="noopener noreferrer"
class="flex-shrink-0 text-blue-500 hover:text-blue-600 dark:text-blue-400 dark:hover:text-blue-300 transition-colors"
title={`Open ${getHostCustomUrl(summary.host.id)}`}
onClick={(e) => e.stopPropagation()}
>
<svg class="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1" />
</svg>
</a>
</Show>
<button
type="button"
class="flex-shrink-0 p-0.5 rounded text-gray-400 hover:text-gray-600 dark:text-gray-500 dark:hover:text-gray-300 transition-colors opacity-0 group-hover:opacity-100 focus:opacity-100"
classList={{ 'opacity-100': urlEdit.editingId() === summary.host.id }}
title={getHostCustomUrl(summary.host.id) ? 'Edit URL' : 'Add URL'}
onClick={(e) => handleStartEditingUrl(summary.host.id, e)}
>
<svg class="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z" />
</svg>
</button>
<Show when={getDisplayName(summary.host) !== summary.host.hostname}>
<span class="hidden sm:inline text-[9px] text-gray-500 dark:text-gray-400 whitespace-nowrap">
({summary.host.hostname})
@ -384,9 +279,11 @@ export const DockerHostSummaryTable: Component<DockerHostSummaryTableProps> = (p
</div>
</div>
</td>
<td class="px-2 py-1 align-middle">
<div class="flex justify-center items-center h-full w-full whitespace-nowrap">
{renderDockerStatusBadge(summary.host.status)}
<td class="px-2 py-1 align-middle whitespace-nowrap">
<div class="flex justify-center items-center h-full">
<span class="text-xs text-gray-600 dark:text-gray-400">
{uptimeLabel}
</span>
</div>
</td>
<td class="px-2 py-1 align-middle" style={isMobile() ? { "min-width": "60px" } : { width: "140px", "min-width": "140px", "max-width": "140px" }}>
@ -435,13 +332,6 @@ export const DockerHostSummaryTable: Component<DockerHostSummaryTableProps> = (p
</Show>
</div>
</td>
<td class="px-2 py-1 align-middle whitespace-nowrap">
<div class="flex justify-center items-center h-full">
<span class="text-xs text-gray-600 dark:text-gray-400">
{uptimeLabel}
</span>
</div>
</td>
<td class="px-2 py-1 align-middle">
<div class="flex justify-center items-center h-full">
<Show
@ -493,21 +383,6 @@ export const DockerHostSummaryTable: Component<DockerHostSummaryTableProps> = (p
</table>
</ScrollableTable>
</Card>
{/* URL editing popover - using shared component */}
<UrlEditPopover
isOpen={urlEdit.isEditing()}
value={urlEdit.editingValue()}
position={urlEdit.position()}
isSaving={urlEdit.isSaving()}
hasExistingUrl={!!getHostCustomUrl(urlEdit.editingId() || '')}
placeholder="https://portainer.local:9000"
helpText="Add a URL to quickly access this host's management interface (e.g., Portainer)"
onValueChange={urlEdit.setEditingValue}
onSave={handleSaveUrl}
onCancel={urlEdit.cancelEditing}
onDelete={handleDeleteUrl}
/>
</>
);
};

View file

@ -14,17 +14,15 @@ import { useWebSocket } from '@/App';
import { useDebouncedValue } from '@/hooks/useDebouncedValue';
import { usePersistentSignal } from '@/hooks/usePersistentSignal';
import { formatBytes, formatRelativeTime } from '@/utils/format';
import { DockerMetadataAPI, type DockerMetadata } from '@/api/dockerMetadata';
import { DockerHostMetadataAPI, type DockerHostMetadata } from '@/api/dockerHostMetadata';
import { logger } from '@/utils/logger';
import { STORAGE_KEYS } from '@/utils/localStorage';
import { DEGRADED_HEALTH_STATUSES, OFFLINE_HEALTH_STATUSES } from '@/utils/status';
import { MonitoringAPI } from '@/api/monitoring';
import { showSuccess, showError, showToast } from '@/utils/toast';
import { isKioskMode, subscribeToKioskMode } from '@/utils/url';
type DockerMetadataRecord = Record<string, DockerMetadata>;
type DockerHostMetadataRecord = Record<string, DockerHostMetadata>;
interface DockerHostsProps {
hosts: DockerHost[];
@ -36,38 +34,7 @@ export const DockerHosts: Component<DockerHostsProps> = (props) => {
const { initialDataReceived, reconnecting, connected, reconnect } = useWebSocket();
// Load docker metadata from localStorage or API
const loadInitialDockerMetadata = (): DockerMetadataRecord => {
try {
const cached = localStorage.getItem(STORAGE_KEYS.DOCKER_METADATA);
if (cached) {
return JSON.parse(cached);
}
} catch (err) {
logger.warn('Failed to parse cached docker metadata', err);
}
return {};
};
// Load docker host metadata from localStorage
const loadInitialDockerHostMetadata = (): DockerHostMetadataRecord => {
try {
const cached = localStorage.getItem(STORAGE_KEYS.DOCKER_METADATA + '_hosts');
if (cached) {
return JSON.parse(cached);
}
} catch (err) {
logger.warn('Failed to parse cached docker host metadata', err);
}
return {};
};
const [dockerMetadata, setDockerMetadata] = createSignal<DockerMetadataRecord>(
loadInitialDockerMetadata(),
);
const [dockerHostMetadata, setDockerHostMetadata] = createSignal<DockerHostMetadataRecord>(
loadInitialDockerHostMetadata(),
);
const sortedHosts = createMemo(() => {
const hosts = props.hosts || [];
@ -236,97 +203,13 @@ export const DockerHosts: Component<DockerHostsProps> = (props) => {
onMount(() => {
document.addEventListener('keydown', handleKeyDown);
// Load docker metadata from API
DockerMetadataAPI.getAllMetadata()
.then((metadata) => {
setDockerMetadata(metadata || {});
try {
localStorage.setItem(STORAGE_KEYS.DOCKER_METADATA, JSON.stringify(metadata || {}));
} catch (err) {
logger.warn('Failed to cache docker metadata', err);
}
})
.catch((err) => {
logger.debug('Failed to load docker metadata', err);
});
// Load docker host metadata from API
DockerHostMetadataAPI.getAllMetadata()
.then((metadata) => {
setDockerHostMetadata(metadata || {});
try {
localStorage.setItem(STORAGE_KEYS.DOCKER_METADATA + '_hosts', JSON.stringify(metadata || {}));
} catch (err) {
logger.warn('Failed to cache docker host metadata', err);
}
})
.catch((err) => {
logger.debug('Failed to load docker host metadata', err);
});
});
onCleanup(() => document.removeEventListener('keydown', handleKeyDown));
// Handler to update docker host custom URL
const handleHostCustomUrlUpdate = (hostId: string, url: string) => {
const trimmedUrl = url.trim();
const nextUrl = trimmedUrl === '' ? undefined : trimmedUrl;
setDockerHostMetadata((prev) => {
const updated = { ...prev };
if (nextUrl === undefined) {
// Remove URL but keep other metadata fields
if (updated[hostId]) {
const { customUrl: _removed, ...rest } = updated[hostId];
if (Object.keys(rest).length === 0 || (Object.keys(rest).length === 1 && !rest.customDisplayName && !rest.notes?.length)) {
delete updated[hostId];
} else {
updated[hostId] = rest;
}
}
} else {
updated[hostId] = {
...(prev[hostId] || {}),
customUrl: nextUrl,
};
}
// Cache to localStorage
try {
localStorage.setItem(STORAGE_KEYS.DOCKER_METADATA + '_hosts', JSON.stringify(updated));
} catch (err) {
logger.warn('Failed to cache docker host metadata', err);
}
return updated;
});
};
// Handler to update docker resource custom URL
const handleCustomUrlUpdate = (resourceId: string, url: string) => {
const trimmedUrl = url.trim();
const nextUrl = trimmedUrl === '' ? undefined : trimmedUrl;
setDockerMetadata((prev) => {
const updated = { ...prev };
if (nextUrl === undefined) {
delete updated[resourceId];
} else {
updated[resourceId] = {
...(prev[resourceId] || { id: resourceId }),
customUrl: nextUrl,
};
}
// Cache to localStorage
try {
localStorage.setItem(STORAGE_KEYS.DOCKER_METADATA, JSON.stringify(updated));
} catch (err) {
logger.warn('Failed to cache docker metadata', err);
}
return updated;
});
};
createEffect(() => {
const hostId = selectedHostId();
@ -612,8 +495,7 @@ export const DockerHosts: Component<DockerHostsProps> = (props) => {
summaries={filteredHostSummaries}
selectedHostId={selectedHostId}
onSelect={handleHostSelect}
dockerHostMetadata={dockerHostMetadata()}
onHostCustomUrlUpdate={handleHostCustomUrlUpdate}
/>
</Show>
@ -627,9 +509,6 @@ export const DockerHosts: Component<DockerHostsProps> = (props) => {
searchTerm={debouncedSearch()}
statsFilter={statsFilter()}
selectedHostId={selectedHostId}
dockerMetadata={dockerMetadata()}
dockerHostMetadata={dockerHostMetadata()}
onCustomUrlUpdate={handleCustomUrlUpdate}
batchUpdateState={batchUpdateState}
groupingMode={groupingMode() === 'flat' ? 'flat' : 'grouped'}
/>

View file

@ -11,8 +11,6 @@ import {
getShortImageName,
formatAbsoluteTime,
} from '@/utils/format';
import type { DockerMetadata } from '@/api/dockerMetadata';
import type { DockerHostMetadata } from '@/api/dockerHostMetadata';
import { resolveHostRuntime } from './runtimeDisplay';
// ... (other imports remain) ...
@ -78,15 +76,11 @@ type DockerRow =
};
interface DockerUnifiedTableProps {
export interface DockerUnifiedTableProps {
hosts: DockerHost[];
searchTerm?: string;
statsFilter?: StatsFilter;
selectedHostId?: () => string | null;
dockerMetadata?: Record<string, DockerMetadata>;
dockerHostMetadata?: Record<string, DockerHostMetadata>;
onCustomUrlUpdate?: (resourceId: string, url: string) => void;
onHostCustomUrlUpdate?: (hostId: string, url: string) => void;
batchUpdateState?: Record<string, 'updating' | 'queued' | 'error'>;
groupingMode?: 'grouped' | 'flat';
}
@ -760,8 +754,6 @@ const UNGROUPED_RESOURCE_INDENT = 'pl-4 sm:pl-5 lg:pl-6';
const DockerHostGroupHeader: Component<{
host: DockerHost;
columnCount: number;
customUrl?: string;
onCustomUrlUpdate?: (hostId: string, url: string) => void;
}> = (props) => {
const displayName = getHostDisplayName(props.host);
const hostStatus = () => getDockerHostStatusIndicator(props.host);
@ -780,23 +772,7 @@ const DockerHostGroupHeader: Component<{
ariaLabel={hostStatus().label}
size="xs"
/>
<Show
when={props.customUrl}
fallback={<span>{displayName}</span>}
>
<a
href={props.customUrl}
target="_blank"
rel="noopener noreferrer"
class="inline-flex items-center gap-1 text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300 hover:underline"
title={`Open ${props.customUrl}`}
>
<span>{displayName}</span>
<svg class="w-3.5 h-3.5 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
</svg>
</a>
</Show>
<span>{displayName}</span>
<span class="text-xs font-normal text-slate-500 dark:text-slate-400 ml-1">
({props.host.hostname})
</span>
@ -809,8 +785,6 @@ const DockerHostGroupHeader: Component<{
const DockerContainerRow: Component<{
row: Extract<DockerRow, { kind: 'container' }>;
isMobile: Accessor<boolean>;
customUrl?: string;
onCustomUrlUpdate?: (resourceId: string, url: string) => void;
showHostContext?: boolean;
resourceIndentClass?: string;
batchUpdateState?: Record<string, 'updating' | 'queued' | 'error'>;
@ -824,10 +798,8 @@ const DockerContainerRow: Component<{
const resourceIndent = () => props.resourceIndentClass ?? GROUPED_RESOURCE_INDENT;
const [customUrl, setCustomUrl] = createSignal<string | undefined>(props.customUrl);
const [activeTab, setActiveTab] = createSignal<'overview' | 'discovery'>('overview');
const [historyRange, setHistoryRange] = createSignal<HistoryTimeRange>('1h');
const [shouldAnimateIcon, setShouldAnimateIcon] = createSignal(false);
const expanded = createMemo(() => currentlyExpandedRowId() === rowId);
@ -909,19 +881,7 @@ const DockerContainerRow: Component<{
);
});
// Update custom URL when prop changes
createEffect(() => {
const prevUrl = customUrl();
const newUrl = props.customUrl;
// Only animate when URL transitions from empty to having a value
if (!prevUrl && newUrl) {
setShouldAnimateIcon(true);
setTimeout(() => setShouldAnimateIcon(false), 200);
}
setCustomUrl(newUrl);
});
// Auto-focus the input when editing starts
@ -1017,21 +977,6 @@ const DockerContainerRow: Component<{
</span>
)}
</Show>
<Show when={customUrl()}>
<a
href={customUrl()}
target="_blank"
rel="noopener noreferrer"
class={`flex-shrink-0 text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 transition-colors ${shouldAnimateIcon() ? 'animate-fadeIn' : ''}`}
title="Open in new tab"
onClick={(event) => event.stopPropagation()}
>
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
</svg>
</a>
</Show>
{/* Edit URL button - shows on hover */}
<Show when={props.showHostContext}>
<span
@ -1045,7 +990,7 @@ const DockerContainerRow: Component<{
</div>
</div>
</div>
</div>
</div >
);
case 'type':
return (
@ -1645,11 +1590,6 @@ const DockerContainerRow: Component<{
resourceId={container.id}
hostname={container.name}
guestId={container.id}
customUrl={customUrl()}
onCustomUrlChange={(url) => {
setCustomUrl(url || undefined);
props.onCustomUrlUpdate?.(container.id, url);
}}
/>
</Show>
</div>
@ -1663,7 +1603,6 @@ const DockerContainerRow: Component<{
const DockerServiceRow: Component<{
row: Extract<DockerRow, { kind: 'service' }>;
isMobile: Accessor<boolean>;
customUrl?: string;
onCustomUrlUpdate?: (resourceId: string, url: string) => void;
showHostContext?: boolean;
resourceIndentClass?: string;
@ -1677,8 +1616,6 @@ const DockerServiceRow: Component<{
const hostDisplayName = () => getHostDisplayName(host);
const resourceIndent = () => props.resourceIndentClass ?? GROUPED_RESOURCE_INDENT;
const [customUrl, setCustomUrl] = createSignal<string | undefined>(props.customUrl);
const [shouldAnimateIcon, setShouldAnimateIcon] = createSignal(false);
const expanded = createMemo(() => currentlyExpandedRowId() === rowId);
@ -1686,19 +1623,7 @@ const DockerServiceRow: Component<{
// Update custom URL when prop changes
createEffect(() => {
const prevUrl = customUrl();
const newUrl = props.customUrl;
// Only animate when URL transitions from empty to having a value
if (!prevUrl && newUrl) {
setShouldAnimateIcon(true);
setTimeout(() => setShouldAnimateIcon(false), 200);
}
setCustomUrl(newUrl);
});
@ -1753,21 +1678,6 @@ const DockerServiceRow: Component<{
>
{service.name || service.id || 'Service'}
</span>
<Show when={customUrl()}>
<a
href={customUrl()}
target="_blank"
rel="noopener noreferrer"
class={`flex-shrink-0 text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 transition-colors ${shouldAnimateIcon() ? 'animate-fadeIn' : ''}`}
title="Open in new tab"
onClick={(event) => event.stopPropagation()}
>
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
</svg>
</a>
</Show>
{/* Edit URL button - shows on hover */}
<Show when={service.stack}>
<span class="text-[10px] text-gray-500 dark:text-gray-400 truncate" title={`Stack: ${service.stack}`}>
@ -1787,7 +1697,7 @@ const DockerServiceRow: Component<{
</div>
</div>
</div>
</div>
</div >
);
case 'type':
return (
@ -2078,11 +1988,6 @@ const DockerServiceRow: Component<{
resourceId={service.id || service.name || ''}
hostname={service.name || service.id || 'Service'}
guestId={resourceId()}
customUrl={customUrl()}
onCustomUrlChange={(url) => {
setCustomUrl(url || undefined);
props.onCustomUrlUpdate?.(resourceId(), url);
}}
/>
</Show>
</div>
@ -2433,18 +2338,12 @@ const DockerUnifiedTable: Component<DockerUnifiedTableProps> = (props) => {
);
const renderRow = (row: DockerRow, grouped: boolean) => {
const resourceId =
row.kind === 'container'
? `${row.host.id}:container:${row.container.id || row.container.name}`
: `${row.host.id}:service:${row.service.id || row.service.name}`;
const metadata = props.dockerMetadata?.[resourceId];
return row.kind === 'container' ? (
<DockerContainerRow
row={row}
isMobile={isMobile}
customUrl={metadata?.customUrl}
onCustomUrlUpdate={props.onCustomUrlUpdate}
showHostContext={!grouped}
resourceIndentClass={grouped ? GROUPED_RESOURCE_INDENT : UNGROUPED_RESOURCE_INDENT}
batchUpdateState={props.batchUpdateState}
@ -2454,8 +2353,6 @@ const DockerUnifiedTable: Component<DockerUnifiedTableProps> = (props) => {
<DockerServiceRow
row={row}
isMobile={isMobile}
customUrl={metadata?.customUrl}
onCustomUrlUpdate={props.onCustomUrlUpdate}
showHostContext={!grouped}
resourceIndentClass={grouped ? GROUPED_RESOURCE_INDENT : UNGROUPED_RESOURCE_INDENT}
/>
@ -2549,23 +2446,17 @@ const DockerUnifiedTable: Component<DockerUnifiedTableProps> = (props) => {
}
>
<For each={orderedGroups()}>
{(group) => {
const hostId = group.host.id;
const metadata = props.dockerHostMetadata?.[hostId];
return (
<>
<DockerHostGroupHeader
host={group.host}
columnCount={DOCKER_COLUMNS.length}
customUrl={metadata?.customUrl}
onCustomUrlUpdate={props.onHostCustomUrlUpdate}
/>
<For each={group.rows}>
{(row) => renderRow(row, true)}
</For>
</>
);
}}
{(group) => (
<>
<DockerHostGroupHeader
host={group.host}
columnCount={DOCKER_COLUMNS.length}
/>
<For each={group.rows}>
{(row) => renderRow(row, true)}
</For>
</>
)}
</For>
</Show>
</tbody>
@ -2592,7 +2483,7 @@ const DockerUnifiedTable: Component<DockerUnifiedTableProps> = (props) => {
</span>
</div>
</Show>
</div>
</div >
);
};