Replace container ⋮ menu with a designed actions modal
Release rolling / release (push) Successful in 8m24s

Row more-button opens containerActionsModal with lifecycle, manage, and
danger-zone actions instead of a fragile context/dropdown menu.
This commit is contained in:
2026-07-11 22:23:58 -04:00
parent 41d71b57a3
commit 441781f9d6
3 changed files with 548 additions and 325 deletions
+267 -229
View File
@@ -7521,31 +7521,9 @@ function buildContainerRow(container) {
<button class="btn btn-outline-primary action-terminal p-1" title="Terminal" ${container.State !== 'running' ? 'disabled' : ''}>
<i class="fas fa-terminal"></i>
</button>
</div>
<div class="dropdown container-actions-more">
<button
class="btn btn-sm btn-outline-secondary dropdown-toggle p-1 container-actions-more-toggle"
type="button"
aria-haspopup="true"
aria-expanded="false"
title="More actions"
>
<button class="btn btn-outline-secondary action-more p-1" type="button" title="More actions" aria-label="More actions">
<i class="fas fa-ellipsis"></i>
</button>
<ul class="dropdown-menu dropdown-menu-end dropdown-menu-dark container-actions-dropdown-menu" role="menu">
<li><button type="button" class="dropdown-item action-restart" ${container.State !== 'running' ? 'disabled' : ''}><i class="fas fa-redo me-2"></i>Restart</button></li>
<li><button type="button" class="dropdown-item action-kill" ${container.State !== 'running' ? 'disabled' : ''}><i class="fas fa-skull me-2"></i>Kill</button></li>
<li><button type="button" class="dropdown-item action-pause" ${container.State !== 'running' ? 'disabled' : ''}><i class="fas fa-pause me-2"></i>Pause</button></li>
<li><button type="button" class="dropdown-item action-top" ${container.State !== 'running' ? 'disabled' : ''}><i class="fas fa-microchip me-2"></i>Processes</button></li>
<li><hr class="dropdown-divider"></li>
<li><button type="button" class="dropdown-item action-inspect"><i class="fas fa-info-circle me-2"></i>Inspect</button></li>
<li><button type="button" class="dropdown-item action-duplicate"><i class="fas fa-clone me-2"></i>Duplicate</button></li>
<li><button type="button" class="dropdown-item action-rename"><i class="fas fa-edit me-2"></i>Rename</button></li>
<li><button type="button" class="dropdown-item action-recreate" data-min-role="admin"><i class="fas fa-rotate me-2"></i>Recreate</button></li>
<li><button type="button" class="dropdown-item action-resources" data-min-role="operator"><i class="fas fa-sliders me-2"></i>Resources</button></li>
<li><hr class="dropdown-divider"></li>
<li><button type="button" class="dropdown-item text-danger action-remove"><i class="fas fa-trash me-2"></i>Remove</button></li>
</ul>
</div>
</div>
</td>
@@ -7553,19 +7531,6 @@ function buildContainerRow(container) {
const checkbox = row.querySelector('.container-checkbox');
if (checkbox) checkbox.addEventListener('change', () => updateBulkActionsToolbar());
const duplicateBtn = row.querySelector('.action-duplicate');
if (duplicateBtn) duplicateBtn.addEventListener('click', () => openDuplicateModal(container));
const recreateBtn = row.querySelector('.action-recreate');
if (recreateBtn) {
recreateBtn.addEventListener('click', () => recreateContainerAction(container));
}
const resourcesBtn = row.querySelector('.action-resources');
if (resourcesBtn) {
resourcesBtn.addEventListener('click', () => {
const name = (container.Names?.[0] || '').replace(/^\//, '');
window.peardockOps?.openResourceEditor?.(container.Id, { name });
});
}
const nameLink = row.querySelector('.container-name-link');
if (nameLink) {
nameLink.addEventListener('click', (e) => {
@@ -7580,209 +7545,279 @@ function buildContainerRow(container) {
showContainerDetails(container);
});
}
const moreBtn = row.querySelector('.action-more');
if (moreBtn) {
moreBtn.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
openContainerActionsModal(container);
});
}
addActionListeners(row, container);
return row;
}
/**
* Portaled container menu lives on document.body so it:
* - never expands table overflow / scrollbars
* - never sits under following rows
* - does not raise the whole <tr> (which stole mouse events over lower rows)
* Container "More actions" modal (replaces fragile dropdown menus).
* @type {{ container: object|null }}
*/
const containerActionsMenu = {
open: false,
toggle: null,
menu: null,
homeParent: null,
homeNext: null,
};
const containerActionsModalState = { container: null };
function positionPortaledContainerMenu(toggleBtn, menu) {
const rect = toggleBtn.getBoundingClientRect();
const pad = 6;
// Measure after show for flip-above if needed
menu.style.position = 'fixed';
menu.style.zIndex = '10050';
menu.style.inset = 'auto';
menu.style.transform = 'none';
menu.style.margin = '0';
menu.style.display = 'block';
menu.style.visibility = 'hidden';
// provisional place
menu.style.top = `${Math.round(rect.bottom + pad)}px`;
menu.style.left = '0px';
menu.style.right = 'auto';
const mw = menu.offsetWidth || 184;
const mh = menu.offsetHeight || 280;
let top = rect.bottom + pad;
if (top + mh > window.innerHeight - 8 && rect.top - pad - mh > 8) {
top = rect.top - pad - mh;
}
let left = rect.right - mw;
if (left < 8) left = 8;
if (left + mw > window.innerWidth - 8) left = Math.max(8, window.innerWidth - 8 - mw);
menu.style.top = `${Math.round(top)}px`;
menu.style.left = `${Math.round(left)}px`;
menu.style.visibility = 'visible';
}
function restorePortaledContainerMenu() {
const st = containerActionsMenu;
if (!st.menu) return;
const menu = st.menu;
menu.classList.remove('show');
menu.style.cssText = '';
// Return to original parent if still in DOM
if (st.homeParent && st.homeParent.isConnected) {
if (st.homeNext && st.homeNext.parentNode === st.homeParent) {
st.homeParent.insertBefore(menu, st.homeNext);
} else {
st.homeParent.appendChild(menu);
}
} else {
menu.remove();
}
if (st.toggle) {
st.toggle.setAttribute('aria-expanded', 'false');
st.toggle.classList.remove('show');
st.toggle.closest('.dropdown')?.classList.remove('show');
}
st.open = false;
st.toggle = null;
st.menu = null;
st.homeParent = null;
st.homeNext = null;
/** No-op: kept so list reconcile still has a safe close hook. */
function closeContainerActionMenus() {
containerVirt.menuOpen = false;
document.querySelectorAll('#container-list tr.row-menu-open').forEach((tr) => {
tr.classList.remove('row-menu-open');
});
document.body.classList.remove('pd-container-menu-open');
}
function openPortaledContainerMenu(toggleBtn) {
if (!toggleBtn) return;
// Same toggle → close
if (containerActionsMenu.open && containerActionsMenu.toggle === toggleBtn) {
restorePortaledContainerMenu();
return;
}
// Different row → close previous first
if (containerActionsMenu.open) restorePortaledContainerMenu();
let menu = toggleBtn.nextElementSibling;
if (!menu?.classList?.contains('dropdown-menu')) {
menu = toggleBtn.parentElement?.querySelector('.dropdown-menu');
}
if (!menu) return;
containerActionsMenu.homeParent = menu.parentElement;
containerActionsMenu.homeNext = menu.nextSibling;
containerActionsMenu.toggle = toggleBtn;
containerActionsMenu.menu = menu;
containerActionsMenu.open = true;
containerVirt.menuOpen = true;
menu.classList.add('container-actions-dropdown-menu', 'show', 'pd-portaled-actions-menu');
document.body.appendChild(menu);
document.body.classList.add('pd-container-menu-open');
toggleBtn.setAttribute('aria-expanded', 'true');
toggleBtn.classList.add('show');
toggleBtn.closest('.dropdown')?.classList.add('show');
positionPortaledContainerMenu(toggleBtn, menu);
}
/** Close any open container action menus before DOM moves. */
function closeContainerActionMenus(_root) {
restorePortaledContainerMenu();
// Belt-and-suspenders: hide any leftover shown menus in the table
try {
document.querySelectorAll?.('.container-actions-dropdown-menu.show, .pd-portaled-actions-menu').forEach((menu) => {
menu.classList.remove('show', 'pd-portaled-actions-menu');
menu.style.cssText = '';
});
document.querySelectorAll?.('.container-actions-more-toggle[aria-expanded="true"]').forEach((btn) => {
btn.setAttribute('aria-expanded', 'false');
btn.classList.remove('show');
btn.closest('.dropdown')?.classList.remove('show');
});
} catch {
// ignore
}
containerVirt.menuOpen = false;
document.body.classList.remove('pd-container-menu-open');
}
/**
* Click handler for toggles does not use Bootstrap Dropdown (avoids table
* overflow, multi-row hover glitches, and Popper/table fights).
* @param {HTMLElement} toggleBtn
* Open the designed more-actions modal for a container.
* @param {object} container
*/
function initContainerActionsDropdown(toggleBtn) {
// no-op retained for call sites; real work is delegated click below
return toggleBtn || null;
}
// Capture-phase: open portaled menu; block Bootstrap's data-api if present
document.addEventListener(
'click',
(e) => {
const btn = e.target?.closest?.('.container-actions-more-toggle');
if (btn) {
e.preventDefault();
e.stopPropagation();
openPortaledContainerMenu(btn);
function openContainerActionsModal(container) {
if (!container) return;
const modalEl = document.getElementById('containerActionsModal');
if (!modalEl || typeof bootstrap === 'undefined') {
console.warn('[WARN] containerActionsModal missing');
return;
}
// Click inside portaled menu — let item handlers run, then close
const inMenu = e.target?.closest?.('.pd-portaled-actions-menu, .container-actions-dropdown-menu.show');
if (inMenu && containerActionsMenu.open) {
// Allow the action button click to process; close on next tick
const item = e.target?.closest?.('.dropdown-item, button, a');
if (item && !item.disabled) {
setTimeout(() => restorePortaledContainerMenu(), 0);
}
return;
}
// Outside click closes
if (containerActionsMenu.open) {
restorePortaledContainerMenu();
}
},
true
);
// Reposition on resize; close on scroll of the table so menu doesn't float wrong
window.addEventListener(
'resize',
() => {
if (containerActionsMenu.open && containerActionsMenu.toggle && containerActionsMenu.menu) {
positionPortaledContainerMenu(containerActionsMenu.toggle, containerActionsMenu.menu);
containerActionsModalState.container = container;
containerVirt.menuOpen = true;
const name = (container.Names?.[0] || '').replace(/^\//, '') || container.Id?.slice(0, 12) || 'Container';
const state = container.State || 'unknown';
const running = String(state).toLowerCase() === 'running';
const image = formatImageName(container.Image || '—');
const shortId = (container.Id || '').slice(0, 12);
const titleEl = document.getElementById('containerActionsModalLabel');
if (titleEl) titleEl.textContent = name;
const subtitle = document.getElementById('cam-subtitle');
if (subtitle) {
subtitle.innerHTML = `
<span class="cam-id mono">${escapeHtmlLite(shortId)}</span>
<span class="cam-sep">·</span>
<span class="badge ${containerStatusClass(state)}">${escapeHtmlLite(state)}</span>
<span class="cam-sep">·</span>
<span class="cam-image text-muted">${escapeHtmlLite(image)}</span>
`;
}
},
{ passive: true }
);
document.addEventListener(
'scroll',
(e) => {
if (!containerActionsMenu.open) return;
// Ignore scrolls inside the portaled menu itself
if (e.target?.closest?.('.pd-portaled-actions-menu')) return;
restorePortaledContainerMenu();
},
true
);
// Enable/disable state-sensitive actions
modalEl.querySelectorAll('[data-cam-need-running]').forEach((btn) => {
btn.disabled = !running;
});
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && containerActionsMenu.open) {
restorePortaledContainerMenu();
const modal = bootstrap.Modal.getOrCreateInstance(modalEl);
modal.show();
}
function hideContainerActionsModal() {
const modalEl = document.getElementById('containerActionsModal');
if (!modalEl || typeof bootstrap === 'undefined') return;
bootstrap.Modal.getInstance(modalEl)?.hide();
}
function bindContainerActionsModalOnce() {
const modalEl = document.getElementById('containerActionsModal');
if (!modalEl || modalEl.dataset.bound === '1') return;
modalEl.dataset.bound = '1';
modalEl.addEventListener('hidden.bs.modal', () => {
containerActionsModalState.container = null;
containerVirt.menuOpen = false;
if (containerVirt.rows.length > 80) {
requestAnimationFrame(() => paintVirtualContainers());
}
});
modalEl.addEventListener('click', async (e) => {
const btn = e.target?.closest?.('[data-cam-action]');
if (!btn || btn.disabled) return;
const action = btn.getAttribute('data-cam-action');
const container = containerActionsModalState.container;
if (!container || !action) return;
const name = (container.Names?.[0] || '').replace(/^\//, '') || container.Id;
const hide = () => hideContainerActionsModal();
try {
switch (action) {
case 'restart': {
hide();
showStatusIndicator(`Restarting container "${name}"...`);
sendCommand('restartContainer', { id: container.Id });
try {
const response = await waitForPeerResponse(`Container ${container.Id} restarted`);
showAlert('success', response.message);
sendCommand('listContainers');
} catch (err) {
showAlert('danger', err.message || 'Failed to restart container.');
} finally {
hideStatusIndicator();
}
break;
}
case 'kill': {
hide();
showStatusIndicator(`Killing container "${name}"...`);
try {
const response = await sendCommand('killContainer', { id: container.Id });
if (response?.success) {
showAlert('success', response.message || 'Container killed');
sendCommand('listContainers');
}
} catch (err) {
showAlert('danger', err.message || 'Failed to kill container.');
} finally {
hideStatusIndicator();
}
break;
}
case 'pause': {
hide();
showStatusIndicator(`Pausing container "${name}"...`);
sendCommand('pauseContainer', { id: container.Id });
try {
const response = await waitForPeerResponse(`Container ${container.Id} paused`);
showAlert('success', response.message);
sendCommand('listContainers');
} catch (err) {
showAlert('danger', err.message || 'Failed to pause container.');
} finally {
hideStatusIndicator();
}
break;
}
case 'processes': {
hide();
showContainerDetails(container);
setTimeout(() => {
document.getElementById('processes-tab')?.click();
if (typeof loadContainerTop === 'function') loadContainerTop(container.Id);
}, 200);
break;
}
case 'inspect': {
hide();
showContainerDetails(container);
break;
}
case 'duplicate': {
hide();
openDuplicateModal(container);
break;
}
case 'rename': {
hide();
// Prefer dedicated rename modal when present
const renameModalEl = document.getElementById('renameContainerModal');
const input = document.getElementById('new-container-name');
if (renameModalEl && input && typeof bootstrap !== 'undefined') {
input.value = name;
input.dataset.containerId = container.Id;
bootstrap.Modal.getOrCreateInstance(renameModalEl).show();
const confirmBtn = document.getElementById('confirm-rename-btn');
if (confirmBtn) {
confirmBtn.onclick = async () => {
const newName = input.value.trim();
if (!newName) {
showAlert('danger', 'Container name cannot be empty');
return;
}
if (newName === name) {
bootstrap.Modal.getInstance(renameModalEl)?.hide();
return;
}
sendCommand('renameContainer', { id: container.Id, name: newName });
bootstrap.Modal.getInstance(renameModalEl)?.hide();
showStatusIndicator(`Renaming to "${newName}"...`);
try {
const response = await waitForPeerResponse(`Container renamed to "${newName}"`);
showAlert('success', response.message || `Renamed to ${newName}`);
sendCommand('listContainers');
} catch (err) {
showAlert('danger', err.message || 'Rename failed');
} finally {
hideStatusIndicator();
}
};
}
} else {
showAlert('info', 'Rename is unavailable in this build.');
}
break;
}
case 'recreate': {
hide();
if (typeof recreateContainerAction === 'function') recreateContainerAction(container);
break;
}
case 'resources': {
hide();
window.peardockOps?.openResourceEditor?.(container.Id, { name });
break;
}
case 'remove': {
hide();
const deleteModalEl = document.getElementById('deleteModal');
if (deleteModalEl && typeof bootstrap !== 'undefined') {
const deleteModal = bootstrap.Modal.getOrCreateInstance(deleteModalEl);
deleteModal.show();
const confirmDeleteBtn = document.getElementById('confirm-delete-btn');
if (confirmDeleteBtn) {
confirmDeleteBtn.onclick = async () => {
deleteModal.hide();
if (typeof closeAllModals === 'function') closeAllModals();
notificationManager?.add?.('info', `Deleting container "${name}"...`, {
autoDismiss: false,
});
showStatusIndicator(`Deleting container "${name}"...`);
if (window.openTerminals?.[container.Id]) {
window.openTerminals[container.Id].forEach((terminalId) => {
try {
cleanUpTerminal(terminalId);
} catch {
// ignore
}
});
delete window.openTerminals[container.Id];
}
sendCommand('removeContainer', { id: container.Id });
try {
const response = await waitForPeerResponse(
`Container ${container.Id} removed`,
30000
);
showAlert('success', response.message || 'Container removed');
sendCommand('listContainers');
} catch (err) {
showAlert('danger', err.message || 'Failed to remove container.');
} finally {
hideStatusIndicator();
}
};
}
}
break;
}
default:
break;
}
} catch (err) {
showAlert('danger', err.message || 'Action failed');
hideStatusIndicator();
}
});
}
// Bind modal once DOM is ready (module scripts often load after DOMContentLoaded)
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', bindContainerActionsModalOnce);
} else {
bindContainerActionsModalOnce();
}
/**
* In-place DOM reconcile never clears tbody (no blank frame / disappear flash).
* Only removes rows for containers that are truly gone; patches the rest.
@@ -7791,8 +7826,8 @@ document.addEventListener('keydown', (e) => {
function reconcileContainerRows(listElement, containers) {
if (!listElement) return;
// Menus with Popper fixed strategy desync when rows move — close first
closeContainerActionMenus(listElement);
// Close any open more-actions UI before DOM moves
closeContainerActionMenus();
// Drop skeleton / empty-state rows when we have real data
if (containers.length) {
@@ -8040,7 +8075,8 @@ function addActionListeners(row, container) {
const killBtn = row.querySelector('.action-kill');
const topBtn = row.querySelector('.action-top');
// Start Button
// Start Button (primary row actions only — more actions use the modal)
if (!startBtn) return;
startBtn.addEventListener('click', async () => {
showStatusIndicator(`Starting container "${container.Names[0]}"...`);
sendCommand('startContainer', { id: container.Id });
@@ -8068,6 +8104,7 @@ function addActionListeners(row, container) {
});
if (stopBtn) {
stopBtn.addEventListener('click', async () => {
showStatusIndicator(`Stopping container "${container.Names[0]}"...`);
sendCommand('stopContainer', { id: container.Id });
@@ -8093,10 +8130,10 @@ function addActionListeners(row, container) {
hideStatusIndicator();
}
});
}
// Restart Button
// Restart / kill / pause / etc. live in the more-actions modal when not on the row
if (restartBtn) {
restartBtn.addEventListener('click', async () => {
showStatusIndicator(`Restarting container "${container.Names[0]}"...`);
sendCommand('restartContainer', { id: container.Id });
@@ -8119,6 +8156,7 @@ function addActionListeners(row, container) {
hideStatusIndicator();
}
});
}
if (killBtn) {
killBtn.addEventListener('click', async () => {
@@ -8328,7 +8366,7 @@ function addActionListeners(row, container) {
}
const logsBtn = row.querySelector('.action-logs');
logsBtn.addEventListener('click', () => openLogModal(container.Id));
if (logsBtn) logsBtn.addEventListener('click', () => openLogModal(container.Id));
function openLogModal(containerId) {
console.log(`[INFO] Opening logs modal for container: ${containerId}`);
@@ -8359,8 +8397,8 @@ function addActionListeners(row, container) {
modal.show();
}
// Remove Button
removeBtn.addEventListener('click', async () => {
// Remove Button (optional — often only on more-actions modal)
if (removeBtn) removeBtn.addEventListener('click', async () => {
const deleteModal = new bootstrap.Modal(document.getElementById('deleteModal'));
deleteModal.show();
@@ -8427,7 +8465,7 @@ function addActionListeners(row, container) {
};
});
if (terminalBtn) {
terminalBtn.addEventListener('click', () => {
console.log(`[DEBUG] Opening terminal for container ID: ${container.Id}`);
try {
@@ -8437,7 +8475,7 @@ function addActionListeners(row, container) {
showAlert('danger', `Failed to start terminal: ${error.message}`);
}
});
}
// Inspect Button
if (inspectBtn) {
inspectBtn.addEventListener('click', () => {
+101
View File
@@ -3821,6 +3821,107 @@ services:
</div>
</div>
<!-- Container more-actions modal (replaces ⋮ context menu) -->
<div class="modal fade" id="containerActionsModal" tabindex="-1" aria-labelledby="containerActionsModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content cam-modal bg-dark text-white">
<div class="modal-header cam-header border-0 pb-0">
<div class="cam-header-text">
<h5 class="modal-title mb-1" id="containerActionsModalLabel">Container</h5>
<div id="cam-subtitle" class="cam-subtitle small"></div>
</div>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body cam-body pt-3">
<div class="cam-section-label">Lifecycle</div>
<div class="cam-grid">
<button type="button" class="cam-action" data-cam-action="restart" data-cam-need-running>
<span class="cam-action-icon"><i class="fas fa-redo"></i></span>
<span class="cam-action-text">
<strong>Restart</strong>
<small>Stop then start the container</small>
</span>
</button>
<button type="button" class="cam-action" data-cam-action="pause" data-cam-need-running>
<span class="cam-action-icon"><i class="fas fa-pause"></i></span>
<span class="cam-action-text">
<strong>Pause</strong>
<small>Freeze all processes</small>
</span>
</button>
<button type="button" class="cam-action cam-action-warn" data-cam-action="kill" data-cam-need-running>
<span class="cam-action-icon"><i class="fas fa-skull"></i></span>
<span class="cam-action-text">
<strong>Kill</strong>
<small>Force stop (SIGKILL)</small>
</span>
</button>
<button type="button" class="cam-action" data-cam-action="processes" data-cam-need-running>
<span class="cam-action-icon"><i class="fas fa-microchip"></i></span>
<span class="cam-action-text">
<strong>Processes</strong>
<small>View running process list</small>
</span>
</button>
</div>
<div class="cam-section-label mt-3">Manage</div>
<div class="cam-grid">
<button type="button" class="cam-action" data-cam-action="inspect">
<span class="cam-action-icon"><i class="fas fa-info-circle"></i></span>
<span class="cam-action-text">
<strong>Inspect</strong>
<small>Details, logs &amp; config</small>
</span>
</button>
<button type="button" class="cam-action" data-cam-action="duplicate">
<span class="cam-action-icon"><i class="fas fa-clone"></i></span>
<span class="cam-action-text">
<strong>Duplicate</strong>
<small>Create from this config</small>
</span>
</button>
<button type="button" class="cam-action" data-cam-action="rename">
<span class="cam-action-icon"><i class="fas fa-edit"></i></span>
<span class="cam-action-text">
<strong>Rename</strong>
<small>Change container name</small>
</span>
</button>
<button type="button" class="cam-action" data-cam-action="recreate" data-min-role="admin">
<span class="cam-action-icon"><i class="fas fa-rotate"></i></span>
<span class="cam-action-text">
<strong>Recreate</strong>
<small>Replace with same settings</small>
</span>
</button>
<button type="button" class="cam-action" data-cam-action="resources" data-min-role="operator">
<span class="cam-action-icon"><i class="fas fa-sliders"></i></span>
<span class="cam-action-text">
<strong>Resources</strong>
<small>CPU &amp; memory limits</small>
</span>
</button>
</div>
<div class="cam-section-label mt-3">Danger zone</div>
<div class="cam-grid">
<button type="button" class="cam-action cam-action-danger" data-cam-action="remove">
<span class="cam-action-icon"><i class="fas fa-trash"></i></span>
<span class="cam-action-text">
<strong>Remove</strong>
<small>Delete this container</small>
</span>
</button>
</div>
</div>
<div class="modal-footer border-0 pt-0">
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
<!-- Rename Container Modal -->
<div class="modal fade" id="renameContainerModal" tabindex="-1" aria-labelledby="renameContainerModalLabel" aria-hidden="true">
<div class="modal-dialog">
+131 -47
View File
@@ -2363,7 +2363,7 @@ textarea::placeholder {
display: none !important;
}
/* Container action overflow */
/* Container action buttons */
.container-actions {
display: inline-flex;
align-items: center;
@@ -2371,7 +2371,6 @@ textarea::placeholder {
gap: 4px;
flex-wrap: nowrap;
max-width: 100%;
position: relative;
}
.container-actions-primary {
@@ -2382,59 +2381,19 @@ textarea::placeholder {
.container-actions-cell {
white-space: nowrap;
width: 10.5rem;
max-width: 10.5rem;
width: 11rem;
max-width: 11rem;
overflow: hidden;
position: relative;
text-align: right;
}
.container-actions-more {
position: relative;
}
.container-actions .btn {
flex: 0 0 auto;
}
/* In-table menus stay hidden — open menu is portaled to body */
.container-actions .dropdown-menu:not(.pd-portaled-actions-menu),
.container-actions-dropdown-menu:not(.pd-portaled-actions-menu) {
display: none !important;
}
/*
* Portaled ⋮ menu (appended to <body> while open).
* position:fixed + high z-index — never expands table overflow/scrollbars.
*/
.pd-portaled-actions-menu,
.dropdown-menu.pd-portaled-actions-menu,
.container-actions-dropdown-menu.pd-portaled-actions-menu {
position: fixed !important;
z-index: 10050 !important;
min-width: 11.5rem;
max-height: min(70vh, 28rem);
overflow-y: auto;
overflow-x: hidden;
font-size: 13px;
display: block !important;
visibility: visible !important;
pointer-events: auto !important;
margin: 0 !important;
inset: auto !important;
transform: none !important;
box-shadow: var(--shadow-lg);
border: 1px solid var(--border-strong);
background: var(--bg-elevated) !important;
color: var(--text-primary);
}
/* Containers table shell — never unlock overflow when menu open */
#containers-view .table-responsive {
overflow-x: hidden !important;
overflow-y: auto;
position: relative;
z-index: 1;
max-width: 100%;
width: 100%;
}
@@ -2448,11 +2407,136 @@ textarea::placeholder {
overflow: hidden;
}
#containers-view #container-list > tr > td.container-actions-cell {
overflow: hidden;
/* ── Container more-actions modal ── */
.cam-modal {
border: 1px solid var(--border-strong);
border-radius: 16px;
background: var(--bg-elevated) !important;
box-shadow: var(--shadow-lg);
}
/* Do NOT elevate open rows — that stole mouse events over rows below */
.cam-header {
align-items: flex-start;
}
.cam-header-text {
min-width: 0;
padding-right: 0.5rem;
}
.cam-subtitle {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.35rem 0.4rem;
color: var(--text-secondary);
line-height: 1.4;
}
.cam-subtitle .mono {
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
font-size: 12px;
color: var(--text-muted);
}
.cam-sep {
opacity: 0.45;
}
.cam-image {
max-width: 14rem;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.cam-section-label {
font-size: 11px;
font-weight: 600;
letter-spacing: 0.06em;
text-transform: uppercase;
color: var(--text-muted);
margin-bottom: 0.5rem;
}
.cam-grid {
display: grid;
grid-template-columns: 1fr;
gap: 0.45rem;
}
@media (min-width: 520px) {
.cam-grid {
grid-template-columns: 1fr 1fr;
}
}
.cam-action {
display: flex;
align-items: flex-start;
gap: 0.75rem;
width: 100%;
text-align: left;
padding: 0.75rem 0.85rem;
border-radius: 12px;
border: 1px solid var(--border-color);
background: var(--bg-surface, rgba(255, 255, 255, 0.03));
color: var(--text-primary);
transition: background 0.15s ease, border-color 0.15s ease, transform 0.12s ease;
cursor: pointer;
}
.cam-action:hover:not(:disabled) {
background: var(--bg-hover);
border-color: var(--border-strong);
transform: translateY(-1px);
}
.cam-action:disabled {
opacity: 0.4;
cursor: not-allowed;
}
.cam-action-icon {
flex: 0 0 auto;
width: 2rem;
height: 2rem;
border-radius: 10px;
display: inline-flex;
align-items: center;
justify-content: center;
background: rgba(56, 189, 248, 0.12);
color: var(--accent-info, #38bdf8);
font-size: 0.9rem;
}
.cam-action-warn .cam-action-icon {
background: rgba(251, 191, 36, 0.14);
color: var(--accent-warning, #fbbf24);
}
.cam-action-danger .cam-action-icon {
background: rgba(248, 113, 113, 0.14);
color: var(--accent-danger, #f87171);
}
.cam-action-text {
display: flex;
flex-direction: column;
gap: 0.15rem;
min-width: 0;
}
.cam-action-text strong {
font-size: 13px;
font-weight: 600;
}
.cam-action-text small {
font-size: 11px;
color: var(--text-muted);
line-height: 1.3;
}
/* Accordion bodies must stay collapsed when not .show (defensive vs BS CDN lag) */
.accordion-collapse.collapse:not(.show) {