mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-23 20:25:56 +00:00
fix(portal): normalize legacy team roles
This commit is contained in:
+1
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"source_hash": "b48ed843c8ab211f26d40bdb069e5a6a3f35c206eedef2cf15301ca0f640e8e3",
|
||||
"source_hash": "b99ef3907290e1285e448dd8af5af07b4767f6fe4f6afd76d1b46722980f5a18",
|
||||
"build_inputs": [
|
||||
"package.json",
|
||||
"tsconfig.json",
|
||||
|
||||
+16
-10
@@ -1,7 +1,11 @@
|
||||
(() => {
|
||||
// src/account_view.ts
|
||||
function normalizedTeamRole(role) {
|
||||
if (role === "member") return "read_only";
|
||||
return role || "read_only";
|
||||
}
|
||||
function roleLabel(role) {
|
||||
switch (role) {
|
||||
switch (normalizedTeamRole(role)) {
|
||||
case "owner":
|
||||
return "Owner";
|
||||
case "admin":
|
||||
@@ -17,7 +21,7 @@
|
||||
}
|
||||
}
|
||||
function roleCapabilityCopy(role) {
|
||||
switch (role) {
|
||||
switch (normalizedTeamRole(role)) {
|
||||
case "owner":
|
||||
return "Full account control, including billing, team access, and hosted operations.";
|
||||
case "admin":
|
||||
@@ -110,7 +114,7 @@
|
||||
function countMembersByRole(members, role) {
|
||||
var count = 0;
|
||||
for (var i = 0; i < members.length; i += 1) {
|
||||
if (members[i].role === role) count += 1;
|
||||
if (normalizedTeamRole(members[i].role) === role) count += 1;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
@@ -133,10 +137,11 @@
|
||||
stats.innerHTML = '<div class="team-stat-card"><span class="team-stat-label">Members</span><span class="team-stat-value">' + String(members.length) + '</span></div><div class="team-stat-card"><span class="team-stat-label">Owners</span><span class="team-stat-value">' + String(countMembersByRole(members, "owner")) + '</span></div><div class="team-stat-card"><span class="team-stat-label">Admins</span><span class="team-stat-value">' + String(countMembersByRole(members, "admin")) + '</span></div><div class="team-stat-card"><span class="team-stat-label">Operators</span><span class="team-stat-value">' + String(countMembersByRole(members, "tech") + countMembersByRole(members, "read_only")) + "</span></div>";
|
||||
}
|
||||
function renderTeamRoleControl(accountID, member, isOwner) {
|
||||
if (member.role === "owner" && !isOwner) {
|
||||
var currentRole = normalizedTeamRole(member.role);
|
||||
if (currentRole === "owner" && !isOwner) {
|
||||
var locked = document.createElement("span");
|
||||
locked.className = "team-role-badge";
|
||||
locked.textContent = roleLabel(member.role);
|
||||
locked.textContent = roleLabel(currentRole);
|
||||
return locked;
|
||||
}
|
||||
var sel = document.createElement("select");
|
||||
@@ -146,7 +151,7 @@
|
||||
var opt = document.createElement("option");
|
||||
opt.value = roles[j];
|
||||
opt.textContent = roleLabel(roles[j]);
|
||||
if (member.role === roles[j]) opt.selected = true;
|
||||
if (currentRole === roles[j]) opt.selected = true;
|
||||
sel.appendChild(opt);
|
||||
}
|
||||
sel.setAttribute("data-action", "change-role");
|
||||
@@ -155,7 +160,7 @@
|
||||
return sel;
|
||||
}
|
||||
function renderTeamMemberAction(accountID, member, isOwner) {
|
||||
if (member.role === "owner" && !isOwner) {
|
||||
if (normalizedTeamRole(member.role) === "owner" && !isOwner) {
|
||||
return null;
|
||||
}
|
||||
var btn = document.createElement("button");
|
||||
@@ -1763,11 +1768,12 @@
|
||||
}
|
||||
function renderAccountOverviewSection(account) {
|
||||
var workspaces = Array.isArray(account.workspaces) ? account.workspaces : [];
|
||||
var summaryText = accountKindLabel(account) + " \xB7 " + titleCase(account.role) + " \xB7 " + workspaceCountLabel(workspaces.length);
|
||||
var healthyCount = countWorkspacesByHealth(workspaces, "healthy");
|
||||
var checkingCount = countWorkspacesByHealth(workspaces, "checking");
|
||||
var unhealthyCount = countWorkspacesByHealth(workspaces, "unhealthy");
|
||||
var activeCount = countWorkspacesByState(workspaces, "active");
|
||||
var postureTitle = unhealthyCount > 0 ? "Hosted posture needs review" : checkingCount > 0 ? "Hosted posture is still settling" : "Hosted posture is stable";
|
||||
var postureCopy = unhealthyCount > 0 ? "One or more workspaces still need attention before the hosted fleet is trustworthy." : checkingCount > 0 ? "The hosted fleet is mostly healthy, but some workspaces are still waiting on a completed health check." : "The hosted fleet is healthy and ready for routine operator work.";
|
||||
var operationsCopy = account.kind === "msp" ? "Manage the client fleet from this account surface. Workspace creation, billing, and team actions belong here." : "Use this account surface to open hosted workspaces, manage billing, and control access for this hosted account.";
|
||||
var actions = "";
|
||||
var addWorkspaceForm = "";
|
||||
@@ -1777,8 +1783,8 @@
|
||||
addWorkspaceForm = '<div class="add-workspace-form" id="add-ws-form-' + escapeAttr(account.id) + '"><label for="ws-name-' + escapeAttr(account.id) + '">Workspace name (for example, a client name)</label><input type="text" id="ws-name-' + escapeAttr(account.id) + '" placeholder="Acme Corp" maxlength="80" autocomplete="off"><div class="form-actions"><button type="button" class="btn-primary" data-action="create-workspace" data-account-id="' + escapeAttr(account.id) + '">Create workspace</button><button type="button" class="btn-secondary" data-action="toggle-add-workspace" data-account-id="' + escapeAttr(account.id) + '">Cancel</button><div class="spinner" id="ws-spinner-' + escapeAttr(account.id) + '" hidden></div></div></div>';
|
||||
}
|
||||
}
|
||||
return '<section class="account-content-panel account-content-panel-overview"><div class="account-command-deck"><div class="account-overview-card"><div class="account-panel-kicker">Hosted posture</div><h3>' + escapeHTML(account.name) + "</h3><p>" + escapeHTML(summaryText) + '</p><div class="account-overview-callout">' + escapeHTML(
|
||||
account.kind === "msp" ? "This is the control surface for hosted clients, account billing, and operator access." : "This is the control surface for hosted workspaces, billing, and account access."
|
||||
return '<section class="account-content-panel account-content-panel-overview"><div class="account-command-deck"><div class="account-overview-card"><div class="account-panel-kicker">Hosted posture</div><h3>' + escapeHTML(postureTitle) + "</h3><p>" + escapeHTML(postureCopy) + '</p><div class="account-overview-callout">' + escapeHTML(
|
||||
account.kind === "msp" ? "Use this console to run client workspaces, account billing, and operator access from one place." : "Use this console to run hosted workspaces, account billing, and operator access from one place."
|
||||
) + '</div></div><div class="account-metric-strip"><div class="account-stat-card account-stat-card-inline"><span class="account-stat-label">Active workspaces</span><span class="account-stat-value">' + String(activeCount) + '</span></div><div class="account-stat-card account-stat-card-inline"><span class="account-stat-label">Healthy</span><span class="account-stat-value account-stat-healthy">' + String(healthyCount) + '</span></div><div class="account-stat-card account-stat-card-inline"><span class="account-stat-label">Checking</span><span class="account-stat-value account-stat-checking">' + String(checkingCount) + '</span></div><div class="account-stat-card account-stat-card-inline"><span class="account-stat-label">Needs attention</span><span class="account-stat-value account-stat-unhealthy">' + String(unhealthyCount) + "</span></div></div>" + actions + addWorkspaceForm + '<div class="account-overview-secondary"><div class="overview-side-card overview-side-card-primary"><div class="account-panel-kicker">Operator overview</div><h4>Start from the next action, not the whole account</h4><p>Use the overview for posture, then move into workspaces, team, or account services depending on what needs attention.</p><div class="overview-quick-actions">' + renderOverviewQuickAction("workspaces", "Open workspaces", "Review the hosted fleet and move into a workspace") + renderOverviewQuickAction("team", "Review team access", "Check who can operate billing and hosted workspaces") + renderOverviewQuickAction("services", "Open account services", "Handle billing, licenses, refunds, and privacy actions") + "</div></div>" + renderAttentionPanel(workspaces) + "</div></div></section>";
|
||||
}
|
||||
function renderAccountWorkspaceSection(account, accountAPIBasePath) {
|
||||
|
||||
@@ -111,6 +111,34 @@ describe('account view', function() {
|
||||
expect(document.getElementById('team-stats-acct_1')?.textContent).toContain('2');
|
||||
});
|
||||
|
||||
it('normalizes legacy member roles into the current read-only operator model', function() {
|
||||
document.body.innerHTML =
|
||||
'<div id="team-section-acct_1" class="team-section" data-actor-role="owner">' +
|
||||
'<div id="team-stats-acct_1"></div>' +
|
||||
'<div id="team-list-acct_1"></div>' +
|
||||
'</div>';
|
||||
|
||||
renderTeamSection(
|
||||
'acct_1',
|
||||
createEntry({
|
||||
teamVisible: true,
|
||||
teamQuery: {
|
||||
status: 'ready',
|
||||
error: '',
|
||||
data: [
|
||||
{ email: 'legacy@example.com', role: 'member', user_id: 'u_legacy' },
|
||||
],
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
var roleSelect = document.querySelector('.team-role-select') as HTMLSelectElement;
|
||||
expect(document.getElementById('team-list-acct_1')?.textContent).toContain('Read-only');
|
||||
expect(roleSelect.value).toBe('read_only');
|
||||
expect(document.getElementById('team-stats-acct_1')?.textContent).toContain('Operators');
|
||||
expect(document.getElementById('team-stats-acct_1')?.textContent).toContain('1');
|
||||
});
|
||||
|
||||
it('renders workspace management selection from account UI state', function() {
|
||||
document.body.innerHTML =
|
||||
'<div id="workspace-management-acct_1" class="workspace-management-panel">' +
|
||||
|
||||
@@ -8,8 +8,13 @@ import type {
|
||||
|
||||
type FormValueElement = HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement;
|
||||
|
||||
function normalizedTeamRole(role: string): string {
|
||||
if (role === 'member') return 'read_only';
|
||||
return role || 'read_only';
|
||||
}
|
||||
|
||||
function roleLabel(role: string): string {
|
||||
switch (role) {
|
||||
switch (normalizedTeamRole(role)) {
|
||||
case 'owner':
|
||||
return 'Owner';
|
||||
case 'admin':
|
||||
@@ -26,7 +31,7 @@ function roleLabel(role: string): string {
|
||||
}
|
||||
|
||||
function roleCapabilityCopy(role: string): string {
|
||||
switch (role) {
|
||||
switch (normalizedTeamRole(role)) {
|
||||
case 'owner':
|
||||
return 'Full account control, including billing, team access, and hosted operations.';
|
||||
case 'admin':
|
||||
@@ -132,7 +137,7 @@ function setContainerMessage(container: HTMLElement, msg: string, isError: boole
|
||||
function countMembersByRole(members: PortalTeamMember[], role: string): number {
|
||||
var count = 0;
|
||||
for (var i = 0; i < members.length; i += 1) {
|
||||
if (members[i].role === role) count += 1;
|
||||
if (normalizedTeamRole(members[i].role) === role) count += 1;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
@@ -162,10 +167,11 @@ function renderTeamStats(accountID: string, entry: PortalAccountUIEntry): void {
|
||||
}
|
||||
|
||||
function renderTeamRoleControl(accountID: string, member: PortalTeamMember, isOwner: boolean): HTMLElement {
|
||||
if (member.role === 'owner' && !isOwner) {
|
||||
var currentRole = normalizedTeamRole(member.role);
|
||||
if (currentRole === 'owner' && !isOwner) {
|
||||
var locked = document.createElement('span');
|
||||
locked.className = 'team-role-badge';
|
||||
locked.textContent = roleLabel(member.role);
|
||||
locked.textContent = roleLabel(currentRole);
|
||||
return locked;
|
||||
}
|
||||
|
||||
@@ -176,7 +182,7 @@ function renderTeamRoleControl(accountID: string, member: PortalTeamMember, isOw
|
||||
var opt = document.createElement('option');
|
||||
opt.value = roles[j];
|
||||
opt.textContent = roleLabel(roles[j]);
|
||||
if (member.role === roles[j]) opt.selected = true;
|
||||
if (currentRole === roles[j]) opt.selected = true;
|
||||
sel.appendChild(opt);
|
||||
}
|
||||
sel.setAttribute('data-action', 'change-role');
|
||||
@@ -186,7 +192,7 @@ function renderTeamRoleControl(accountID: string, member: PortalTeamMember, isOw
|
||||
}
|
||||
|
||||
function renderTeamMemberAction(accountID: string, member: PortalTeamMember, isOwner: boolean): HTMLElement | null {
|
||||
if (member.role === 'owner' && !isOwner) {
|
||||
if (normalizedTeamRole(member.role) === 'owner' && !isOwner) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -142,6 +142,8 @@ describe('shell view', function() {
|
||||
expect(html).toContain('Manage the client fleet from this account surface.');
|
||||
expect(html).toContain('Operator overview');
|
||||
expect(html).toContain('Start from the next action, not the whole account');
|
||||
expect(html).toContain('Hosted posture needs review');
|
||||
expect(html).toContain('Use this console to run client workspaces, account billing, and operator access from one place.');
|
||||
expect(html).toContain('Open workspaces');
|
||||
expect(html).toContain('Review team access');
|
||||
expect(html).toContain('Needs review');
|
||||
|
||||
@@ -328,11 +328,20 @@ function renderWorkspaceCard(account: PortalAccountSummary, workspace: PortalWor
|
||||
|
||||
function renderAccountOverviewSection(account: PortalAccountSummary): string {
|
||||
var workspaces = Array.isArray(account.workspaces) ? account.workspaces : [];
|
||||
var summaryText = accountKindLabel(account) + ' · ' + titleCase(account.role) + ' · ' + workspaceCountLabel(workspaces.length);
|
||||
var healthyCount = countWorkspacesByHealth(workspaces, 'healthy');
|
||||
var checkingCount = countWorkspacesByHealth(workspaces, 'checking');
|
||||
var unhealthyCount = countWorkspacesByHealth(workspaces, 'unhealthy');
|
||||
var activeCount = countWorkspacesByState(workspaces, 'active');
|
||||
var postureTitle = unhealthyCount > 0
|
||||
? 'Hosted posture needs review'
|
||||
: checkingCount > 0
|
||||
? 'Hosted posture is still settling'
|
||||
: 'Hosted posture is stable';
|
||||
var postureCopy = unhealthyCount > 0
|
||||
? 'One or more workspaces still need attention before the hosted fleet is trustworthy.'
|
||||
: checkingCount > 0
|
||||
? 'The hosted fleet is mostly healthy, but some workspaces are still waiting on a completed health check.'
|
||||
: 'The hosted fleet is healthy and ready for routine operator work.';
|
||||
var operationsCopy = account.kind === 'msp'
|
||||
? 'Manage the client fleet from this account surface. Workspace creation, billing, and team actions belong here.'
|
||||
: 'Use this account surface to open hosted workspaces, manage billing, and control access for this hosted account.';
|
||||
@@ -398,11 +407,11 @@ function renderAccountOverviewSection(account: PortalAccountSummary): string {
|
||||
'<div class="account-command-deck">' +
|
||||
'<div class="account-overview-card">' +
|
||||
'<div class="account-panel-kicker">Hosted posture</div>' +
|
||||
'<h3>' + escapeHTML(account.name) + '</h3>' +
|
||||
'<p>' + escapeHTML(summaryText) + '</p>' +
|
||||
'<h3>' + escapeHTML(postureTitle) + '</h3>' +
|
||||
'<p>' + escapeHTML(postureCopy) + '</p>' +
|
||||
'<div class="account-overview-callout">' + escapeHTML(account.kind === 'msp'
|
||||
? 'This is the control surface for hosted clients, account billing, and operator access.'
|
||||
: 'This is the control surface for hosted workspaces, billing, and account access.'
|
||||
? 'Use this console to run client workspaces, account billing, and operator access from one place.'
|
||||
: 'Use this console to run hosted workspaces, account billing, and operator access from one place.'
|
||||
) + '</div>' +
|
||||
'</div>' +
|
||||
'<div class="account-metric-strip">' +
|
||||
|
||||
Reference in New Issue
Block a user