mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-11 14:00:29 +00:00
Render Docker bind mounts in the Discovery tab
The backend captures DockerMounts (and now surfaces them to the assistant), but the human-facing DiscoveryTab showed config/data/log paths and ports while dropping the host<->container bind mounts — so a user could not see where a container path like /config actually lives on the host to edit or back up files. Add docker_mounts to the frontend ResourceDiscovery type and render a 'Bind Mounts' card (host source copyable, '-> destination in container', read-only marker), consistent with the Ports card. Test renders an app-container discovery with a bind mount and asserts the host source is visible. Type-check + full lint + prettier clean; full suite green (2 pre-existing guardrail failures unrelated).
This commit is contained in:
@@ -904,6 +904,38 @@ export const DiscoveryTab: Component<DiscoveryTabProps> = (props) => {
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
{/* Docker bind mounts — host ↔ container path mapping, only on
|
||||
Docker resources. The host source is where to actually edit or
|
||||
back up files; the container destination is what the app sees. */}
|
||||
<Show when={(d().docker_mounts?.length ?? 0) > 0}>
|
||||
<div class="rounded border border-border p-3 shadow-sm">
|
||||
<div class="mb-2 flex items-center gap-1.5 text-[11px] font-medium uppercase tracking-wide text-base-content">
|
||||
<span>Bind Mounts</span>
|
||||
<DiscoveryProvenanceMarker showLabel={false} />
|
||||
</div>
|
||||
<div class="space-y-1.5">
|
||||
<For each={d().docker_mounts}>
|
||||
{(mount) => (
|
||||
<div class="space-y-0.5">
|
||||
<CopyableCodeRow
|
||||
value={mount.source}
|
||||
copiedValue={copiedDiscoveryValue}
|
||||
onCopy={handleCopyDiscoveryValue}
|
||||
label="Copy host path"
|
||||
/>
|
||||
<div class="pl-1 text-[10px] text-muted">
|
||||
→ {mount.destination} in container
|
||||
<Show when={mount.read_only}>
|
||||
<span> (read-only)</span>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
{/* Ports */}
|
||||
<Show when={d().ports?.length > 0}>
|
||||
<div class="rounded border border-border p-3 shadow-sm">
|
||||
|
||||
@@ -354,4 +354,54 @@ describe('DiscoveryTab', () => {
|
||||
await screen.findByText('Mapped open ports and running processes to a PostgreSQL service.'),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders Docker bind mounts so the host path is visible', async () => {
|
||||
vi.mocked(discoveryApi.getDiscoveryInfo).mockResolvedValue(discoveryInfoWithProvider());
|
||||
vi.mocked(discoveryApi.getDiscovery).mockResolvedValue({
|
||||
id: 'app-container:nuc:homeassistant',
|
||||
resource_type: 'app-container',
|
||||
resource_id: 'homeassistant',
|
||||
target_id: 'nuc',
|
||||
hostname: 'nuc',
|
||||
service_type: 'home-assistant',
|
||||
service_name: 'Home Assistant',
|
||||
service_version: '2026.5',
|
||||
category: 'home_automation',
|
||||
cli_access: 'docker exec homeassistant bash',
|
||||
facts: [],
|
||||
config_paths: ['/config/automations.yaml'],
|
||||
data_paths: [],
|
||||
log_paths: [],
|
||||
ports: [],
|
||||
docker_mounts: [
|
||||
{
|
||||
container_name: 'homeassistant',
|
||||
source: '/opt/homeassistant/config',
|
||||
destination: '/config',
|
||||
type: 'bind',
|
||||
},
|
||||
],
|
||||
user_notes: '',
|
||||
user_secrets: {},
|
||||
confidence: 0.9,
|
||||
ai_reasoning: '',
|
||||
discovered_at: '2026-05-01T00:00:00Z',
|
||||
updated_at: '2026-05-01T00:00:00Z',
|
||||
scan_duration: 10,
|
||||
});
|
||||
|
||||
render(() => (
|
||||
<DiscoveryTab
|
||||
resourceType="app-container"
|
||||
agentId="nuc"
|
||||
resourceId="homeassistant"
|
||||
hostname="nuc"
|
||||
/>
|
||||
));
|
||||
|
||||
// The host source is where you actually edit/back up the files — it must be
|
||||
// visible, not just the container-internal /config path.
|
||||
expect(await screen.findByText('Bind Mounts')).toBeInTheDocument();
|
||||
expect(await screen.findByText('/opt/homeassistant/config')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -48,6 +48,16 @@ export interface PortInfo {
|
||||
address: string;
|
||||
}
|
||||
|
||||
export interface DockerBindMount {
|
||||
container_name?: string;
|
||||
/** Host path — where the container's files actually live. */
|
||||
source: string;
|
||||
/** Container path — what the service sees. */
|
||||
destination: string;
|
||||
type?: string;
|
||||
read_only?: boolean;
|
||||
}
|
||||
|
||||
export interface ResourceDiscovery {
|
||||
id: string;
|
||||
resource_type: APIResourceType;
|
||||
@@ -67,6 +77,7 @@ export interface ResourceDiscovery {
|
||||
data_paths: string[];
|
||||
log_paths: string[];
|
||||
ports: PortInfo[];
|
||||
docker_mounts?: DockerBindMount[];
|
||||
user_notes: string;
|
||||
user_secrets: Record<string, string>;
|
||||
confidence: number;
|
||||
|
||||
Reference in New Issue
Block a user