Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .changelog/next/fixed-issue-4152.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Fullscreen process-log view no longer gets trapped inside a card on glass themes — its overlay now portals to the document body so it covers the full viewport
19 changes: 16 additions & 3 deletions client/src/components/apps/tabs/ProcessesTab.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useState, useEffect, useRef, Fragment } from 'react';
import { createPortal } from 'react-dom';
import { Maximize2, X } from 'lucide-react';
import * as api from '../../../services/api';
import { executeCommand } from '../../../services/api';
Expand Down Expand Up @@ -227,8 +228,19 @@ export default function ProcessesTab({ appId, pm2ProcessNames, filterFn }) {
</div>
</div>

{/* Fullscreen Log Modal */}
{fullscreen && expandedProcess && (
{/*
Fullscreen Log Modal — portaled to <body>.

This tab renders inside an app-detail / processes-page card tree, and on
"glass" themes (`--port-backdrop-filter` non-none) `index.css` gives every
bordered/rounded `.bg-port-card` a backdrop-filter, which makes that card
the containing block for `position:fixed` descendants. Rendered inline the
overlay would be sized to the card instead of the viewport. Same escape
GalleryImagePicker / FolderPicker get via <ui/Modal>'s `usePortal`, and the
same fix MediaLightbox took in #4151 — reached through createPortal
directly because this overlay isn't a <Modal>.
*/}
{fullscreen && expandedProcess && createPortal(
<div className="fixed inset-0 bg-port-bg z-50 flex flex-col">
<div className="flex items-center justify-between px-6 py-4 border-b border-port-border bg-port-card">
<div className="flex items-center gap-4">
Expand Down Expand Up @@ -273,7 +285,8 @@ export default function ProcessesTab({ appId, pm2ProcessNames, filterFn }) {
>
<ProcessLogLines logs={logs} subscribed={subscribed} showTimestamps timestampGap="mr-3" />
</div>
</div>
</div>,
document.body
)}
</>
);
Expand Down
22 changes: 22 additions & 0 deletions client/src/components/apps/tabs/ProcessesTab.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,26 @@ describe('ProcessesTab', () => {

expect(useProcessLogs).toHaveBeenLastCalledWith('example-api', { lines: 500, appId: 'app-1' });
});

// On "glass" themes a bordered/rounded `.bg-port-card` gets a backdrop-filter,
// which makes it the containing block for `position:fixed` descendants — an
// inline overlay would be sized to the card, not the viewport. Portaling to
// <body> is the escape, so assert the overlay leaves the component's subtree.
it('portals the fullscreen log overlay to <body> so glass-theme cards cannot trap it', () => {
const { container } = render(<ProcessesTab appId="app-1" pm2ProcessNames={['example-api']} />);

fireEvent.click(screen.getByRole('button', { name: 'Expand details for example-api' }));
fireEvent.click(screen.getByTitle('Fullscreen'));

const overlay = document.body.querySelector('.fixed.inset-0');
expect(overlay).toBeTruthy();
// Rendered out of the tab tree entirely, directly under <body>.
expect(container.contains(overlay)).toBe(false);
expect(overlay.parentElement).toBe(document.body);
expect(screen.getByText('Logs: example-api')).toBeTruthy();

// Exiting fullscreen tears the portaled overlay back down.
fireEvent.click(screen.getByRole('button', { name: 'Exit fullscreen' }));
expect(document.body.querySelector('.fixed.inset-0')).toBeNull();
});
});