mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-19 10:33:29 +00:00
refactor(docker): remove deprecated guest URL edit functionality and align table consistency
This commit is contained in:
parent
a3436fbde5
commit
5e0cdfc163
3 changed files with 32 additions and 387 deletions
|
|
@ -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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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'}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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 >
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue