mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-19 10:33:29 +00:00
fix(discovery): persist custom URL from discovery tab for Docker and Kubernetes
The DiscoveryTab component for Docker containers/services and Kubernetes pods was not receiving the customUrl and onCustomUrlChange props, causing saved URLs to be lost when the drawer was reopened. Changes: - Add guest metadata management to DockerUnifiedTable (fetches on mount, caches, listens for external changes) - Pass customUrl and onCustomUrlChange props to DiscoveryTab in DockerContainerRow and DockerServiceRow - Add guest metadata management to KubernetesClusters component - Update PodRow to receive and pass metadata props to DiscoveryTab The fix ensures that when users click 'Use this' on a suggested URL and save, the URL persists correctly across Docker containers, Swarm services, and K8s pods.
This commit is contained in:
parent
7c1ebbecd5
commit
a9d7713313
2 changed files with 153 additions and 15 deletions
|
|
@ -1,4 +1,4 @@
|
|||
import { Component, For, Show, createMemo, createSignal, createEffect, Accessor } from 'solid-js';
|
||||
import { Component, For, Show, createMemo, createSignal, createEffect, Accessor, onMount } from 'solid-js';
|
||||
import type { DockerHost, DockerContainer, DockerService, DockerTask } from '@/types/api';
|
||||
import { Card } from '@/components/shared/Card';
|
||||
import { EmptyState } from '@/components/shared/EmptyState';
|
||||
|
|
@ -36,6 +36,21 @@ import type { ColumnConfig } from '@/types/responsive';
|
|||
import { DiscoveryTab } from '@/components/Discovery/DiscoveryTab';
|
||||
import { HistoryChart } from '@/components/shared/HistoryChart';
|
||||
import type { HistoryTimeRange } from '@/api/charts';
|
||||
import { GuestMetadataAPI, type GuestMetadata } from '@/api/guestMetadata';
|
||||
import { logger } from '@/utils/logger';
|
||||
|
||||
type GuestMetadataRecord = Record<string, GuestMetadata>;
|
||||
|
||||
// Module-level cache for guest metadata to persist across component remounts
|
||||
let cachedDockerGuestMetadata: GuestMetadataRecord | null = null;
|
||||
|
||||
const getDockerGuestMetadataCache = (): GuestMetadataRecord => {
|
||||
return cachedDockerGuestMetadata ?? {};
|
||||
};
|
||||
|
||||
const setDockerGuestMetadataCache = (metadata: GuestMetadataRecord) => {
|
||||
cachedDockerGuestMetadata = metadata;
|
||||
};
|
||||
|
||||
const typeBadgeClass = (type: 'container' | 'service' | 'task' | 'dockerHost' | 'unknown') => {
|
||||
switch (type) {
|
||||
|
|
@ -788,6 +803,8 @@ const DockerContainerRow: Component<{
|
|||
showHostContext?: boolean;
|
||||
resourceIndentClass?: string;
|
||||
batchUpdateState?: Record<string, 'updating' | 'queued' | 'error'>;
|
||||
guestMetadata?: GuestMetadataRecord;
|
||||
onCustomUrlChange?: (guestId: string, url: string) => void;
|
||||
}> = (props) => {
|
||||
const { host, container } = props.row;
|
||||
const runtimeInfo = resolveHostRuntime(host);
|
||||
|
|
@ -1590,6 +1607,8 @@ const DockerContainerRow: Component<{
|
|||
resourceId={container.id}
|
||||
hostname={container.name}
|
||||
guestId={container.id}
|
||||
customUrl={props.guestMetadata?.[container.id]?.customUrl}
|
||||
onCustomUrlChange={(url) => props.onCustomUrlChange?.(container.id, url)}
|
||||
/>
|
||||
</Show>
|
||||
</div>
|
||||
|
|
@ -1603,9 +1622,10 @@ const DockerContainerRow: Component<{
|
|||
const DockerServiceRow: Component<{
|
||||
row: Extract<DockerRow, { kind: 'service' }>;
|
||||
isMobile: Accessor<boolean>;
|
||||
onCustomUrlUpdate?: (resourceId: string, url: string) => void;
|
||||
showHostContext?: boolean;
|
||||
resourceIndentClass?: string;
|
||||
guestMetadata?: GuestMetadataRecord;
|
||||
onCustomUrlChange?: (guestId: string, url: string) => void;
|
||||
}> = (props) => {
|
||||
const { host, service, tasks } = props.row;
|
||||
const rowId = buildRowId(host, props.row);
|
||||
|
|
@ -1940,6 +1960,9 @@ const DockerServiceRow: Component<{
|
|||
<option value="6h">Last 6 hours</option>
|
||||
<option value="12h">Last 12 hours</option>
|
||||
<option value="24h">Last 24 hours</option>
|
||||
<option value="7d">Last 7 days</option>
|
||||
<option value="30d">Last 30 days</option>
|
||||
<option value="90d">Last 90 days</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
|
|
@ -1988,6 +2011,8 @@ const DockerServiceRow: Component<{
|
|||
resourceId={service.id || service.name || ''}
|
||||
hostname={service.name || service.id || 'Service'}
|
||||
guestId={resourceId()}
|
||||
customUrl={props.guestMetadata?.[resourceId()]?.customUrl}
|
||||
onCustomUrlChange={(url) => props.onCustomUrlChange?.(resourceId(), url)}
|
||||
/>
|
||||
</Show>
|
||||
</div>
|
||||
|
|
@ -2010,6 +2035,54 @@ const DockerUnifiedTable: Component<DockerUnifiedTableProps> = (props) => {
|
|||
// Use the breakpoint hook for responsive behavior
|
||||
const { isMobile } = useBreakpoint();
|
||||
|
||||
// Guest metadata for tracking custom URLs
|
||||
const [guestMetadata, setGuestMetadata] = createSignal<GuestMetadataRecord>(getDockerGuestMetadataCache());
|
||||
|
||||
// Load guest metadata on mount
|
||||
onMount(async () => {
|
||||
try {
|
||||
const metadata = await GuestMetadataAPI.getAllMetadata();
|
||||
setGuestMetadata(metadata ?? {});
|
||||
setDockerGuestMetadataCache(metadata ?? {});
|
||||
} catch (err) {
|
||||
logger.debug('Failed to load guest metadata for Docker', err);
|
||||
}
|
||||
|
||||
// Listen for metadata changes from other sources (e.g., AI, other tabs)
|
||||
const handleMetadataChanged = async () => {
|
||||
try {
|
||||
const metadata = await GuestMetadataAPI.getAllMetadata();
|
||||
setGuestMetadata(metadata ?? {});
|
||||
setDockerGuestMetadataCache(metadata ?? {});
|
||||
} catch (err) {
|
||||
logger.debug('Failed to refresh guest metadata', err);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('pulse:metadata-changed', handleMetadataChanged);
|
||||
return () => window.removeEventListener('pulse:metadata-changed', handleMetadataChanged);
|
||||
});
|
||||
|
||||
// Handle custom URL changes from discovery tab
|
||||
const handleCustomUrlChange = (guestId: string, url: string) => {
|
||||
const trimmed = url.trim();
|
||||
setGuestMetadata((prev) => {
|
||||
const updated = { ...prev };
|
||||
if (trimmed) {
|
||||
updated[guestId] = { ...updated[guestId], id: guestId, customUrl: trimmed };
|
||||
} else if (updated[guestId]) {
|
||||
const { customUrl: _, ...rest } = updated[guestId];
|
||||
if (Object.keys(rest).length > 1) {
|
||||
updated[guestId] = rest as GuestMetadata;
|
||||
} else {
|
||||
delete updated[guestId];
|
||||
}
|
||||
}
|
||||
setDockerGuestMetadataCache(updated);
|
||||
return updated;
|
||||
});
|
||||
};
|
||||
|
||||
// Caches for stable object references to prevent re-animations
|
||||
const rowCache = new Map<string, DockerRow>();
|
||||
const tasksCache = new Map<string, DockerTask[]>();
|
||||
|
|
@ -2347,6 +2420,8 @@ const DockerUnifiedTable: Component<DockerUnifiedTableProps> = (props) => {
|
|||
showHostContext={!grouped}
|
||||
resourceIndentClass={grouped ? GROUPED_RESOURCE_INDENT : UNGROUPED_RESOURCE_INDENT}
|
||||
batchUpdateState={props.batchUpdateState}
|
||||
guestMetadata={guestMetadata()}
|
||||
onCustomUrlChange={handleCustomUrlChange}
|
||||
/>
|
||||
|
||||
) : (
|
||||
|
|
@ -2355,6 +2430,8 @@ const DockerUnifiedTable: Component<DockerUnifiedTableProps> = (props) => {
|
|||
isMobile={isMobile}
|
||||
showHostContext={!grouped}
|
||||
resourceIndentClass={grouped ? GROUPED_RESOURCE_INDENT : UNGROUPED_RESOURCE_INDENT}
|
||||
guestMetadata={guestMetadata()}
|
||||
onCustomUrlChange={handleCustomUrlChange}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import type { Component } from 'solid-js';
|
||||
import { For, Show, createMemo, createSignal, createEffect, onMount } from 'solid-js';
|
||||
import { For, Show, createMemo, createSignal, createEffect, onMount, Accessor } from 'solid-js';
|
||||
import { AIAPI } from '@/api/ai';
|
||||
import Sparkles from 'lucide-solid/icons/sparkles';
|
||||
import ExternalLink from 'lucide-solid/icons/external-link';
|
||||
|
|
@ -24,6 +24,21 @@ import { DEGRADED_HEALTH_STATUSES, OFFLINE_HEALTH_STATUSES, type StatusIndicator
|
|||
import { DiscoveryTab } from '@/components/Discovery/DiscoveryTab';
|
||||
import { HistoryChart } from '@/components/shared/HistoryChart';
|
||||
import type { HistoryTimeRange } from '@/api/charts';
|
||||
import { GuestMetadataAPI, type GuestMetadata } from '@/api/guestMetadata';
|
||||
import { logger } from '@/utils/logger';
|
||||
|
||||
type GuestMetadataRecord = Record<string, GuestMetadata>;
|
||||
|
||||
// Module-level cache for guest metadata to persist across component remounts
|
||||
let cachedK8sGuestMetadata: GuestMetadataRecord | null = null;
|
||||
|
||||
const getK8sGuestMetadataCache = (): GuestMetadataRecord => {
|
||||
return cachedK8sGuestMetadata ?? {};
|
||||
};
|
||||
|
||||
const setK8sGuestMetadataCache = (metadata: GuestMetadataRecord) => {
|
||||
cachedK8sGuestMetadata = metadata;
|
||||
};
|
||||
|
||||
// Global state for expanded row
|
||||
const [expandedRowId, setExpandedRowId] = createSignal<string | null>(null);
|
||||
|
|
@ -155,11 +170,12 @@ const PodRow: Component<{
|
|||
cluster: KubernetesCluster;
|
||||
pod: KubernetesPod;
|
||||
columns: { isColumnVisible: (id: string) => boolean };
|
||||
guestMetadata: Accessor<GuestMetadataRecord>;
|
||||
onCustomUrlChange: (guestId: string, url: string) => void;
|
||||
}> = (props) => {
|
||||
const rowId = `${props.cluster.id}:${props.pod.uid}`;
|
||||
const isExpanded = createMemo(() => expandedRowId() === rowId);
|
||||
const [activeTab, setActiveTab] = createSignal<'overview' | 'discovery'>('overview');
|
||||
const [customUrl, setCustomUrl] = createSignal<string | undefined>(undefined);
|
||||
const [historyRange, setHistoryRange] = createSignal<HistoryTimeRange>('1h');
|
||||
|
||||
const toggle = (e: MouseEvent) => {
|
||||
|
|
@ -358,8 +374,8 @@ const PodRow: Component<{
|
|||
resourceId={props.pod.uid}
|
||||
guestId={props.pod.uid}
|
||||
hostname={props.pod.name}
|
||||
customUrl={customUrl()}
|
||||
onCustomUrlChange={setCustomUrl}
|
||||
customUrl={props.guestMetadata()[props.pod.uid]?.customUrl}
|
||||
onCustomUrlChange={(url) => props.onCustomUrlChange(props.pod.uid, url)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -391,6 +407,9 @@ export const KubernetesClusters: Component<KubernetesClustersProps> = (props) =>
|
|||
// Column visibility for pods table
|
||||
const podColumns = useColumnVisibility('k8s-pod-columns', POD_COLUMNS);
|
||||
|
||||
// Guest metadata for tracking custom URLs
|
||||
const [guestMetadata, setGuestMetadata] = createSignal<GuestMetadataRecord>(getK8sGuestMetadataCache());
|
||||
|
||||
// Sorting state with persistence
|
||||
type SortKey = 'name' | 'status' | 'namespace' | 'cluster' | 'age' | 'restarts' | 'ready' | 'replicas';
|
||||
type SortDir = 'asc' | 'desc';
|
||||
|
|
@ -478,6 +497,28 @@ export const KubernetesClusters: Component<KubernetesClustersProps> = (props) =>
|
|||
onMount(() => {
|
||||
void loadLicenseStatus();
|
||||
void loadAiSettings();
|
||||
|
||||
// Load guest metadata
|
||||
GuestMetadataAPI.getAllMetadata().then((metadata) => {
|
||||
setGuestMetadata(metadata ?? {});
|
||||
setK8sGuestMetadataCache(metadata ?? {});
|
||||
}).catch((err) => {
|
||||
logger.debug('Failed to load guest metadata for K8s', err);
|
||||
});
|
||||
|
||||
// Listen for metadata changes from other sources
|
||||
const handleMetadataChanged = async () => {
|
||||
try {
|
||||
const metadata = await GuestMetadataAPI.getAllMetadata();
|
||||
setGuestMetadata(metadata ?? {});
|
||||
setK8sGuestMetadataCache(metadata ?? {});
|
||||
} catch (err) {
|
||||
logger.debug('Failed to refresh guest metadata', err);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('pulse:metadata-changed', handleMetadataChanged);
|
||||
return () => window.removeEventListener('pulse:metadata-changed', handleMetadataChanged);
|
||||
});
|
||||
|
||||
createEffect(() => {
|
||||
|
|
@ -776,6 +817,26 @@ export const KubernetesClusters: Component<KubernetesClustersProps> = (props) =>
|
|||
setViewMode('clusters');
|
||||
};
|
||||
|
||||
// Handle custom URL changes from discovery tab
|
||||
const handleK8sCustomUrlChange = (guestId: string, url: string) => {
|
||||
const trimmed = url.trim();
|
||||
setGuestMetadata((prev) => {
|
||||
const updated = { ...prev };
|
||||
if (trimmed) {
|
||||
updated[guestId] = { ...updated[guestId], id: guestId, customUrl: trimmed };
|
||||
} else if (updated[guestId]) {
|
||||
const { customUrl: _, ...rest } = updated[guestId];
|
||||
if (Object.keys(rest).length > 1) {
|
||||
updated[guestId] = rest as GuestMetadata;
|
||||
} else {
|
||||
delete updated[guestId];
|
||||
}
|
||||
}
|
||||
setK8sGuestMetadataCache(updated);
|
||||
return updated;
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div class="space-y-4">
|
||||
<Show when={clustersForAnalysis().length > 0}>
|
||||
|
|
@ -1295,15 +1356,15 @@ export const KubernetesClusters: Component<KubernetesClustersProps> = (props) =>
|
|||
<For each={filteredPods()} fallback={
|
||||
<tr><td colSpan={8} class="px-4 py-8 text-center text-sm text-gray-500 dark:text-gray-400">No pods match the current filters.</td></tr>
|
||||
}>
|
||||
{({ cluster, pod }) => {
|
||||
return (
|
||||
<PodRow
|
||||
cluster={cluster}
|
||||
pod={pod}
|
||||
columns={podColumns}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
{({ cluster, pod }) => (
|
||||
<PodRow
|
||||
cluster={cluster}
|
||||
pod={pod}
|
||||
columns={podColumns}
|
||||
guestMetadata={guestMetadata}
|
||||
onCustomUrlChange={handleK8sCustomUrlChange}
|
||||
/>
|
||||
)}
|
||||
</For>
|
||||
</tbody>
|
||||
</table>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue