style(portal): strengthen workspace operator desk

This commit is contained in:
rcourtman
2026-03-27 23:02:30 +00:00
parent 66cf9b0e1f
commit d4a9f69ff1
7 changed files with 113 additions and 13 deletions
+1 -1
View File
@@ -1,5 +1,5 @@
{
"source_hash": "0401fbaf26ce16d3985c1cca3d6116f9fc2f3cb72e756d1bb8bca91d0d8633e2",
"source_hash": "46e32b3f10be76cc7ecaf28e84ea41b97ba549984641bbc401f9b9cd5484880a",
"build_inputs": [
"package.json",
"tsconfig.json",
+43
View File
@@ -912,10 +912,29 @@ header .logout-btn:hover,
padding: 14px 16px;
background: #fff;
border-top: 1px solid var(--line);
transition:
border-color 140ms ease,
background 140ms ease,
box-shadow 140ms ease;
}
.workspace-row:first-child {
border-top: none;
}
.workspace-row:hover {
background: #fbfcfe;
}
.workspace-row.selected {
position: relative;
border-color: rgba(11, 106, 114, 0.34);
background: #eef8fa;
box-shadow: inset 4px 0 0 var(--accent), 0 0 0 1px rgba(11, 106, 114, 0.12);
}
.workspace-row.selected .workspace-name {
color: var(--accent-strong);
}
.workspace-row.selected .workspace-row-note {
color: var(--ink);
}
.workspace-row-primary {
display: flex;
flex-direction: column;
@@ -1066,6 +1085,30 @@ header .logout-btn:hover,
font-size: 12px;
line-height: 1.5;
}
.workspace-management-empty-checklist {
display: flex;
flex-direction: column;
gap: 10px;
}
.workspace-management-empty-check {
display: flex;
flex-direction: column;
gap: 4px;
padding: 12px;
border: 1px solid var(--line);
border-radius: 10px;
background: rgba(255, 255, 255, 0.78);
}
.workspace-management-empty-check strong {
font-size: 13px;
line-height: 1.3;
color: var(--ink);
}
.workspace-management-empty-check span {
font-size: 12px;
line-height: 1.5;
color: var(--ink-soft);
}
.workspace-management-empty-actions {
display: flex;
gap: 10px;
+6 -2
View File
@@ -114,6 +114,10 @@
if (!empty || !content || !title || !meta || !summary || !health || !lifecycle || !created || !guidance || !actionButton || !closeButton) return;
var workspace = entry.selectedWorkspaceID ? findWorkspace(account, entry.selectedWorkspaceID) : null;
var hasSelection = !!workspace;
var rows = document.querySelectorAll("[data-workspace-row]");
for (var i = 0; i < rows.length; i += 1) {
rows[i].classList.toggle("selected", !!workspace && rows[i].getAttribute("data-workspace-row") === workspace.id);
}
panel.classList.add("visible");
empty.hidden = hasSelection;
content.hidden = !hasSelection;
@@ -1805,7 +1809,7 @@
if (account.can_manage && (state === "active" || state === "suspended" || state === "failed")) {
manageAction = '<button type="button" class="btn-secondary btn-workspace-manage" data-action="select-workspace" data-account-id="' + escapeAttr(account.id) + '" data-workspace-id="' + escapeAttr(workspace.id) + '">Manage</button>';
}
return '<article class="workspace-row"><div class="workspace-row-primary"><div class="workspace-row-heading"><h4 class="workspace-name">' + escapeHTML(workspace.display_name) + '</h4><div class="workspace-meta">' + metaParts.join("") + '</div></div><div class="workspace-row-note">' + escapeHTML(workspaceRowNote(workspace)) + '</div></div><div class="workspace-row-status-cell workspace-row-status-cell-badge">' + healthBadgeHTML(workspace) + '</div><div class="workspace-row-status-cell workspace-row-status-cell-badge"><span class="badge badge-' + escapeHTML(state || "unknown") + '">' + escapeHTML(titleCase(state || "Unknown")) + '</span></div><div class="workspace-actions">' + openAction + manageAction + "</div></article>";
return '<article class="workspace-row workspace-row-health-' + escapeAttr(status) + " workspace-row-state-" + escapeAttr(state || "unknown") + '" data-workspace-row="' + escapeAttr(workspace.id) + '"><div class="workspace-row-primary"><div class="workspace-row-heading"><h4 class="workspace-name">' + escapeHTML(workspace.display_name) + '</h4><div class="workspace-meta">' + metaParts.join("") + '</div></div><div class="workspace-row-note">' + escapeHTML(workspaceRowNote(workspace)) + '</div></div><div class="workspace-row-status-cell workspace-row-status-cell-badge">' + healthBadgeHTML(workspace) + '</div><div class="workspace-row-status-cell workspace-row-status-cell-badge"><span class="badge badge-' + escapeHTML(state || "unknown") + '">' + escapeHTML(titleCase(state || "Unknown")) + '</span></div><div class="workspace-actions">' + openAction + manageAction + "</div></article>";
}
function renderAccountOverviewSection(account) {
var workspaces = Array.isArray(account.workspaces) ? account.workspaces : [];
@@ -1844,7 +1848,7 @@
workspaceDeskActions += '<button type="button" class="btn-secondary btn-compact" data-action="open-billing" data-account-id="' + escapeAttr(account.id) + '">Manage billing</button>';
}
workspaceDeskActions += '<button type="button" class="btn-secondary btn-compact" data-action="toggle-team" data-account-id="' + escapeAttr(account.id) + '" data-shell-target="team">Manage team</button>';
workspaceManagement = '<section class="workspace-management-panel" id="workspace-management-' + escapeAttr(account.id) + '"><div class="workspace-management-header"><div><div class="account-panel-kicker">Workspace management</div><h3>Review one workspace at a time</h3><p>Select a workspace from the fleet to review its lifecycle state and run explicit management actions.</p></div><button type="button" class="btn-secondary btn-compact" id="workspace-management-close-' + escapeAttr(account.id) + '" data-action="clear-workspace-selection" data-account-id="' + escapeAttr(account.id) + '">Done</button></div><div class="workspace-management-empty" id="workspace-management-empty-' + escapeAttr(account.id) + '"><div class="workspace-management-empty-copy">Choose a workspace from the fleet to open its management desk.</div><div class="workspace-management-empty-grid"><div class="workspace-management-empty-card"><div class="account-panel-kicker">Account desk</div><h4>Keep account-wide actions close</h4><p>Use the management desk for workspace-level decisions, and keep billing or team changes one click away.</p><div class="workspace-management-empty-actions">' + workspaceDeskActions + '</div></div><div class="workspace-management-empty-card workspace-management-empty-card-muted"><div class="account-panel-kicker">What shows here</div><ul class="workspace-management-empty-list"><li>Lifecycle and health summary for the selected workspace</li><li>The next explicit lifecycle action for that workspace</li><li>Account-level actions when you need billing or team changes instead</li></ul></div></div></div><div class="workspace-management-content" id="workspace-management-content-' + escapeAttr(account.id) + '" hidden><div class="workspace-management-meta" id="workspace-management-meta-' + escapeAttr(account.id) + '"></div><h4 id="workspace-management-title-' + escapeAttr(account.id) + '"></h4><p class="workspace-management-summary" id="workspace-management-summary-' + escapeAttr(account.id) + '"></p><div class="workspace-management-facts"><div class="workspace-management-fact"><span>Health</span><strong id="workspace-management-health-' + escapeAttr(account.id) + '"></strong></div><div class="workspace-management-fact"><span>Lifecycle</span><strong id="workspace-management-lifecycle-' + escapeAttr(account.id) + '"></strong></div><div class="workspace-management-fact"><span>Created</span><strong id="workspace-management-created-' + escapeAttr(account.id) + '"></strong></div></div><div class="workspace-management-guidance" id="workspace-management-guidance-' + escapeAttr(account.id) + '"></div><div class="workspace-management-actions"><button type="button" class="btn-danger" id="workspace-management-action-' + escapeAttr(account.id) + '" data-action="workspace-action" data-account-id="' + escapeAttr(account.id) + '">Manage workspace</button></div></div></section>';
workspaceManagement = '<section class="workspace-management-panel" id="workspace-management-' + escapeAttr(account.id) + '"><div class="workspace-management-header"><div><div class="account-panel-kicker">Workspace management</div><h3>Review one workspace at a time</h3><p>Select a workspace from the fleet to review its lifecycle state and run explicit management actions.</p></div><button type="button" class="btn-secondary btn-compact" id="workspace-management-close-' + escapeAttr(account.id) + '" data-action="clear-workspace-selection" data-account-id="' + escapeAttr(account.id) + '">Done</button></div><div class="workspace-management-empty" id="workspace-management-empty-' + escapeAttr(account.id) + '"><div class="workspace-management-empty-copy">Choose a workspace from the fleet to open its management desk.</div><div class="workspace-management-empty-grid"><div class="workspace-management-empty-card"><div class="account-panel-kicker">Account desk</div><h4>Keep account-wide actions close</h4><p>Use the desk for one workspace at a time while keeping billing, team, and create-workspace actions within reach.</p><div class="workspace-management-empty-actions">' + workspaceDeskActions + '</div></div><div class="workspace-management-empty-card workspace-management-empty-card-muted"><div class="account-panel-kicker">Review flow</div><div class="workspace-management-empty-checklist"><div class="workspace-management-empty-check"><strong>1. Pick the workspace</strong><span>Select a fleet row to bring its health, lifecycle, and next action into this desk.</span></div><div class="workspace-management-empty-check"><strong>2. Check posture first</strong><span>Treat checking or needs-attention workspaces as review work, not routine operations.</span></div><div class="workspace-management-empty-check"><strong>3. Escalate account changes separately</strong><span>Billing and team changes stay account-wide even when you are focused on one workspace.</span></div></div></div></div></div><div class="workspace-management-content" id="workspace-management-content-' + escapeAttr(account.id) + '" hidden><div class="workspace-management-meta" id="workspace-management-meta-' + escapeAttr(account.id) + '"></div><h4 id="workspace-management-title-' + escapeAttr(account.id) + '"></h4><p class="workspace-management-summary" id="workspace-management-summary-' + escapeAttr(account.id) + '"></p><div class="workspace-management-facts"><div class="workspace-management-fact"><span>Health</span><strong id="workspace-management-health-' + escapeAttr(account.id) + '"></strong></div><div class="workspace-management-fact"><span>Lifecycle</span><strong id="workspace-management-lifecycle-' + escapeAttr(account.id) + '"></strong></div><div class="workspace-management-fact"><span>Created</span><strong id="workspace-management-created-' + escapeAttr(account.id) + '"></strong></div></div><div class="workspace-management-guidance" id="workspace-management-guidance-' + escapeAttr(account.id) + '"></div><div class="workspace-management-actions"><button type="button" class="btn-danger" id="workspace-management-action-' + escapeAttr(account.id) + '" data-action="workspace-action" data-account-id="' + escapeAttr(account.id) + '">Manage workspace</button></div></div></section>';
}
var workspaceHTML = workspaces.length ? '<div class="workspace-list-wrap"><div class="workspace-list-toolbar"><div class="workspace-list-summary">Review the hosted fleet, open a workspace, and keep lifecycle actions explicit.</div><div class="workspace-list-stats"><span class="workspace-list-stat"><strong>' + String(workspaces.length) + '</strong> total</span><span class="workspace-list-stat"><strong>' + String(healthyCount) + '</strong> healthy</span><span class="workspace-list-stat"><strong>' + String(checkingCount) + '</strong> checking</span><span class="workspace-list-stat workspace-list-stat-attention"><strong>' + String(unhealthyCount) + '</strong> needs attention</span></div></div><div class="workspace-list-head"><span>Workspace</span><span>Health</span><span>Lifecycle</span><span>Actions</span></div><div class="workspace-list">' + workspaces.map(function(workspace) {
return renderWorkspaceCard(account, workspace, accountAPIBasePath);
@@ -136,6 +136,10 @@ export function renderWorkspaceManagement(account: PortalAccountSummary, entry:
var workspace = entry.selectedWorkspaceID ? findWorkspace(account, entry.selectedWorkspaceID) : null;
var hasSelection = !!workspace;
var rows = document.querySelectorAll<HTMLElement>('[data-workspace-row]');
for (var i = 0; i < rows.length; i += 1) {
rows[i].classList.toggle('selected', !!workspace && rows[i].getAttribute('data-workspace-row') === workspace.id);
}
panel.classList.add('visible');
empty.hidden = hasSelection;
content.hidden = !hasSelection;
@@ -166,8 +166,9 @@ describe('shell view', function() {
expect(html).toContain('data-action="select-workspace"');
expect(html).toContain('Workspace management');
expect(html).toContain('Choose a workspace from the fleet to open its management desk.');
expect(html).toContain('What shows here');
expect(html).toContain('Lifecycle and health summary for the selected workspace');
expect(html).toContain('Review flow');
expect(html).toContain('Pick the workspace');
expect(html).toContain('Escalate account changes separately');
expect(html).toContain('data-action="clear-workspace-selection"');
expect(html).toContain('Team management');
expect(html).toContain('Invite someone new');
@@ -309,7 +309,7 @@ function renderWorkspaceCard(account: PortalAccountSummary, workspace: PortalWor
}
return (
'<article class="workspace-row">' +
'<article class="workspace-row workspace-row-health-' + escapeAttr(status) + ' workspace-row-state-' + escapeAttr(state || 'unknown') + '" data-workspace-row="' + escapeAttr(workspace.id) + '">' +
'<div class="workspace-row-primary">' +
'<div class="workspace-row-heading">' +
'<h4 class="workspace-name">' + escapeHTML(workspace.display_name) + '</h4>' +
@@ -511,16 +511,16 @@ function renderAccountWorkspaceSection(account: PortalAccountSummary, accountAPI
'<div class="workspace-management-empty-card">' +
'<div class="account-panel-kicker">Account desk</div>' +
'<h4>Keep account-wide actions close</h4>' +
'<p>Use the management desk for workspace-level decisions, and keep billing or team changes one click away.</p>' +
'<p>Use the desk for one workspace at a time while keeping billing, team, and create-workspace actions within reach.</p>' +
'<div class="workspace-management-empty-actions">' + workspaceDeskActions + '</div>' +
'</div>' +
'<div class="workspace-management-empty-card workspace-management-empty-card-muted">' +
'<div class="account-panel-kicker">What shows here</div>' +
'<ul class="workspace-management-empty-list">' +
'<li>Lifecycle and health summary for the selected workspace</li>' +
'<li>The next explicit lifecycle action for that workspace</li>' +
'<li>Account-level actions when you need billing or team changes instead</li>' +
'</ul>' +
'<div class="account-panel-kicker">Review flow</div>' +
'<div class="workspace-management-empty-checklist">' +
'<div class="workspace-management-empty-check"><strong>1. Pick the workspace</strong><span>Select a fleet row to bring its health, lifecycle, and next action into this desk.</span></div>' +
'<div class="workspace-management-empty-check"><strong>2. Check posture first</strong><span>Treat checking or needs-attention workspaces as review work, not routine operations.</span></div>' +
'<div class="workspace-management-empty-check"><strong>3. Escalate account changes separately</strong><span>Billing and team changes stay account-wide even when you are focused on one workspace.</span></div>' +
'</div>' +
'</div>' +
'</div>' +
'</div>' +
@@ -1017,12 +1017,32 @@ header .logout-btn:hover,
padding: 14px 16px;
background: #fff;
border-top: 1px solid var(--line);
transition: border-color 140ms ease, background 140ms ease, box-shadow 140ms ease;
}
.workspace-row:first-child {
border-top: none;
}
.workspace-row:hover {
background: #fbfcfe;
}
.workspace-row.selected {
position: relative;
border-color: rgba(11, 106, 114, 0.34);
background: #eef8fa;
box-shadow: inset 4px 0 0 var(--accent), 0 0 0 1px rgba(11, 106, 114, 0.12);
}
.workspace-row.selected .workspace-name {
color: var(--accent-strong);
}
.workspace-row.selected .workspace-row-note {
color: var(--ink);
}
.workspace-row-primary {
display: flex;
flex-direction: column;
@@ -1198,6 +1218,34 @@ header .logout-btn:hover,
line-height: 1.5;
}
.workspace-management-empty-checklist {
display: flex;
flex-direction: column;
gap: 10px;
}
.workspace-management-empty-check {
display: flex;
flex-direction: column;
gap: 4px;
padding: 12px;
border: 1px solid var(--line);
border-radius: 10px;
background: rgba(255, 255, 255, 0.78);
}
.workspace-management-empty-check strong {
font-size: 13px;
line-height: 1.3;
color: var(--ink);
}
.workspace-management-empty-check span {
font-size: 12px;
line-height: 1.5;
color: var(--ink-soft);
}
.workspace-management-empty-actions {
display: flex;
gap: 10px;