diff --git a/web/static/css/style.css b/web/static/css/style.css index e85c9b6f..30022588 100644 --- a/web/static/css/style.css +++ b/web/static/css/style.css @@ -36249,16 +36249,88 @@ html[data-theme="dark"] .form-group select { position: relative; } -.rbac-summary-toggle { - margin-left: 4px; - vertical-align: middle; +.rbac-summary-count { + display: block; + margin-top: 3px; + color: var(--text-primary); + font-size: 0.9375rem; + font-weight: 700; + line-height: 1.2; } -.rbac-effective-permissions { - margin: 0 16px 12px; +.rbac-summary-count--interactive { + display: inline-flex; + align-items: center; + margin-top: 3px; + padding: 0 0 1px; + border: none; + border-bottom: 1px dashed color-mix(in srgb, var(--text-secondary) 55%, transparent); + border-radius: 0; + background: none; + color: var(--text-secondary); + font-size: 0.9375rem; + font-weight: 700; + line-height: 1.2; + cursor: pointer; + transition: color 0.15s ease, border-color 0.15s ease; +} + +.rbac-summary-count--interactive:hover { + color: var(--text-primary); + border-bottom-color: color-mix(in srgb, var(--text-secondary) 80%, transparent); +} + +.rbac-summary-count--interactive[aria-expanded="true"] { + color: var(--text-primary); + border-bottom-color: var(--text-secondary); + border-bottom-style: solid; +} + +.rbac-summary-count--interactive:focus-visible { + outline: 2px solid color-mix(in srgb, var(--text-secondary) 35%, transparent); + outline-offset: 3px; +} + +.rbac-permissions-popover { + position: fixed; + z-index: 220; + width: min(380px, calc(100vw - 32px)); +} + +.rbac-permissions-popover[hidden] { + display: none !important; +} + +.rbac-permissions-popover-inner { + position: relative; + overflow: hidden; border: 1px solid var(--border-color); - border-radius: 8px; + border-radius: 10px; background: var(--bg-primary); + box-shadow: var(--shadow-md, 0 8px 24px rgba(15, 23, 42, 0.12)); +} + +.rbac-permissions-popover-inner::before { + content: ''; + position: absolute; + top: -5px; + left: var(--rbac-popover-arrow-left, 18px); + width: 10px; + height: 10px; + background: var(--bg-primary); + border-left: 1px solid var(--border-color); + border-top: 1px solid var(--border-color); + transform: rotate(45deg); +} + +.rbac-permissions-popover-count { + color: var(--text-muted); + font-size: 0.75rem; + font-weight: 500; +} + +.rbac-permissions-popover .rbac-effective-permissions-body { + max-height: min(320px, calc(100vh - 180px)); } .rbac-effective-permissions-head, @@ -36744,6 +36816,11 @@ html[data-theme="dark"] .form-group select { font-size: 0.875rem; } +.rbac-panel-detail .rbac-selected-summary .rbac-summary-count--interactive { + margin-top: 2px; + font-size: 0.875rem; +} + .rbac-panel-detail .rbac-detail-head { min-height: 0; padding: 8px 14px; diff --git a/web/static/js/rbac.js b/web/static/js/rbac.js index cefe0931..43b840ac 100644 --- a/web/static/js/rbac.js +++ b/web/static/js/rbac.js @@ -251,43 +251,120 @@ function resolveUserEffectiveAccess(user) { return { userRoles, effectivePermissions, permissionsByModule, effectiveScope, assignmentCount }; } -function toggleRbacEffectivePermissions() { - rbacState.showEffectivePermissions = !rbacState.showEffectivePermissions; - renderRbacEffectivePermissions(selectedRbacUser()); +let rbacPermissionsPopoverDocBound = false; + +function positionRbacPermissionsPopover() { + const popover = document.getElementById('rbac-permissions-popover'); + const btn = document.querySelector('.rbac-summary-count--interactive'); + if (!popover || !btn || popover.hidden) return; + const rect = btn.getBoundingClientRect(); + const width = Math.min(380, window.innerWidth - 32); + let left = rect.left; + if (left + width > window.innerWidth - 16) left = window.innerWidth - width - 16; + left = Math.max(16, left); + const top = Math.min(rect.bottom + 8, window.innerHeight - 16); + popover.style.width = `${width}px`; + popover.style.left = `${left}px`; + popover.style.top = `${top}px`; + const arrowLeft = Math.min(Math.max(rect.left + rect.width / 2 - left - 5, 12), width - 22); + popover.style.setProperty('--rbac-popover-arrow-left', `${arrowLeft}px`); } -function renderRbacEffectivePermissions(user) { - const box = document.getElementById('rbac-effective-permissions'); - if (!box) return; - if (!user || !rbacState.showEffectivePermissions) { - box.hidden = true; - box.innerHTML = ''; - return; +function onRbacPermissionsPopoverReposition() { + if (rbacState.showEffectivePermissions) positionRbacPermissionsPopover(); +} + +function setRbacPermissionsPopoverOpen(open) { + rbacState.showEffectivePermissions = open; + const popover = document.getElementById('rbac-permissions-popover'); + const btn = document.querySelector('.rbac-summary-count--interactive'); + if (popover) popover.hidden = !open; + if (btn) btn.setAttribute('aria-expanded', open ? 'true' : 'false'); + if (open) { + requestAnimationFrame(() => positionRbacPermissionsPopover()); } +} + +function closeRbacPermissionsPopover() { + setRbacPermissionsPopoverOpen(false); +} + +function toggleRbacEffectivePermissions(ev) { + if (ev) { + ev.preventDefault(); + ev.stopPropagation(); + } + const opening = !rbacState.showEffectivePermissions; + if (opening) renderRbacEffectivePermissionsContent(selectedRbacUser()); + setRbacPermissionsPopoverOpen(opening); +} + +function onRbacPermissionsPopoverPointerDown(ev) { + if (!rbacState.showEffectivePermissions) return; + const popover = document.getElementById('rbac-permissions-popover'); + const btn = document.querySelector('.rbac-summary-count--interactive'); + if (popover && popover.contains(ev.target)) return; + if (btn && btn.contains(ev.target)) return; + closeRbacPermissionsPopover(); +} + +function bindRbacPermissionsPopoverDismiss() { + if (rbacPermissionsPopoverDocBound) return; + rbacPermissionsPopoverDocBound = true; + document.addEventListener('mousedown', onRbacPermissionsPopoverPointerDown); + document.addEventListener('keydown', ev => { + if (ev.key === 'Escape' && rbacState.showEffectivePermissions) closeRbacPermissionsPopover(); + }); + window.addEventListener('resize', onRbacPermissionsPopoverReposition); + window.addEventListener('scroll', onRbacPermissionsPopoverReposition, true); +} + +function renderRbacEffectivePermissionsContent(user) { + const popover = document.getElementById('rbac-permissions-popover'); + if (!popover || !user) return; const access = resolveUserEffectiveAccess(user); const modules = Object.keys(access.permissionsByModule).sort(); if (!modules.length) { - box.hidden = false; - box.innerHTML = `