mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-25 04:59:43 +00:00
Open a guest's saved web interface from its drawer header
Phone and narrow workload rows hide the adjacent web link so guest names stay distinguishable. On a phone that left a saved service reachable only from the URL field on the drawer's Manage tab, three taps deep and behind a settings form. The guest drawer header now carries an Open action whenever a URL is saved, built from the shared WebInterfaceLink and drawer-header action class, so the service is one tap from the expanded row with a 44px phone target. Rows are unchanged: showing the row link at 393px truncated names until checkout-web-01 and checkout-web-02 read the same. Refs #1556.
This commit is contained in:
@@ -1111,6 +1111,13 @@ change may globally weaken the Task 03 lifecycle-state idempotency invariant.
|
||||
rows without platform metadata retain the Proxmox-compatible behavior at
|
||||
the migration boundary.
|
||||
11. Extend workload drawer derivations and runtime wiring through `frontend-modern/src/components/Workloads/guestDrawerModel.ts` and `frontend-modern/src/components/Workloads/useGuestDrawerState.ts`, and extend drawer overview rendering through `frontend-modern/src/components/Workloads/GuestDrawerOverview.tsx`, rather than rebuilding canonical guest identity, discovery routing, or drawer-local normalization inside `frontend-modern/src/components/Workloads/GuestDrawer.tsx`
|
||||
The guest drawer header owns the saved web-interface launch action. It
|
||||
reuses the shared `WebInterfaceLink` and drawer-header action class, so
|
||||
phone and narrow rows keep hiding the adjacent row link for name legibility
|
||||
while the saved service stays one tap from the expanded row. The action
|
||||
renders only for a saved URL and adds no fetch, polling or per-row work.
|
||||
`GuestDrawer.test.tsx` verifies its target, the unsafe-URL warning, and
|
||||
that opening it does not collapse the drawer.
|
||||
Drawer history charts belong to `frontend-modern/src/components/Workloads/GuestDrawerHistory.tsx`.
|
||||
A current metric may remain visible in a chart legend only when labelled
|
||||
`current`; it must never be expanded into synthetic timestamps or a flat
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"version": 1,
|
||||
"base_sha": "f5157115b7b224000ef7035285d9ce64bbe60bb7",
|
||||
"verified_at": "2026-10-04T22:49:37Z",
|
||||
"result": "passed",
|
||||
"changed_paths": [
|
||||
"frontend-modern/src/components/Workloads/GuestDrawer.tsx"
|
||||
],
|
||||
"content_sha256": {
|
||||
"frontend-modern/src/components/Workloads/GuestDrawer.tsx": "7e2b570d0af3b0cdf15ddd0c8944520094434937a71774d5f6eba5d7578b69c9"
|
||||
},
|
||||
"routes": [
|
||||
"/proxmox"
|
||||
],
|
||||
"viewports": [
|
||||
{
|
||||
"width": 1280,
|
||||
"height": 800
|
||||
},
|
||||
{
|
||||
"width": 393,
|
||||
"height": 852
|
||||
},
|
||||
{
|
||||
"width": 375,
|
||||
"height": 812
|
||||
}
|
||||
],
|
||||
"states": [
|
||||
"Private mock stack built from this tree (backend :7701, Vite :5221, 3 nodes, 31 guests); web-interface URLs saved on five pve1 guests, one through the real Manage-tab field and four through PUT /api/guests/metadata.",
|
||||
"Before: at 375 and 393 widths the saved row links were display:none in phone/narrow rows and the drawer header held only the name and collapse chevron, so the URL was reachable only from the Manage tab field.",
|
||||
"Rejected alternative measured live: un-hiding the row link at 393 truncated names to 'checkout-w...' and 'postgres-p...', indistinguishable from checkout-web-02 and postgres-replica-01, so phone rows stay unchanged.",
|
||||
"After: auth-service-01 and postgres-primary-01 drawers show an 'Open' header action at 69x44 on 375/393 phones and 69x32 at 1280, href to the saved URL, target _blank, rel noopener noreferrer; drawer heading not truncated; no document horizontal overflow at any width.",
|
||||
"edge-proxy-01 (no saved URL) shows no header action. Desktop rows keep the adjacent row link. This is not physical-device, real-service reachability or release acceptance."
|
||||
],
|
||||
"interactions": [
|
||||
"Tapped guest rows to expand drawers at 375, 393 and 1280 widths; switched to the Manage tab and saved a URL with Enter; the row link and header action appeared without reload.",
|
||||
"Activated the header Open link with navigation prevented in the capture phase: the drawer stayed expanded at 393 and 1280 (the link stops propagation and does not hit the collapse overlay)."
|
||||
]
|
||||
}
|
||||
@@ -1530,6 +1530,46 @@ describe('GuestDrawer', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('header web interface action', () => {
|
||||
it('opens the saved URL from the header without collapsing the drawer', () => {
|
||||
const onClose = vi.fn();
|
||||
render(() => (
|
||||
<GuestDrawer guest={makeGuest()} customUrl="https://auth.home.example" onClose={onClose} />
|
||||
));
|
||||
|
||||
const open = screen.getByRole('link', { name: 'Open web interface for test-vm' });
|
||||
expect(open).toHaveAttribute('href', 'https://auth.home.example');
|
||||
expect(open).toHaveAttribute('target', '_blank');
|
||||
expect(open).toHaveAttribute('rel', 'noopener noreferrer');
|
||||
expect(open).toHaveTextContent('Open');
|
||||
expect(open.className).toContain('min-h-11');
|
||||
|
||||
fireEvent.click(open);
|
||||
expect(onClose).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it.each([undefined, ' '])(
|
||||
'renders no header action without a saved URL (%j)',
|
||||
(customUrl) => {
|
||||
render(() => <GuestDrawer guest={makeGuest()} customUrl={customUrl} onClose={vi.fn()} />);
|
||||
|
||||
expect(
|
||||
screen.queryByRole('link', { name: 'Open web interface for test-vm' }),
|
||||
).not.toBeInTheDocument();
|
||||
},
|
||||
);
|
||||
|
||||
it('never turns an unsafe saved URL into a header link', () => {
|
||||
const unsafeUrl = 'data:text/html,<script>alert(1)</script>';
|
||||
render(() => <GuestDrawer guest={makeGuest()} customUrl={unsafeUrl} onClose={vi.fn()} />);
|
||||
|
||||
expect(
|
||||
screen.queryByRole('link', { name: 'Open web interface for test-vm' }),
|
||||
).not.toBeInTheDocument();
|
||||
expect(screen.getByRole('img', { name: 'Invalid web interface URL' })).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
// ── Discovery tab integration ──
|
||||
|
||||
describe('DiscoveryTab integration', () => {
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import { Component, Show, Suspense, createMemo } from 'solid-js';
|
||||
import ExternalLinkIcon from 'lucide-solid/icons/external-link';
|
||||
import { DiscoveryTab } from '../Discovery/DiscoveryTab';
|
||||
import { getDrawerHeaderActionButtonClass } from '@/components/shared/buttonModel';
|
||||
import { DiscoveryLoadingFallback } from '@/components/shared/DiscoveryLoadingFallback';
|
||||
import { DrawerSubjectHeading } from '@/components/shared/DrawerSubjectHeading';
|
||||
import { DiscoveryReadinessBadge } from '@/components/shared/DiscoveryReadinessBadge';
|
||||
import { ObjectDrawerHeader } from '@/components/shared/ObjectDrawerHeader';
|
||||
import { Subtabs, type SubtabOption } from '@/components/shared/Subtabs';
|
||||
import { WebInterfaceLink } from '@/components/shared/WebInterfaceLink';
|
||||
import { getSimpleStatusIndicator } from '@/utils/status';
|
||||
import {
|
||||
GUEST_DRAWER_BACKUP_PRECAUTION,
|
||||
@@ -70,9 +73,23 @@ const GuestDrawerContent: Component<GuestDrawerProps> = (props) => {
|
||||
|
||||
return (
|
||||
<section class="space-y-3" aria-labelledby={headingId()}>
|
||||
{/* Phone rows hide the adjacent web link so names stay distinguishable;
|
||||
the header keeps the saved service one tap from the expanded row. */}
|
||||
<ObjectDrawerHeader
|
||||
collapseLabel={`Collapse ${props.guest.name} details`}
|
||||
onCollapse={props.onClose}
|
||||
actions={
|
||||
<Show when={props.customUrl?.trim()}>
|
||||
<WebInterfaceLink
|
||||
url={props.customUrl}
|
||||
ariaLabel={`Open web interface for ${props.guest.name}`}
|
||||
class={getDrawerHeaderActionButtonClass()}
|
||||
>
|
||||
<ExternalLinkIcon class="h-3.5 w-3.5" aria-hidden="true" />
|
||||
<span>Open</span>
|
||||
</WebInterfaceLink>
|
||||
</Show>
|
||||
}
|
||||
>
|
||||
<DrawerSubjectHeading
|
||||
headingId={headingId()}
|
||||
|
||||
Reference in New Issue
Block a user