mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-23 20:25:56 +00:00
style(portal): add team access policy column
This commit is contained in:
+1
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"source_hash": "8b0b284da60918f341d35eb2e51aed2893c7af4b399d949fd6dc0cef3e166408",
|
||||
"source_hash": "06fe7351022ab56b379fb5059391505df0cd8b1ddb0c897f4b7ba30a9cca6866",
|
||||
"build_inputs": [
|
||||
"package.json",
|
||||
"tsconfig.json",
|
||||
|
||||
+24
-8
@@ -1101,11 +1101,22 @@ header .logout-btn:hover,
|
||||
grid-template-columns: minmax(0, 1.65fr) minmax(320px, 0.92fr);
|
||||
gap: 14px;
|
||||
}
|
||||
.team-side-column {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
}
|
||||
.team-roster,
|
||||
.team-invite-panel {
|
||||
background: #fff;
|
||||
border-color: var(--line);
|
||||
}
|
||||
.team-policy-panel {
|
||||
padding: 16px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 12px;
|
||||
background: #fff;
|
||||
}
|
||||
.team-panel-heading {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
@@ -1232,27 +1243,32 @@ header .logout-btn:hover,
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
.team-invite-guide {
|
||||
.team-policy-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
padding: 10px 12px;
|
||||
gap: 0;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 10px;
|
||||
background: #fff;
|
||||
background: var(--panel-muted);
|
||||
overflow: hidden;
|
||||
}
|
||||
.team-invite-guide-row {
|
||||
.team-policy-row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
gap: 4px;
|
||||
padding: 12px 14px;
|
||||
border-top: 1px solid var(--line);
|
||||
}
|
||||
.team-invite-guide-row strong {
|
||||
.team-policy-row:first-child {
|
||||
border-top: none;
|
||||
}
|
||||
.team-policy-row strong {
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
color: var(--ink-muted);
|
||||
}
|
||||
.team-invite-guide-row span {
|
||||
.team-policy-row span {
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
color: var(--ink-soft);
|
||||
|
||||
+2
-1
@@ -1813,7 +1813,8 @@
|
||||
return '<section class="account-content-panel account-content-panel-workspaces"><div class="account-stage-header"><div><div class="account-panel-kicker">Workspace fleet</div><h3>Hosted fleet</h3><p>Use the fleet view to open workspaces, watch health posture, and keep management actions explicit.</p></div></div><div class="workspace-operations-shell"><div class="workspace-operations-main">' + workspaceHTML + '</div><div class="workspace-operations-detail">' + workspaceManagement + "</div></div></section>";
|
||||
}
|
||||
function renderAccountTeamSection(account) {
|
||||
return '<section class="account-content-panel account-content-panel-team"><section class="team-management-panel team-section team-section-shell" id="team-section-' + escapeAttr(account.id) + '" data-actor-role="' + escapeAttr(account.role) + '"><div class="team-management-header"><div><div class="account-panel-kicker">Team management</div><h3>Control who can operate this account</h3><p>Owners manage billing and access. Admins and techs keep the hosted fleet running day to day.</p></div><button type="button" class="btn-secondary btn-compact" data-shell-action="activate-section" data-shell-section="workspaces">Done</button></div><div class="team-management-stats" id="team-stats-' + escapeAttr(account.id) + '"></div><div class="team-management-grid"><div class="team-roster"><div class="team-panel-heading"><h4>People on this account</h4><p>Keep the roster small and role assignment explicit. The people listed here are the ones who can operate the hosted fleet.</p></div><div class="team-roster-list" id="team-list-' + escapeAttr(account.id) + '"><div class="team-list-message">Loading\u2026</div></div></div><div class="team-invite-panel"><div class="team-panel-heading"><h4>Invite someone new</h4><p>Add another operator with the minimum role they need for this account.</p></div><div class="team-invite"><div><label for="invite-email-' + escapeAttr(account.id) + '">Email</label><input type="email" id="invite-email-' + escapeAttr(account.id) + '" placeholder="user@example.com" autocomplete="off"></div><div><label for="invite-role-' + escapeAttr(account.id) + '">Role</label><select id="invite-role-' + escapeAttr(account.id) + '"><option value="admin">Admin</option><option value="tech">Tech</option><option value="read_only">Read-only</option></select></div><div class="team-invite-guide"><div class="team-invite-guide-row"><strong>Admin</strong><span>Billing and hosted operations.</span></div><div class="team-invite-guide-row"><strong>Tech</strong><span>Operational access without billing control.</span></div><div class="team-invite-guide-row"><strong>Read-only</strong><span>Review hosted state without making changes.</span></div></div><button type="button" class="btn-primary btn-compact" data-action="invite-member" data-account-id="' + escapeAttr(account.id) + '">Invite</button></div></div></div></section></section>';
|
||||
var accessPolicy = '<div class="team-policy-panel"><div class="team-panel-heading"><h4>Access model</h4><p>Assign the smallest role that still lets someone do the work they own on this account.</p></div><div class="team-policy-list"><div class="team-policy-row"><strong>Owner</strong><span>Billing, team access, and full hosted control.</span></div><div class="team-policy-row"><strong>Admin</strong><span>Hosted operations plus billing for the account.</span></div><div class="team-policy-row"><strong>Tech</strong><span>Workspace operations without billing ownership.</span></div><div class="team-policy-row"><strong>Read-only</strong><span>State review and verification without control-plane changes.</span></div></div></div>';
|
||||
return '<section class="account-content-panel account-content-panel-team"><section class="team-management-panel team-section team-section-shell" id="team-section-' + escapeAttr(account.id) + '" data-actor-role="' + escapeAttr(account.role) + '"><div class="team-management-header"><div><div class="account-panel-kicker">Team management</div><h3>Control who can operate this account</h3><p>Owners manage billing and access. Admins and techs keep the hosted fleet running day to day.</p></div><button type="button" class="btn-secondary btn-compact" data-shell-action="activate-section" data-shell-section="workspaces">Done</button></div><div class="team-management-stats" id="team-stats-' + escapeAttr(account.id) + '"></div><div class="team-management-grid"><div class="team-roster"><div class="team-panel-heading"><h4>People on this account</h4><p>Keep the roster small and role assignment explicit. The people listed here are the ones who can operate the hosted fleet.</p></div><div class="team-roster-list" id="team-list-' + escapeAttr(account.id) + '"><div class="team-list-message">Loading\u2026</div></div></div><div class="team-side-column"><div class="team-invite-panel"><div class="team-panel-heading"><h4>Invite someone new</h4><p>Add another operator with the minimum role they need for this account.</p></div><div class="team-invite"><div><label for="invite-email-' + escapeAttr(account.id) + '">Email</label><input type="email" id="invite-email-' + escapeAttr(account.id) + '" placeholder="user@example.com" autocomplete="off"></div><div><label for="invite-role-' + escapeAttr(account.id) + '">Role</label><select id="invite-role-' + escapeAttr(account.id) + '"><option value="admin">Admin</option><option value="tech">Tech</option><option value="read_only">Read-only</option></select></div><button type="button" class="btn-primary btn-compact" data-action="invite-member" data-account-id="' + escapeAttr(account.id) + '">Invite</button></div></div>' + accessPolicy + "</div></div></section></section>";
|
||||
}
|
||||
function renderSupportSection(context) {
|
||||
return '<section class="portal-support-panel"><div class="account-panel-kicker">Support</div><h2>Support and escalation</h2><p>Use support when hosted access looks wrong, billing does not behave as expected, or you need help with commercial licensing and privacy actions.</p><div class="portal-support-card-grid"><div class="portal-support-card"><h3>Account support</h3><p>For access, tenant handoff, team, and billing issues, contact the hosted operations desk.</p><a class="portal-support-link" href="mailto:' + escapeAttr(context.bootstrap.support_email || "") + '">' + escapeHTML(context.bootstrap.support_email || "") + '</a></div><div class="portal-support-card"><h3>Commercial services</h3><p>Self-hosted subscriptions, license recovery, refunds, and privacy requests all route through the same account surface.</p><button type="button" class="btn-secondary" data-shell-action="activate-section" data-shell-section="services">Open account services</button></div></div></section>';
|
||||
|
||||
@@ -169,6 +169,8 @@ describe('shell view', function() {
|
||||
expect(html).toContain('data-action="clear-workspace-selection"');
|
||||
expect(html).toContain('Team management');
|
||||
expect(html).toContain('Invite someone new');
|
||||
expect(html).toContain('Access model');
|
||||
expect(html).toContain('Workspace operations without billing ownership.');
|
||||
expect(html).toContain('People on this account');
|
||||
expect(html).toContain('data-action="workspace-action"');
|
||||
expect(html).toContain('service-action-row');
|
||||
|
||||
@@ -579,6 +579,20 @@ function renderAccountWorkspaceSection(account: PortalAccountSummary, accountAPI
|
||||
}
|
||||
|
||||
function renderAccountTeamSection(account: PortalAccountSummary): string {
|
||||
var accessPolicy =
|
||||
'<div class="team-policy-panel">' +
|
||||
'<div class="team-panel-heading">' +
|
||||
'<h4>Access model</h4>' +
|
||||
'<p>Assign the smallest role that still lets someone do the work they own on this account.</p>' +
|
||||
'</div>' +
|
||||
'<div class="team-policy-list">' +
|
||||
'<div class="team-policy-row"><strong>Owner</strong><span>Billing, team access, and full hosted control.</span></div>' +
|
||||
'<div class="team-policy-row"><strong>Admin</strong><span>Hosted operations plus billing for the account.</span></div>' +
|
||||
'<div class="team-policy-row"><strong>Tech</strong><span>Workspace operations without billing ownership.</span></div>' +
|
||||
'<div class="team-policy-row"><strong>Read-only</strong><span>State review and verification without control-plane changes.</span></div>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
|
||||
return (
|
||||
'<section class="account-content-panel account-content-panel-team">' +
|
||||
'<section class="team-management-panel team-section team-section-shell" id="team-section-' +
|
||||
@@ -609,31 +623,29 @@ function renderAccountTeamSection(account: PortalAccountSummary): string {
|
||||
'<div class="team-list-message">Loading…</div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="team-invite-panel">' +
|
||||
'<div class="team-panel-heading">' +
|
||||
'<h4>Invite someone new</h4>' +
|
||||
'<p>Add another operator with the minimum role they need for this account.</p>' +
|
||||
'</div>' +
|
||||
'<div class="team-invite">' +
|
||||
'<div><label for="invite-email-' +
|
||||
escapeAttr(account.id) +
|
||||
'">Email</label><input type="email" id="invite-email-' +
|
||||
escapeAttr(account.id) +
|
||||
'" placeholder="user@example.com" autocomplete="off"></div>' +
|
||||
'<div><label for="invite-role-' +
|
||||
escapeAttr(account.id) +
|
||||
'">Role</label><select id="invite-role-' +
|
||||
escapeAttr(account.id) +
|
||||
'"><option value="admin">Admin</option><option value="tech">Tech</option><option value="read_only">Read-only</option></select></div>' +
|
||||
'<div class="team-invite-guide">' +
|
||||
'<div class="team-invite-guide-row"><strong>Admin</strong><span>Billing and hosted operations.</span></div>' +
|
||||
'<div class="team-invite-guide-row"><strong>Tech</strong><span>Operational access without billing control.</span></div>' +
|
||||
'<div class="team-invite-guide-row"><strong>Read-only</strong><span>Review hosted state without making changes.</span></div>' +
|
||||
'<div class="team-side-column">' +
|
||||
'<div class="team-invite-panel">' +
|
||||
'<div class="team-panel-heading">' +
|
||||
'<h4>Invite someone new</h4>' +
|
||||
'<p>Add another operator with the minimum role they need for this account.</p>' +
|
||||
'</div>' +
|
||||
'<div class="team-invite">' +
|
||||
'<div><label for="invite-email-' +
|
||||
escapeAttr(account.id) +
|
||||
'">Email</label><input type="email" id="invite-email-' +
|
||||
escapeAttr(account.id) +
|
||||
'" placeholder="user@example.com" autocomplete="off"></div>' +
|
||||
'<div><label for="invite-role-' +
|
||||
escapeAttr(account.id) +
|
||||
'">Role</label><select id="invite-role-' +
|
||||
escapeAttr(account.id) +
|
||||
'"><option value="admin">Admin</option><option value="tech">Tech</option><option value="read_only">Read-only</option></select></div>' +
|
||||
'<button type="button" class="btn-primary btn-compact" data-action="invite-member" data-account-id="' +
|
||||
escapeAttr(account.id) +
|
||||
'">Invite</button>' +
|
||||
'</div>' +
|
||||
'<button type="button" class="btn-primary btn-compact" data-action="invite-member" data-account-id="' +
|
||||
escapeAttr(account.id) +
|
||||
'">Invite</button>' +
|
||||
'</div>' +
|
||||
accessPolicy +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</section>' +
|
||||
|
||||
@@ -1235,12 +1235,25 @@ header .logout-btn:hover,
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.team-side-column {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.team-roster,
|
||||
.team-invite-panel {
|
||||
background: #fff;
|
||||
border-color: var(--line);
|
||||
}
|
||||
|
||||
.team-policy-panel {
|
||||
padding: 16px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 12px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.team-panel-heading {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
@@ -1386,30 +1399,36 @@ header .logout-btn:hover,
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.team-invite-guide {
|
||||
.team-policy-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
padding: 10px 12px;
|
||||
gap: 0;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 10px;
|
||||
background: #fff;
|
||||
background: var(--panel-muted);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.team-invite-guide-row {
|
||||
.team-policy-row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
gap: 4px;
|
||||
padding: 12px 14px;
|
||||
border-top: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.team-invite-guide-row strong {
|
||||
.team-policy-row:first-child {
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
.team-policy-row strong {
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
color: var(--ink-muted);
|
||||
}
|
||||
|
||||
.team-invite-guide-row span {
|
||||
.team-policy-row span {
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
color: var(--ink-soft);
|
||||
|
||||
Reference in New Issue
Block a user