567 lines
35 KiB
HTML
567 lines
35 KiB
HTML
|
|
|
||
|
|
<style>
|
||
|
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||
|
|
body { font-family: var(--font-sans, sans-serif); background: transparent; }
|
||
|
|
|
||
|
|
.app { display: grid; grid-template-columns: 200px 1fr; grid-template-rows: 48px 1fr; height: 640px; border: 0.5px solid var(--color-border-tertiary); border-radius: var(--border-radius-lg); overflow: hidden; }
|
||
|
|
|
||
|
|
/* Top bar */
|
||
|
|
.topbar { grid-column: 1 / -1; display: flex; align-items: center; justify-content: space-between; padding: 0 20px; border-bottom: 0.5px solid var(--color-border-tertiary); background: var(--color-background-primary); }
|
||
|
|
.topbar-brand { display: flex; align-items: center; gap: 8px; font-size: 13px; font-weight: 500; color: var(--color-text-primary); }
|
||
|
|
.brand-dot { width: 8px; height: 8px; border-radius: 50%; background: #1D9E75; }
|
||
|
|
.topbar-actions { display: flex; align-items: center; gap: 8px; }
|
||
|
|
.badge-env { font-size: 11px; padding: 2px 8px; border-radius: 4px; background: var(--color-background-secondary); color: var(--color-text-secondary); border: 0.5px solid var(--color-border-tertiary); }
|
||
|
|
|
||
|
|
/* Sidebar */
|
||
|
|
.sidebar { background: var(--color-background-secondary); border-right: 0.5px solid var(--color-border-tertiary); padding: 12px 0; overflow-y: auto; }
|
||
|
|
.sidebar-section { margin-bottom: 20px; }
|
||
|
|
.sidebar-label { font-size: 10px; font-weight: 500; color: var(--color-text-tertiary); text-transform: uppercase; letter-spacing: 0.08em; padding: 0 16px; margin-bottom: 4px; }
|
||
|
|
.nav-item { display: flex; align-items: center; gap: 8px; padding: 7px 16px; font-size: 13px; color: var(--color-text-secondary); cursor: pointer; transition: background 0.1s; border-left: 2px solid transparent; }
|
||
|
|
.nav-item:hover { background: var(--color-background-primary); color: var(--color-text-primary); }
|
||
|
|
.nav-item.active { background: var(--color-background-primary); color: var(--color-text-primary); font-weight: 500; border-left-color: #1D9E75; }
|
||
|
|
.nav-icon { width: 14px; height: 14px; opacity: 0.6; flex-shrink: 0; }
|
||
|
|
.nav-item.active .nav-icon { opacity: 1; }
|
||
|
|
.nav-count { margin-left: auto; font-size: 11px; background: var(--color-background-tertiary); color: var(--color-text-tertiary); padding: 1px 6px; border-radius: 10px; }
|
||
|
|
|
||
|
|
/* Main content */
|
||
|
|
.main { background: var(--color-background-primary); overflow-y: auto; display: flex; flex-direction: column; }
|
||
|
|
|
||
|
|
/* Panels */
|
||
|
|
.panel { display: none; flex-direction: column; height: 100%; }
|
||
|
|
.panel.active { display: flex; }
|
||
|
|
|
||
|
|
/* Panel header */
|
||
|
|
.panel-header { display: flex; align-items: center; justify-content: space-between; padding: 16px 20px 12px; border-bottom: 0.5px solid var(--color-border-tertiary); flex-shrink: 0; }
|
||
|
|
.panel-title { font-size: 15px; font-weight: 500; color: var(--color-text-primary); }
|
||
|
|
.panel-subtitle { font-size: 12px; color: var(--color-text-tertiary); margin-top: 2px; }
|
||
|
|
.btn-add { font-size: 12px; padding: 6px 12px; border: 0.5px solid var(--color-border-secondary); border-radius: var(--border-radius-md); background: transparent; color: var(--color-text-primary); cursor: pointer; display: flex; align-items: center; gap: 4px; }
|
||
|
|
.btn-add:hover { background: var(--color-background-secondary); }
|
||
|
|
|
||
|
|
/* Search bar */
|
||
|
|
.search-bar { padding: 10px 20px; border-bottom: 0.5px solid var(--color-border-tertiary); flex-shrink: 0; }
|
||
|
|
.search-input { width: 100%; padding: 6px 10px; font-size: 12px; border: 0.5px solid var(--color-border-tertiary); border-radius: var(--border-radius-md); background: var(--color-background-secondary); color: var(--color-text-primary); outline: none; }
|
||
|
|
.search-input:focus { border-color: var(--color-border-primary); background: var(--color-background-primary); }
|
||
|
|
|
||
|
|
/* Entity list */
|
||
|
|
.entity-list { flex: 1; overflow-y: auto; }
|
||
|
|
.entity-item { display: flex; align-items: center; gap: 10px; padding: 10px 20px; border-bottom: 0.5px solid var(--color-border-tertiary); cursor: pointer; transition: background 0.1s; }
|
||
|
|
.entity-item:hover { background: var(--color-background-secondary); }
|
||
|
|
.entity-item.selected { background: var(--color-background-info); }
|
||
|
|
.avatar { width: 32px; height: 32px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 11px; font-weight: 500; flex-shrink: 0; }
|
||
|
|
.av-user { background: #E6F1FB; color: #0C447C; }
|
||
|
|
.av-group { background: #E1F5EE; color: #0F6E56; border-radius: 8px; }
|
||
|
|
.av-role { background: #FAEEDA; color: #633806; border-radius: 6px; }
|
||
|
|
.entity-info { flex: 1; min-width: 0; }
|
||
|
|
.entity-name { font-size: 13px; font-weight: 500; color: var(--color-text-primary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||
|
|
.entity-meta { font-size: 11px; color: var(--color-text-tertiary); margin-top: 1px; }
|
||
|
|
.tag-list { display: flex; gap: 4px; flex-wrap: wrap; margin-top: 4px; }
|
||
|
|
.tag { font-size: 10px; padding: 1px 6px; border-radius: 4px; }
|
||
|
|
.tag-role { background: #FAEEDA; color: #633806; }
|
||
|
|
.tag-group { background: #E1F5EE; color: #0F6E56; }
|
||
|
|
.tag-inherited { opacity: 0.65; font-style: italic; }
|
||
|
|
.status-dot { width: 6px; height: 6px; border-radius: 50%; flex-shrink: 0; }
|
||
|
|
.status-active { background: #1D9E75; }
|
||
|
|
.status-inactive { background: var(--color-border-secondary); }
|
||
|
|
|
||
|
|
/* Detail pane (right side of entity) */
|
||
|
|
.split { display: flex; flex: 1; overflow: hidden; }
|
||
|
|
.list-pane { width: 52%; border-right: 0.5px solid var(--color-border-tertiary); display: flex; flex-direction: column; overflow: hidden; }
|
||
|
|
.detail-pane { flex: 1; overflow-y: auto; padding: 16px 18px; }
|
||
|
|
|
||
|
|
.detail-empty { display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100%; color: var(--color-text-tertiary); font-size: 13px; gap: 8px; }
|
||
|
|
.detail-section { margin-bottom: 20px; }
|
||
|
|
.detail-section-title { font-size: 11px; font-weight: 500; text-transform: uppercase; letter-spacing: 0.07em; color: var(--color-text-tertiary); margin-bottom: 8px; display: flex; align-items: center; justify-content: space-between; }
|
||
|
|
.detail-section-title span { font-size: 10px; color: var(--color-text-tertiary); text-transform: none; letter-spacing: 0; }
|
||
|
|
.chip { display: inline-flex; align-items: center; gap: 4px; font-size: 11px; padding: 3px 8px; border-radius: 20px; border: 0.5px solid var(--color-border-tertiary); color: var(--color-text-secondary); background: var(--color-background-secondary); margin: 2px; }
|
||
|
|
.chip svg { width: 10px; height: 10px; opacity: 0.5; }
|
||
|
|
.chip.inherited { border-style: dashed; color: var(--color-text-tertiary); }
|
||
|
|
.chip-remove { cursor: pointer; opacity: 0.4; }
|
||
|
|
.chip-remove:hover { opacity: 0.9; }
|
||
|
|
.inherit-note { font-size: 11px; color: var(--color-text-tertiary); font-style: italic; margin-top: 6px; padding: 6px 8px; background: var(--color-background-secondary); border-radius: var(--border-radius-md); border-left: 2px solid #9FE1CB; }
|
||
|
|
.field-row { display: flex; align-items: center; gap: 10px; margin-bottom: 8px; }
|
||
|
|
.field-label { font-size: 11px; color: var(--color-text-tertiary); width: 70px; flex-shrink: 0; }
|
||
|
|
.field-val { font-size: 12px; color: var(--color-text-primary); }
|
||
|
|
.field-val.mono { font-family: var(--font-mono, monospace); font-size: 11px; color: var(--color-text-secondary); }
|
||
|
|
.detail-avatar { width: 44px; height: 44px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 15px; font-weight: 500; margin-bottom: 12px; }
|
||
|
|
.detail-name { font-size: 16px; font-weight: 500; color: var(--color-text-primary); margin-bottom: 4px; }
|
||
|
|
.detail-email { font-size: 12px; color: var(--color-text-secondary); margin-bottom: 12px; }
|
||
|
|
.divider { border: none; border-top: 0.5px solid var(--color-border-tertiary); margin: 12px 0; }
|
||
|
|
|
||
|
|
/* Breadcrumb tree */
|
||
|
|
.tree-row { display: flex; align-items: center; gap: 6px; padding: 5px 0; font-size: 12px; color: var(--color-text-secondary); }
|
||
|
|
.tree-indent { width: 16px; flex-shrink: 0; display: flex; justify-content: center; }
|
||
|
|
.tree-line { width: 1px; height: 16px; background: var(--color-border-tertiary); }
|
||
|
|
.tree-corner { width: 10px; height: 10px; border-left: 0.5px solid var(--color-border-tertiary); border-bottom: 0.5px solid var(--color-border-tertiary); border-bottom-left-radius: 2px; }
|
||
|
|
|
||
|
|
/* Overview panel */
|
||
|
|
.overview-grid { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 12px; padding: 16px 20px; }
|
||
|
|
.stat-card { background: var(--color-background-secondary); border-radius: var(--border-radius-md); padding: 14px; }
|
||
|
|
.stat-label { font-size: 11px; color: var(--color-text-tertiary); margin-bottom: 6px; }
|
||
|
|
.stat-value { font-size: 22px; font-weight: 500; color: var(--color-text-primary); line-height: 1; }
|
||
|
|
.stat-sub { font-size: 11px; color: var(--color-text-tertiary); margin-top: 4px; }
|
||
|
|
|
||
|
|
.inheritance-diagram { margin: 16px 20px 0; }
|
||
|
|
.inh-title { font-size: 11px; font-weight: 500; text-transform: uppercase; letter-spacing: 0.07em; color: var(--color-text-tertiary); margin-bottom: 10px; }
|
||
|
|
.inh-row { display: flex; align-items: flex-start; gap: 0; }
|
||
|
|
.inh-col { flex: 1; }
|
||
|
|
.inh-col-title { font-size: 11px; font-weight: 500; color: var(--color-text-secondary); margin-bottom: 6px; text-align: center; }
|
||
|
|
.inh-arrow { width: 40px; display: flex; align-items: center; justify-content: center; padding-top: 22px; color: var(--color-text-tertiary); font-size: 14px; }
|
||
|
|
.inh-item { font-size: 11px; padding: 4px 8px; border-radius: var(--border-radius-md); border: 0.5px solid var(--color-border-tertiary); margin-bottom: 4px; color: var(--color-text-secondary); background: var(--color-background-secondary); text-align: center; }
|
||
|
|
.inh-item.group { border-color: #9FE1CB; color: #0F6E56; background: #E1F5EE; }
|
||
|
|
.inh-item.role { border-color: #FAC775; color: #633806; background: #FAEEDA; }
|
||
|
|
.inh-item.user { border-color: #B5D4F4; color: #0C447C; background: #E6F1FB; }
|
||
|
|
|
||
|
|
/* Tabs */
|
||
|
|
.tabs { display: flex; gap: 0; border-bottom: 0.5px solid var(--color-border-tertiary); margin-bottom: 14px; }
|
||
|
|
.tab { font-size: 12px; padding: 6px 12px; cursor: pointer; color: var(--color-text-secondary); border-bottom: 2px solid transparent; margin-bottom: -0.5px; }
|
||
|
|
.tab.active { color: var(--color-text-primary); border-bottom-color: #1D9E75; font-weight: 500; }
|
||
|
|
</style>
|
||
|
|
|
||
|
|
<div class="app">
|
||
|
|
<!-- Top bar -->
|
||
|
|
<div class="topbar">
|
||
|
|
<div class="topbar-brand">
|
||
|
|
<div class="brand-dot"></div>
|
||
|
|
Monitor RBAC
|
||
|
|
</div>
|
||
|
|
<div class="topbar-actions">
|
||
|
|
<span class="badge-env">production</span>
|
||
|
|
<div style="width:24px;height:24px;border-radius:50%;background:#E6F1FB;display:flex;align-items:center;justify-content:center;font-size:10px;font-weight:500;color:#0C447C">A</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Sidebar -->
|
||
|
|
<nav class="sidebar">
|
||
|
|
<div class="sidebar-section">
|
||
|
|
<div class="sidebar-label">Overview</div>
|
||
|
|
<div class="nav-item" onclick="showPanel('overview')">
|
||
|
|
<svg class="nav-icon" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="2" y="2" width="5" height="5" rx="1"/><rect x="9" y="2" width="5" height="5" rx="1"/><rect x="2" y="9" width="5" height="5" rx="1"/><rect x="9" y="9" width="5" height="5" rx="1"/></svg>
|
||
|
|
Dashboard
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div class="sidebar-section">
|
||
|
|
<div class="sidebar-label">Identity</div>
|
||
|
|
<div class="nav-item active" id="nav-users" onclick="showPanel('users')">
|
||
|
|
<svg class="nav-icon" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="8" cy="5" r="3"/><path d="M2 14c0-3.3 2.7-6 6-6s6 2.7 6 6"/></svg>
|
||
|
|
Users
|
||
|
|
<span class="nav-count">8</span>
|
||
|
|
</div>
|
||
|
|
<div class="nav-item" id="nav-groups" onclick="showPanel('groups')">
|
||
|
|
<svg class="nav-icon" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="5.5" cy="5.5" r="2.5"/><circle cx="10.5" cy="5.5" r="2.5"/><path d="M1 14c0-2.5 2-4.5 4.5-4.5"/><path d="M15 14c0-2.5-2-4.5-4.5-4.5"/><path d="M5.5 9.5c0-2.5 2.25-4.5 5-4.5"/></svg>
|
||
|
|
Groups
|
||
|
|
<span class="nav-count">5</span>
|
||
|
|
</div>
|
||
|
|
<div class="nav-item" id="nav-roles" onclick="showPanel('roles')">
|
||
|
|
<svg class="nav-icon" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M8 2l1.5 4H14l-3.5 2.5 1.5 4L8 10l-4 2.5 1.5-4L2 6h4.5z"/></svg>
|
||
|
|
Roles
|
||
|
|
<span class="nav-count">6</span>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div class="sidebar-section">
|
||
|
|
<div class="sidebar-label">Audit</div>
|
||
|
|
<div class="nav-item" onclick="showPanel('overview')">
|
||
|
|
<svg class="nav-icon" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M3 3h10v10H3z"/><path d="M6 7h4M6 10h2"/></svg>
|
||
|
|
Audit log
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</nav>
|
||
|
|
|
||
|
|
<!-- Main area -->
|
||
|
|
<main class="main">
|
||
|
|
|
||
|
|
<!-- OVERVIEW PANEL -->
|
||
|
|
<div class="panel active" id="panel-overview">
|
||
|
|
<div class="panel-header">
|
||
|
|
<div><div class="panel-title">RBAC overview</div><div class="panel-subtitle">Inheritance model and system summary</div></div>
|
||
|
|
</div>
|
||
|
|
<div class="overview-grid">
|
||
|
|
<div class="stat-card"><div class="stat-label">Users</div><div class="stat-value">8</div><div class="stat-sub">6 active</div></div>
|
||
|
|
<div class="stat-card"><div class="stat-label">Groups</div><div class="stat-value">5</div><div class="stat-sub">Nested up to 3 levels</div></div>
|
||
|
|
<div class="stat-card"><div class="stat-label">Roles</div><div class="stat-value">6</div><div class="stat-sub">Direct + inherited</div></div>
|
||
|
|
</div>
|
||
|
|
<div class="inheritance-diagram">
|
||
|
|
<div class="inh-title">Inheritance model</div>
|
||
|
|
<div class="inh-row">
|
||
|
|
<div class="inh-col">
|
||
|
|
<div class="inh-col-title">Groups</div>
|
||
|
|
<div class="inh-item group">Engineering</div>
|
||
|
|
<div class="inh-item group" style="margin-left:10px;font-size:10px">→ Backend</div>
|
||
|
|
<div class="inh-item group" style="margin-left:10px;font-size:10px">→ Frontend</div>
|
||
|
|
<div class="inh-item group">Ops</div>
|
||
|
|
<div class="inh-item group">Admins</div>
|
||
|
|
</div>
|
||
|
|
<div class="inh-arrow">→</div>
|
||
|
|
<div class="inh-col">
|
||
|
|
<div class="inh-col-title">Roles on groups</div>
|
||
|
|
<div class="inh-item role">viewer</div>
|
||
|
|
<div class="inh-item role">editor</div>
|
||
|
|
<div class="inh-item role">deployer</div>
|
||
|
|
<div class="inh-item role">admin</div>
|
||
|
|
</div>
|
||
|
|
<div class="inh-arrow">→</div>
|
||
|
|
<div class="inh-col">
|
||
|
|
<div class="inh-col-title">Users inherit</div>
|
||
|
|
<div class="inh-item user">alice</div>
|
||
|
|
<div class="inh-item user">bob</div>
|
||
|
|
<div class="inh-item user">carol</div>
|
||
|
|
<div class="inh-item" style="font-size:10px;color:var(--color-text-tertiary)">+ 5 more…</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div class="inherit-note" style="margin-top:12px">
|
||
|
|
Users inherit all roles from every group they belong to — and transitively from parent groups. Roles can also be assigned directly to users, overriding or extending inherited permissions.
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- USERS PANEL -->
|
||
|
|
<div class="panel" id="panel-users">
|
||
|
|
<div class="panel-header">
|
||
|
|
<div><div class="panel-title">Users</div><div class="panel-subtitle">Manage identities, group membership and direct roles</div></div>
|
||
|
|
<button class="btn-add">+ Add user</button>
|
||
|
|
</div>
|
||
|
|
<div class="split">
|
||
|
|
<div class="list-pane">
|
||
|
|
<div class="search-bar"><input class="search-input" placeholder="Search users…" oninput="filterList(this,'user-items')"/></div>
|
||
|
|
<div class="entity-list" id="user-items">
|
||
|
|
<div class="entity-item selected" onclick="selectUser(this,'alice')">
|
||
|
|
<div class="avatar av-user">AL</div>
|
||
|
|
<div class="entity-info">
|
||
|
|
<div class="entity-name">Alice Lang</div>
|
||
|
|
<div class="entity-meta">alice@corp.io · Engineering → Backend</div>
|
||
|
|
<div class="tag-list"><span class="tag tag-role">admin</span><span class="tag tag-role tag-inherited">viewer</span><span class="tag tag-group">Backend</span></div>
|
||
|
|
</div>
|
||
|
|
<div class="status-dot status-active"></div>
|
||
|
|
</div>
|
||
|
|
<div class="entity-item" onclick="selectUser(this,'bob')">
|
||
|
|
<div class="avatar av-user">BK</div>
|
||
|
|
<div class="entity-info">
|
||
|
|
<div class="entity-name">Bob Kim</div>
|
||
|
|
<div class="entity-meta">bob@corp.io · Engineering → Frontend</div>
|
||
|
|
<div class="tag-list"><span class="tag tag-role tag-inherited">editor</span><span class="tag tag-group">Frontend</span></div>
|
||
|
|
</div>
|
||
|
|
<div class="status-dot status-active"></div>
|
||
|
|
</div>
|
||
|
|
<div class="entity-item" onclick="selectUser(this,'carol')">
|
||
|
|
<div class="avatar av-user">CS</div>
|
||
|
|
<div class="entity-info">
|
||
|
|
<div class="entity-name">Carol Sanz</div>
|
||
|
|
<div class="entity-meta">carol@corp.io · Ops</div>
|
||
|
|
<div class="tag-list"><span class="tag tag-role">deployer</span><span class="tag tag-role tag-inherited">viewer</span><span class="tag tag-group">Ops</span></div>
|
||
|
|
</div>
|
||
|
|
<div class="status-dot status-active"></div>
|
||
|
|
</div>
|
||
|
|
<div class="entity-item" onclick="selectUser(this,'dan')">
|
||
|
|
<div class="avatar av-user">DM</div>
|
||
|
|
<div class="entity-info">
|
||
|
|
<div class="entity-name">Dan Müller</div>
|
||
|
|
<div class="entity-meta">dan@corp.io · Admins</div>
|
||
|
|
<div class="tag-list"><span class="tag tag-role tag-inherited">admin</span><span class="tag tag-group">Admins</span></div>
|
||
|
|
</div>
|
||
|
|
<div class="status-dot status-active"></div>
|
||
|
|
</div>
|
||
|
|
<div class="entity-item" onclick="selectUser(this,'eve')">
|
||
|
|
<div class="avatar av-user" style="background:#FBEAF0;color:#72243E">EP</div>
|
||
|
|
<div class="entity-info">
|
||
|
|
<div class="entity-name">Eve Park</div>
|
||
|
|
<div class="entity-meta">eve@corp.io · Engineering → Backend</div>
|
||
|
|
<div class="tag-list"><span class="tag tag-role tag-inherited">editor</span><span class="tag tag-group">Backend</span></div>
|
||
|
|
</div>
|
||
|
|
<div class="status-dot status-active"></div>
|
||
|
|
</div>
|
||
|
|
<div class="entity-item" onclick="selectUser(this,'frank')">
|
||
|
|
<div class="avatar av-user" style="background:#F1EFE8;color:#444441">FR</div>
|
||
|
|
<div class="entity-info">
|
||
|
|
<div class="entity-name">Frank Rossi</div>
|
||
|
|
<div class="entity-meta">frank@corp.io · (no groups)</div>
|
||
|
|
<div class="tag-list"><span class="tag tag-role">viewer</span></div>
|
||
|
|
</div>
|
||
|
|
<div class="status-dot status-inactive"></div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div class="detail-pane" id="user-detail">
|
||
|
|
<!-- Alice default -->
|
||
|
|
<div class="detail-avatar av-user" style="width:44px;height:44px;font-size:15px">AL</div>
|
||
|
|
<div class="detail-name">Alice Lang</div>
|
||
|
|
<div class="detail-email">alice@corp.io</div>
|
||
|
|
<div class="field-row"><span class="field-label">Status</span><span class="field-val" style="color:#0F6E56;font-size:12px">● Active</span></div>
|
||
|
|
<div class="field-row"><span class="field-label">ID</span><span class="field-val mono">usr_01HX…4AF</span></div>
|
||
|
|
<div class="field-row"><span class="field-label">Created</span><span class="field-val">2024-03-12</span></div>
|
||
|
|
<hr class="divider">
|
||
|
|
<div class="detail-section">
|
||
|
|
<div class="detail-section-title">Group membership <span>direct only</span></div>
|
||
|
|
<span class="chip">
|
||
|
|
<svg viewBox="0 0 10 10" fill="none" stroke="currentColor" stroke-width="1.2"><rect x="1" y="1" width="8" height="8" rx="1.5"/></svg>
|
||
|
|
Engineering
|
||
|
|
</span>
|
||
|
|
<span class="chip">
|
||
|
|
<svg viewBox="0 0 10 10" fill="none" stroke="currentColor" stroke-width="1.2"><rect x="1" y="1" width="8" height="8" rx="1.5"/></svg>
|
||
|
|
Backend
|
||
|
|
<span style="opacity:0.4;margin-left:2px;font-size:9px">via Engineering</span>
|
||
|
|
</span>
|
||
|
|
</div>
|
||
|
|
<div class="detail-section">
|
||
|
|
<div class="detail-section-title">Effective roles <span>direct + inherited</span></div>
|
||
|
|
<span class="chip" style="border-color:#FAC775;color:#633806;background:#FAEEDA">admin</span>
|
||
|
|
<span class="chip inherited" style="border-color:#FAC775;color:#854F0B">
|
||
|
|
viewer
|
||
|
|
<span style="font-size:9px;opacity:0.6;margin-left:2px">↑ Engineering</span>
|
||
|
|
</span>
|
||
|
|
<span class="chip inherited" style="border-color:#FAC775;color:#854F0B">
|
||
|
|
editor
|
||
|
|
<span style="font-size:9px;opacity:0.6;margin-left:2px">↑ Backend</span>
|
||
|
|
</span>
|
||
|
|
<div class="inherit-note">Dashed roles are inherited transitively through group membership.</div>
|
||
|
|
</div>
|
||
|
|
<div class="detail-section">
|
||
|
|
<div class="detail-section-title">Group tree</div>
|
||
|
|
<div class="tree-row"><div class="tree-indent"></div>Engineering</div>
|
||
|
|
<div class="tree-row"><div class="tree-indent"><div class="tree-corner"></div></div>Backend <span style="font-size:10px;color:var(--color-text-tertiary);margin-left:4px">child group</span></div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- GROUPS PANEL -->
|
||
|
|
<div class="panel" id="panel-groups">
|
||
|
|
<div class="panel-header">
|
||
|
|
<div><div class="panel-title">Groups</div><div class="panel-subtitle">Organise users in nested hierarchies; roles propagate to all members</div></div>
|
||
|
|
<button class="btn-add">+ Add group</button>
|
||
|
|
</div>
|
||
|
|
<div class="split">
|
||
|
|
<div class="list-pane">
|
||
|
|
<div class="search-bar"><input class="search-input" placeholder="Search groups…"/></div>
|
||
|
|
<div class="entity-list">
|
||
|
|
<div class="entity-item selected" onclick="selectGroup(this,'engineering')">
|
||
|
|
<div class="avatar av-group">EN</div>
|
||
|
|
<div class="entity-info">
|
||
|
|
<div class="entity-name">Engineering</div>
|
||
|
|
<div class="entity-meta">Top-level · 2 child groups · 5 members</div>
|
||
|
|
<div class="tag-list"><span class="tag tag-role">viewer</span></div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div class="entity-item" onclick="selectGroup(this,'backend')">
|
||
|
|
<div class="avatar av-group" style="font-size:10px">BE</div>
|
||
|
|
<div class="entity-info">
|
||
|
|
<div class="entity-name">Backend</div>
|
||
|
|
<div class="entity-meta">Child of Engineering · 3 members</div>
|
||
|
|
<div class="tag-list"><span class="tag tag-role">editor</span><span class="tag tag-role tag-inherited">viewer</span></div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div class="entity-item" onclick="selectGroup(this,'frontend')">
|
||
|
|
<div class="avatar av-group" style="font-size:10px">FE</div>
|
||
|
|
<div class="entity-info">
|
||
|
|
<div class="entity-name">Frontend</div>
|
||
|
|
<div class="entity-meta">Child of Engineering · 2 members</div>
|
||
|
|
<div class="tag-list"><span class="tag tag-role">editor</span><span class="tag tag-role tag-inherited">viewer</span></div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div class="entity-item" onclick="selectGroup(this,'ops')">
|
||
|
|
<div class="avatar av-group" style="background:#FAEEDA;color:#633806">OP</div>
|
||
|
|
<div class="entity-info">
|
||
|
|
<div class="entity-name">Ops</div>
|
||
|
|
<div class="entity-meta">Top-level · 2 members</div>
|
||
|
|
<div class="tag-list"><span class="tag tag-role">deployer</span><span class="tag tag-role">viewer</span></div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div class="entity-item" onclick="selectGroup(this,'admins')">
|
||
|
|
<div class="avatar av-group" style="background:#FCEBEB;color:#791F1F">AD</div>
|
||
|
|
<div class="entity-info">
|
||
|
|
<div class="entity-name">Admins</div>
|
||
|
|
<div class="entity-meta">Top-level · 1 member</div>
|
||
|
|
<div class="tag-list"><span class="tag tag-role">admin</span></div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div class="detail-pane" id="group-detail">
|
||
|
|
<div class="detail-avatar av-group" style="width:44px;height:44px;font-size:15px">EN</div>
|
||
|
|
<div class="detail-name">Engineering</div>
|
||
|
|
<div class="detail-email">Top-level group</div>
|
||
|
|
<div class="field-row"><span class="field-label">ID</span><span class="field-val mono">grp_02KX…9BC</span></div>
|
||
|
|
<hr class="divider">
|
||
|
|
<div class="detail-section">
|
||
|
|
<div class="detail-section-title">Members <span>direct</span></div>
|
||
|
|
<span class="chip">Alice Lang</span><span class="chip">Eve Park</span><span class="chip">Bob Kim</span>
|
||
|
|
<div style="font-size:11px;color:var(--color-text-tertiary);margin-top:6px">+ all members of Backend, Frontend</div>
|
||
|
|
</div>
|
||
|
|
<div class="detail-section">
|
||
|
|
<div class="detail-section-title">Child groups</div>
|
||
|
|
<span class="chip" style="border-color:#9FE1CB;color:#0F6E56;background:#E1F5EE">Backend</span>
|
||
|
|
<span class="chip" style="border-color:#9FE1CB;color:#0F6E56;background:#E1F5EE">Frontend</span>
|
||
|
|
</div>
|
||
|
|
<div class="detail-section">
|
||
|
|
<div class="detail-section-title">Assigned roles <span>on this group</span></div>
|
||
|
|
<span class="chip" style="border-color:#FAC775;color:#633806;background:#FAEEDA">viewer</span>
|
||
|
|
<div class="inherit-note">Child groups Backend and Frontend inherit <strong style="font-weight:500">viewer</strong>, and additionally carry their own <strong style="font-weight:500">editor</strong> role.</div>
|
||
|
|
</div>
|
||
|
|
<div class="detail-section">
|
||
|
|
<div class="detail-section-title">Group hierarchy</div>
|
||
|
|
<div class="tree-row">Engineering</div>
|
||
|
|
<div class="tree-row"><div class="tree-indent"><div class="tree-corner"></div></div>Backend</div>
|
||
|
|
<div class="tree-row"><div class="tree-indent"><div class="tree-corner"></div></div>Frontend</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- ROLES PANEL -->
|
||
|
|
<div class="panel" id="panel-roles">
|
||
|
|
<div class="panel-header">
|
||
|
|
<div><div class="panel-title">Roles</div><div class="panel-subtitle">Define permission scopes; assign to users or groups</div></div>
|
||
|
|
<button class="btn-add">+ Add role</button>
|
||
|
|
</div>
|
||
|
|
<div class="split">
|
||
|
|
<div class="list-pane">
|
||
|
|
<div class="search-bar"><input class="search-input" placeholder="Search roles…"/></div>
|
||
|
|
<div class="entity-list">
|
||
|
|
<div class="entity-item selected">
|
||
|
|
<div class="avatar av-role">AD</div>
|
||
|
|
<div class="entity-info">
|
||
|
|
<div class="entity-name">admin</div>
|
||
|
|
<div class="entity-meta">Full access · 2 direct assignments</div>
|
||
|
|
<div class="tag-list"><span class="tag tag-group">Admins</span><span class="tag tag-group">Alice</span></div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div class="entity-item">
|
||
|
|
<div class="avatar av-role">ED</div>
|
||
|
|
<div class="entity-info">
|
||
|
|
<div class="entity-name">editor</div>
|
||
|
|
<div class="entity-meta">Read + write · 2 group assignments</div>
|
||
|
|
<div class="tag-list"><span class="tag tag-group">Backend</span><span class="tag tag-group">Frontend</span></div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div class="entity-item">
|
||
|
|
<div class="avatar av-role">DE</div>
|
||
|
|
<div class="entity-info">
|
||
|
|
<div class="entity-name">deployer</div>
|
||
|
|
<div class="entity-meta">Deploy access · 1 assignment</div>
|
||
|
|
<div class="tag-list"><span class="tag tag-group">Ops</span></div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div class="entity-item">
|
||
|
|
<div class="avatar av-role">VI</div>
|
||
|
|
<div class="entity-info">
|
||
|
|
<div class="entity-name">viewer</div>
|
||
|
|
<div class="entity-meta">Read-only · 1 group assignment</div>
|
||
|
|
<div class="tag-list"><span class="tag tag-group">Engineering</span></div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div class="entity-item">
|
||
|
|
<div class="avatar av-role">AU</div>
|
||
|
|
<div class="entity-info">
|
||
|
|
<div class="entity-name">auditor</div>
|
||
|
|
<div class="entity-meta">Audit log access · 0 assignments</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div class="detail-pane">
|
||
|
|
<div class="detail-avatar av-role" style="width:44px;height:44px;font-size:15px">AD</div>
|
||
|
|
<div class="detail-name">admin</div>
|
||
|
|
<div class="detail-email">Full administrative access</div>
|
||
|
|
<div class="field-row"><span class="field-label">ID</span><span class="field-val mono">rol_00AA…1F2</span></div>
|
||
|
|
<div class="field-row"><span class="field-label">Scope</span><span class="field-val">system-wide</span></div>
|
||
|
|
<hr class="divider">
|
||
|
|
<div class="detail-section">
|
||
|
|
<div class="detail-section-title">Assigned to groups</div>
|
||
|
|
<span class="chip" style="border-color:#9FE1CB;color:#0F6E56;background:#E1F5EE">Admins</span>
|
||
|
|
</div>
|
||
|
|
<div class="detail-section">
|
||
|
|
<div class="detail-section-title">Assigned to users (direct)</div>
|
||
|
|
<span class="chip" style="border-color:#B5D4F4;color:#0C447C;background:#E6F1FB">Alice Lang</span>
|
||
|
|
</div>
|
||
|
|
<div class="detail-section">
|
||
|
|
<div class="detail-section-title">Effective principals <span>via inheritance</span></div>
|
||
|
|
<span class="chip">Alice Lang</span>
|
||
|
|
<span class="chip">Dan Müller</span>
|
||
|
|
<span class="chip inherited">…via Admins group</span>
|
||
|
|
<div class="inherit-note">Dan inherits admin through the Admins group. Alice holds it directly.</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
</main>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<script>
|
||
|
|
function showPanel(name) {
|
||
|
|
document.querySelectorAll('.panel').forEach(p => p.classList.remove('active'));
|
||
|
|
document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active'));
|
||
|
|
const panel = document.getElementById('panel-' + name);
|
||
|
|
if (panel) panel.classList.add('active');
|
||
|
|
const nav = document.getElementById('nav-' + name);
|
||
|
|
if (nav) nav.classList.add('active');
|
||
|
|
}
|
||
|
|
|
||
|
|
function filterList(input, listId) {
|
||
|
|
const q = input.value.toLowerCase();
|
||
|
|
document.querySelectorAll('#' + listId + ' .entity-item').forEach(item => {
|
||
|
|
item.style.display = item.textContent.toLowerCase().includes(q) ? '' : 'none';
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
const userDetails = {
|
||
|
|
alice: {
|
||
|
|
initials: 'AL', name: 'Alice Lang', email: 'alice@corp.io', id: 'usr_01HX…4AF', created: '2024-03-12',
|
||
|
|
groups: ['Engineering','Backend'], roles: ['admin'], inherited: [{role:'viewer',via:'Engineering'},{role:'editor',via:'Backend'}],
|
||
|
|
tree: [['Engineering',''],['Backend','child group']]
|
||
|
|
},
|
||
|
|
bob: {
|
||
|
|
initials: 'BK', name: 'Bob Kim', email: 'bob@corp.io', id: 'usr_02BY…8CD', created: '2024-04-01',
|
||
|
|
groups: ['Engineering','Frontend'], roles: [], inherited: [{role:'editor',via:'Frontend'},{role:'viewer',via:'Engineering'}],
|
||
|
|
tree: [['Engineering',''],['Frontend','child group']]
|
||
|
|
},
|
||
|
|
carol: {
|
||
|
|
initials: 'CS', name: 'Carol Sanz', email: 'carol@corp.io', id: 'usr_03CS…2EF', created: '2024-02-20',
|
||
|
|
groups: ['Ops'], roles: ['deployer'], inherited: [{role:'viewer',via:'Ops'}],
|
||
|
|
tree: [['Ops','']]
|
||
|
|
},
|
||
|
|
dan: {
|
||
|
|
initials: 'DM', name: 'Dan Müller', email: 'dan@corp.io', id: 'usr_04DM…6GH', created: '2023-11-15',
|
||
|
|
groups: ['Admins'], roles: [], inherited: [{role:'admin',via:'Admins'}],
|
||
|
|
tree: [['Admins','']]
|
||
|
|
},
|
||
|
|
eve: {
|
||
|
|
initials: 'EP', name: 'Eve Park', email: 'eve@corp.io', id: 'usr_05EP…3IJ', created: '2024-05-10',
|
||
|
|
groups: ['Engineering','Backend'], roles: [], inherited: [{role:'editor',via:'Backend'},{role:'viewer',via:'Engineering'}],
|
||
|
|
tree: [['Engineering',''],['Backend','child group']]
|
||
|
|
},
|
||
|
|
frank: {
|
||
|
|
initials: 'FR', name: 'Frank Rossi', email: 'frank@corp.io', id: 'usr_06FR…7KL', created: '2024-06-01',
|
||
|
|
groups: [], roles: ['viewer'], inherited: [],
|
||
|
|
tree: []
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
function selectUser(el, key) {
|
||
|
|
document.querySelectorAll('#panel-users .entity-item').forEach(i => i.classList.remove('selected'));
|
||
|
|
el.classList.add('selected');
|
||
|
|
const u = userDetails[key];
|
||
|
|
if (!u) return;
|
||
|
|
const direct = u.roles.map(r => `<span class="chip" style="border-color:#FAC775;color:#633806;background:#FAEEDA">${r}</span>`).join('');
|
||
|
|
const inh = u.inherited.map(r => `<span class="chip inherited" style="border-color:#FAC775;color:#854F0B">${r.role} <span style="font-size:9px;opacity:0.6;margin-left:2px">↑ ${r.via}</span></span>`).join('');
|
||
|
|
const grpChips = u.groups.length ? u.groups.map(g => `<span class="chip"><svg viewBox="0 0 10 10" fill="none" stroke="currentColor" stroke-width="1.2" width="10" height="10"><rect x="1" y="1" width="8" height="8" rx="1.5"/></svg>${g}</span>`).join('') : '<span style="font-size:12px;color:var(--color-text-tertiary)">No group membership</span>';
|
||
|
|
const treeRows = u.tree.length ? u.tree.map((t,i) => i===0 ? `<div class="tree-row">${t[0]}</div>` : `<div class="tree-row"><div class="tree-indent"><div class="tree-corner"></div></div>${t[0]} <span style="font-size:10px;color:var(--color-text-tertiary);margin-left:4px">${t[1]}</span></div>`).join('') : '<span style="font-size:12px;color:var(--color-text-tertiary)">No group hierarchy</span>';
|
||
|
|
const inhNote = u.inherited.length ? `<div class="inherit-note">Dashed roles are inherited transitively through group membership.</div>` : '';
|
||
|
|
document.getElementById('user-detail').innerHTML = `
|
||
|
|
<div class="detail-avatar av-user" style="width:44px;height:44px;font-size:15px">${u.initials}</div>
|
||
|
|
<div class="detail-name">${u.name}</div>
|
||
|
|
<div class="detail-email">${u.email}</div>
|
||
|
|
<div class="field-row"><span class="field-label">Status</span><span class="field-val" style="color:#0F6E56;font-size:12px">● Active</span></div>
|
||
|
|
<div class="field-row"><span class="field-label">ID</span><span class="field-val mono">${u.id}</span></div>
|
||
|
|
<div class="field-row"><span class="field-label">Created</span><span class="field-val">${u.created}</span></div>
|
||
|
|
<hr class="divider">
|
||
|
|
<div class="detail-section"><div class="detail-section-title">Group membership <span>direct</span></div>${grpChips}</div>
|
||
|
|
<div class="detail-section"><div class="detail-section-title">Effective roles <span>direct + inherited</span></div>${direct}${inh}${inhNote}</div>
|
||
|
|
<div class="detail-section"><div class="detail-section-title">Group tree</div>${treeRows}</div>
|
||
|
|
`;
|
||
|
|
}
|
||
|
|
|
||
|
|
function selectGroup(el, key) {
|
||
|
|
document.querySelectorAll('#panel-groups .entity-item').forEach(i => i.classList.remove('selected'));
|
||
|
|
el.classList.add('selected');
|
||
|
|
}
|
||
|
|
</script>
|