mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-23 20:25:56 +00:00
style(portal): strengthen workspace management desk
This commit is contained in:
+1
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"source_hash": "b4d41db9eab964bde794244e2c7d9d7e0aa77de1523520c2c448d1f08cfe969f",
|
||||
"source_hash": "640380cc9fd2cd2036a3ac2a9f949e018d3adfb7a6953cc3aa46c8d799980740",
|
||||
"build_inputs": [
|
||||
"package.json",
|
||||
"tsconfig.json",
|
||||
|
||||
+94
-10
@@ -953,7 +953,7 @@ header .logout-btn:hover,
|
||||
}
|
||||
.workspace-operations-shell {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1.5fr) minmax(320px, 0.9fr);
|
||||
grid-template-columns: minmax(0, 1.42fr) minmax(360px, 1fr);
|
||||
gap: 14px;
|
||||
align-items: start;
|
||||
padding: 16px;
|
||||
@@ -977,10 +977,9 @@ header .logout-btn:hover,
|
||||
background: #fff;
|
||||
}
|
||||
.workspace-management-content {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
gap: 16px;
|
||||
align-items: end;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
}
|
||||
.workspace-management-header,
|
||||
.team-management-header {
|
||||
@@ -1004,16 +1003,50 @@ header .logout-btn:hover,
|
||||
line-height: 1.5;
|
||||
color: var(--ink);
|
||||
}
|
||||
.workspace-management-empty-grid {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
gap: 12px;
|
||||
}
|
||||
.workspace-management-empty-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
padding: 14px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 12px;
|
||||
background: #fff;
|
||||
}
|
||||
.workspace-management-empty-card h4 {
|
||||
margin: 0;
|
||||
font-size: 17px;
|
||||
line-height: 1.1;
|
||||
letter-spacing: -0.025em;
|
||||
}
|
||||
.workspace-management-empty-card p {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
line-height: 1.55;
|
||||
color: var(--ink-soft);
|
||||
}
|
||||
.workspace-management-empty-card-muted {
|
||||
background: var(--panel-muted);
|
||||
}
|
||||
.workspace-management-empty-list {
|
||||
margin: 0;
|
||||
padding-left: 18px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
color: var(--ink-soft);
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.workspace-management-empty-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.workspace-management-empty-note {
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
color: var(--ink-soft);
|
||||
}
|
||||
.workspace-management-content {
|
||||
margin-top: 16px;
|
||||
}
|
||||
@@ -1024,6 +1057,54 @@ header .logout-btn:hover,
|
||||
text-transform: uppercase;
|
||||
color: var(--ink-muted);
|
||||
}
|
||||
.workspace-management-facts {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
.workspace-management-fact {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
padding: 12px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 10px;
|
||||
background: var(--panel-muted);
|
||||
}
|
||||
.workspace-management-fact span {
|
||||
font-size: 11px;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.1em;
|
||||
text-transform: uppercase;
|
||||
color: var(--ink-muted);
|
||||
}
|
||||
.workspace-management-fact strong {
|
||||
font-size: 15px;
|
||||
line-height: 1.2;
|
||||
letter-spacing: -0.02em;
|
||||
color: var(--ink);
|
||||
}
|
||||
.workspace-management-guidance {
|
||||
padding: 14px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 10px;
|
||||
background: #fff;
|
||||
font-size: 13px;
|
||||
line-height: 1.55;
|
||||
color: var(--ink-soft);
|
||||
}
|
||||
.workspace-management-actions {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
.workspace-management-actions .btn-danger {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
max-width: 220px;
|
||||
}
|
||||
.add-workspace-form,
|
||||
.team-section,
|
||||
.service-panel {
|
||||
@@ -1697,6 +1778,9 @@ header .logout-btn:hover,
|
||||
.workspace-management-content {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.workspace-management-facts {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.workspace-list-head {
|
||||
display: none;
|
||||
}
|
||||
|
||||
+36
-2
@@ -54,6 +54,32 @@
|
||||
if (workspace.health_status === "unhealthy") return "This workspace needs attention before it is trustworthy.";
|
||||
return "This workspace is still waiting on a completed health check.";
|
||||
}
|
||||
function workspaceHealthLabel(workspace) {
|
||||
if (workspace.health_status === "healthy") return "Healthy";
|
||||
if (workspace.health_status === "unhealthy") return "Needs attention";
|
||||
return "Checking";
|
||||
}
|
||||
function workspaceCreatedLabel(workspace) {
|
||||
if (!workspace.created_at) return "Unknown";
|
||||
var date = new Date(workspace.created_at);
|
||||
if (Number.isNaN(date.getTime())) return "Unknown";
|
||||
return date.toLocaleDateString(void 0, { month: "short", day: "numeric", year: "numeric" });
|
||||
}
|
||||
function workspaceGuidance(workspace) {
|
||||
if (workspace.state === "active" && workspace.health_status === "healthy") {
|
||||
return "This workspace looks ready for normal operator work. Use the fleet table to open it, or suspend it here if you are intentionally taking it out of service.";
|
||||
}
|
||||
if (workspace.state === "active" && workspace.health_status === "checking") {
|
||||
return "This workspace is active but still waiting on a completed health check. Review it before you treat the hosted posture as settled.";
|
||||
}
|
||||
if (workspace.health_status === "unhealthy") {
|
||||
return "This workspace needs review before it is treated as trustworthy. Use the management action only when you intend to suspend or remove it from the hosted fleet.";
|
||||
}
|
||||
if (workspace.state === "suspended") {
|
||||
return "This workspace is already suspended. The remaining lifecycle action here is deletion, so treat it as a deliberate irreversible step.";
|
||||
}
|
||||
return "Review the lifecycle state before taking the next explicit action for this workspace.";
|
||||
}
|
||||
function workspaceMeta(workspace) {
|
||||
var parts = [workspace.state];
|
||||
if (workspace.health_status) parts.push(workspace.health_status);
|
||||
@@ -79,9 +105,13 @@
|
||||
var title = getElement("workspace-management-title-" + account.id);
|
||||
var meta = getElement("workspace-management-meta-" + account.id);
|
||||
var summary = getElement("workspace-management-summary-" + account.id);
|
||||
var health = getElement("workspace-management-health-" + account.id);
|
||||
var lifecycle = getElement("workspace-management-lifecycle-" + account.id);
|
||||
var created = getElement("workspace-management-created-" + account.id);
|
||||
var guidance = getElement("workspace-management-guidance-" + account.id);
|
||||
var actionButton = getElement("workspace-management-action-" + account.id);
|
||||
var closeButton = getElement("workspace-management-close-" + account.id);
|
||||
if (!empty || !content || !title || !meta || !summary || !actionButton || !closeButton) return;
|
||||
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;
|
||||
panel.classList.add("visible");
|
||||
@@ -97,6 +127,10 @@
|
||||
title.textContent = workspace.display_name;
|
||||
meta.textContent = workspaceMeta(workspace);
|
||||
summary.textContent = workspaceSummary(workspace);
|
||||
health.textContent = workspaceHealthLabel(workspace);
|
||||
lifecycle.textContent = workspace.state ? workspace.state.charAt(0).toUpperCase() + workspace.state.slice(1) : "Unknown";
|
||||
created.textContent = workspaceCreatedLabel(workspace);
|
||||
guidance.textContent = workspaceGuidance(workspace);
|
||||
actionButton.textContent = workspaceActionLabel(workspace);
|
||||
actionButton.disabled = entry.manageWorkspace.pending;
|
||||
actionButton.setAttribute("data-workspace-id", workspace.id);
|
||||
@@ -1816,7 +1850,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 to manage from the fleet above.</div><div class="workspace-management-empty-actions">' + workspaceDeskActions + '</div><div class="workspace-management-empty-note">Use the fleet table for workspace-level work, or run account-wide billing and team actions from here.</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-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 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>';
|
||||
}
|
||||
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);
|
||||
|
||||
@@ -81,7 +81,7 @@ describe('account runtime', function() {
|
||||
'<div id="add-ws-form-acct_1" class="add-workspace-form">' +
|
||||
'<input id="ws-name-acct_1" value="Acme Corp">' +
|
||||
'<div id="ws-spinner-acct_1" hidden></div>' +
|
||||
'<div id="workspace-management-acct_1" class="workspace-management-panel"><button id="workspace-management-close-acct_1"></button><div id="workspace-management-empty-acct_1"></div><div id="workspace-management-content-acct_1" hidden><div id="workspace-management-meta-acct_1"></div><h4 id="workspace-management-title-acct_1"></h4><p id="workspace-management-summary-acct_1"></p><button id="workspace-management-action-acct_1"></button></div></div>' +
|
||||
'<div id="workspace-management-acct_1" class="workspace-management-panel"><button id="workspace-management-close-acct_1"></button><div id="workspace-management-empty-acct_1"></div><div id="workspace-management-content-acct_1" hidden><div id="workspace-management-meta-acct_1"></div><h4 id="workspace-management-title-acct_1"></h4><p id="workspace-management-summary-acct_1"></p><div id="workspace-management-health-acct_1"></div><div id="workspace-management-lifecycle-acct_1"></div><div id="workspace-management-created-acct_1"></div><div id="workspace-management-guidance-acct_1"></div><button id="workspace-management-action-acct_1"></button></div></div>' +
|
||||
'</div>';
|
||||
|
||||
runtime.toggleAddWorkspace('acct_1');
|
||||
@@ -139,6 +139,10 @@ describe('account runtime', function() {
|
||||
'<div id="workspace-management-meta-acct_1"></div>' +
|
||||
'<h4 id="workspace-management-title-acct_1"></h4>' +
|
||||
'<p id="workspace-management-summary-acct_1"></p>' +
|
||||
'<div id="workspace-management-health-acct_1"></div>' +
|
||||
'<div id="workspace-management-lifecycle-acct_1"></div>' +
|
||||
'<div id="workspace-management-created-acct_1"></div>' +
|
||||
'<div id="workspace-management-guidance-acct_1"></div>' +
|
||||
'<button id="workspace-management-action-acct_1"></button>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
@@ -189,7 +193,7 @@ describe('account runtime', function() {
|
||||
);
|
||||
|
||||
document.body.innerHTML =
|
||||
'<div id="workspace-management-acct_1" class="workspace-management-panel"><button id="workspace-management-close-acct_1"></button><div id="workspace-management-empty-acct_1"></div><div id="workspace-management-content-acct_1" hidden><div id="workspace-management-meta-acct_1"></div><h4 id="workspace-management-title-acct_1"></h4><p id="workspace-management-summary-acct_1"></p><button id="workspace-management-action-acct_1"></button></div></div>' +
|
||||
'<div id="workspace-management-acct_1" class="workspace-management-panel"><button id="workspace-management-close-acct_1"></button><div id="workspace-management-empty-acct_1"></div><div id="workspace-management-content-acct_1" hidden><div id="workspace-management-meta-acct_1"></div><h4 id="workspace-management-title-acct_1"></h4><p id="workspace-management-summary-acct_1"></p><div id="workspace-management-health-acct_1"></div><div id="workspace-management-lifecycle-acct_1"></div><div id="workspace-management-created-acct_1"></div><div id="workspace-management-guidance-acct_1"></div><button id="workspace-management-action-acct_1"></button></div></div>' +
|
||||
'<div id="team-section-acct_1" class="team-section" data-actor-role="owner">' +
|
||||
'<div id="team-stats-acct_1"></div>' +
|
||||
'<table><tbody id="team-list-acct_1"></tbody></table>' +
|
||||
|
||||
@@ -148,6 +148,10 @@ describe('account view', function() {
|
||||
'<div id="workspace-management-meta-acct_1"></div>' +
|
||||
'<h4 id="workspace-management-title-acct_1"></h4>' +
|
||||
'<p id="workspace-management-summary-acct_1"></p>' +
|
||||
'<div id="workspace-management-health-acct_1"></div>' +
|
||||
'<div id="workspace-management-lifecycle-acct_1"></div>' +
|
||||
'<div id="workspace-management-created-acct_1"></div>' +
|
||||
'<div id="workspace-management-guidance-acct_1"></div>' +
|
||||
'<button id="workspace-management-action-acct_1"></button>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
@@ -157,16 +161,17 @@ describe('account view', function() {
|
||||
expect((document.getElementById('workspace-management-empty-acct_1') as HTMLElement).hidden).toBe(true);
|
||||
expect((document.getElementById('workspace-management-content-acct_1') as HTMLElement).hidden).toBe(false);
|
||||
expect(document.getElementById('workspace-management-title-acct_1')?.textContent).toContain('Alpha Workspace');
|
||||
expect(document.getElementById('workspace-management-health-acct_1')?.textContent).toContain('Healthy');
|
||||
expect(document.getElementById('workspace-management-action-acct_1')?.textContent).toContain('Suspend workspace');
|
||||
});
|
||||
|
||||
it('renders account UI for every tracked account entry', function() {
|
||||
document.body.innerHTML =
|
||||
'<div id="add-ws-form-acct_1" class="add-workspace-form"></div><div id="ws-spinner-acct_1"></div>' +
|
||||
'<div id="workspace-management-acct_1" class="workspace-management-panel"><button id="workspace-management-close-acct_1"></button><div id="workspace-management-empty-acct_1"></div><div id="workspace-management-content-acct_1" hidden><div id="workspace-management-meta-acct_1"></div><h4 id="workspace-management-title-acct_1"></h4><p id="workspace-management-summary-acct_1"></p><button id="workspace-management-action-acct_1"></button></div></div>' +
|
||||
'<div id="workspace-management-acct_1" class="workspace-management-panel"><button id="workspace-management-close-acct_1"></button><div id="workspace-management-empty-acct_1"></div><div id="workspace-management-content-acct_1" hidden><div id="workspace-management-meta-acct_1"></div><h4 id="workspace-management-title-acct_1"></h4><p id="workspace-management-summary-acct_1"></p><div id="workspace-management-health-acct_1"></div><div id="workspace-management-lifecycle-acct_1"></div><div id="workspace-management-created-acct_1"></div><div id="workspace-management-guidance-acct_1"></div><button id="workspace-management-action-acct_1"></button></div></div>' +
|
||||
'<div id="team-section-acct_1" class="team-section" data-actor-role="admin"><div id="team-stats-acct_1"></div><table><tbody id="team-list-acct_1"></tbody></table></div>' +
|
||||
'<div id="add-ws-form-acct_2" class="add-workspace-form"></div><div id="ws-spinner-acct_2"></div>' +
|
||||
'<div id="workspace-management-acct_2" class="workspace-management-panel"><button id="workspace-management-close-acct_2"></button><div id="workspace-management-empty-acct_2"></div><div id="workspace-management-content-acct_2" hidden><div id="workspace-management-meta-acct_2"></div><h4 id="workspace-management-title-acct_2"></h4><p id="workspace-management-summary-acct_2"></p><button id="workspace-management-action-acct_2"></button></div></div>' +
|
||||
'<div id="workspace-management-acct_2" class="workspace-management-panel"><button id="workspace-management-close-acct_2"></button><div id="workspace-management-empty-acct_2"></div><div id="workspace-management-content-acct_2" hidden><div id="workspace-management-meta-acct_2"></div><h4 id="workspace-management-title-acct_2"></h4><p id="workspace-management-summary-acct_2"></p><div id="workspace-management-health-acct_2"></div><div id="workspace-management-lifecycle-acct_2"></div><div id="workspace-management-created-acct_2"></div><div id="workspace-management-guidance-acct_2"></div><button id="workspace-management-action-acct_2"></button></div></div>' +
|
||||
'<div id="team-section-acct_2" class="team-section" data-actor-role="owner"><div id="team-stats-acct_2"></div><table><tbody id="team-list-acct_2"></tbody></table></div>';
|
||||
|
||||
var state: PortalAccountState = {
|
||||
|
||||
@@ -70,6 +70,35 @@ function workspaceSummary(workspace: PortalWorkspaceSummary): string {
|
||||
return 'This workspace is still waiting on a completed health check.';
|
||||
}
|
||||
|
||||
function workspaceHealthLabel(workspace: PortalWorkspaceSummary): string {
|
||||
if (workspace.health_status === 'healthy') return 'Healthy';
|
||||
if (workspace.health_status === 'unhealthy') return 'Needs attention';
|
||||
return 'Checking';
|
||||
}
|
||||
|
||||
function workspaceCreatedLabel(workspace: PortalWorkspaceSummary): string {
|
||||
if (!workspace.created_at) return 'Unknown';
|
||||
var date = new Date(workspace.created_at);
|
||||
if (Number.isNaN(date.getTime())) return 'Unknown';
|
||||
return date.toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' });
|
||||
}
|
||||
|
||||
function workspaceGuidance(workspace: PortalWorkspaceSummary): string {
|
||||
if (workspace.state === 'active' && workspace.health_status === 'healthy') {
|
||||
return 'This workspace looks ready for normal operator work. Use the fleet table to open it, or suspend it here if you are intentionally taking it out of service.';
|
||||
}
|
||||
if (workspace.state === 'active' && workspace.health_status === 'checking') {
|
||||
return 'This workspace is active but still waiting on a completed health check. Review it before you treat the hosted posture as settled.';
|
||||
}
|
||||
if (workspace.health_status === 'unhealthy') {
|
||||
return 'This workspace needs review before it is treated as trustworthy. Use the management action only when you intend to suspend or remove it from the hosted fleet.';
|
||||
}
|
||||
if (workspace.state === 'suspended') {
|
||||
return 'This workspace is already suspended. The remaining lifecycle action here is deletion, so treat it as a deliberate irreversible step.';
|
||||
}
|
||||
return 'Review the lifecycle state before taking the next explicit action for this workspace.';
|
||||
}
|
||||
|
||||
function workspaceMeta(workspace: PortalWorkspaceSummary): string {
|
||||
var parts = [workspace.state];
|
||||
if (workspace.health_status) parts.push(workspace.health_status);
|
||||
@@ -97,9 +126,13 @@ export function renderWorkspaceManagement(account: PortalAccountSummary, entry:
|
||||
var title = getElement<HTMLElement>('workspace-management-title-' + account.id);
|
||||
var meta = getElement<HTMLElement>('workspace-management-meta-' + account.id);
|
||||
var summary = getElement<HTMLElement>('workspace-management-summary-' + account.id);
|
||||
var health = getElement<HTMLElement>('workspace-management-health-' + account.id);
|
||||
var lifecycle = getElement<HTMLElement>('workspace-management-lifecycle-' + account.id);
|
||||
var created = getElement<HTMLElement>('workspace-management-created-' + account.id);
|
||||
var guidance = getElement<HTMLElement>('workspace-management-guidance-' + account.id);
|
||||
var actionButton = getElement<HTMLButtonElement>('workspace-management-action-' + account.id);
|
||||
var closeButton = getElement<HTMLButtonElement>('workspace-management-close-' + account.id);
|
||||
if (!empty || !content || !title || !meta || !summary || !actionButton || !closeButton) return;
|
||||
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;
|
||||
@@ -118,6 +151,10 @@ export function renderWorkspaceManagement(account: PortalAccountSummary, entry:
|
||||
title.textContent = workspace.display_name;
|
||||
meta.textContent = workspaceMeta(workspace);
|
||||
summary.textContent = workspaceSummary(workspace);
|
||||
health.textContent = workspaceHealthLabel(workspace);
|
||||
lifecycle.textContent = workspace.state ? workspace.state.charAt(0).toUpperCase() + workspace.state.slice(1) : 'Unknown';
|
||||
created.textContent = workspaceCreatedLabel(workspace);
|
||||
guidance.textContent = workspaceGuidance(workspace);
|
||||
actionButton.textContent = workspaceActionLabel(workspace);
|
||||
actionButton.disabled = entry.manageWorkspace.pending;
|
||||
actionButton.setAttribute('data-workspace-id', workspace.id);
|
||||
|
||||
@@ -167,7 +167,9 @@ describe('shell view', function() {
|
||||
expect(html).toContain('Open workspace');
|
||||
expect(html).toContain('data-action="select-workspace"');
|
||||
expect(html).toContain('Workspace management');
|
||||
expect(html).toContain('Choose a workspace to manage from the fleet above.');
|
||||
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('data-action="clear-workspace-selection"');
|
||||
expect(html).toContain('Team management');
|
||||
expect(html).toContain('Invite someone new');
|
||||
|
||||
@@ -518,9 +518,23 @@ function renderAccountWorkspaceSection(account: PortalAccountSummary, accountAPI
|
||||
'<div class="workspace-management-empty" id="workspace-management-empty-' +
|
||||
escapeAttr(account.id) +
|
||||
'">' +
|
||||
'<div class="workspace-management-empty-copy">Choose a workspace to manage from the fleet above.</div>' +
|
||||
'<div class="workspace-management-empty-actions">' + workspaceDeskActions + '</div>' +
|
||||
'<div class="workspace-management-empty-note">Use the fleet table for workspace-level work, or run account-wide billing and team actions from here.</div>' +
|
||||
'<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) +
|
||||
@@ -534,6 +548,23 @@ function renderAccountWorkspaceSection(account: PortalAccountSummary, accountAPI
|
||||
'<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) +
|
||||
|
||||
@@ -1063,7 +1063,7 @@ header .logout-btn:hover,
|
||||
|
||||
.workspace-operations-shell {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1.5fr) minmax(320px, 0.9fr);
|
||||
grid-template-columns: minmax(0, 1.42fr) minmax(360px, 1fr);
|
||||
gap: 14px;
|
||||
align-items: start;
|
||||
padding: 16px;
|
||||
@@ -1092,10 +1092,9 @@ header .logout-btn:hover,
|
||||
}
|
||||
|
||||
.workspace-management-content {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
gap: 16px;
|
||||
align-items: end;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.workspace-management-header,
|
||||
@@ -1123,18 +1122,57 @@ header .logout-btn:hover,
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
.workspace-management-empty-grid {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.workspace-management-empty-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
padding: 14px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 12px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.workspace-management-empty-card h4 {
|
||||
margin: 0;
|
||||
font-size: 17px;
|
||||
line-height: 1.1;
|
||||
letter-spacing: -0.025em;
|
||||
}
|
||||
|
||||
.workspace-management-empty-card p {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
line-height: 1.55;
|
||||
color: var(--ink-soft);
|
||||
}
|
||||
|
||||
.workspace-management-empty-card-muted {
|
||||
background: var(--panel-muted);
|
||||
}
|
||||
|
||||
.workspace-management-empty-list {
|
||||
margin: 0;
|
||||
padding-left: 18px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
color: var(--ink-soft);
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.workspace-management-empty-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.workspace-management-empty-note {
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
color: var(--ink-soft);
|
||||
}
|
||||
|
||||
.workspace-management-content {
|
||||
margin-top: 16px;
|
||||
}
|
||||
@@ -1147,6 +1185,61 @@ header .logout-btn:hover,
|
||||
color: var(--ink-muted);
|
||||
}
|
||||
|
||||
.workspace-management-facts {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.workspace-management-fact {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
padding: 12px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 10px;
|
||||
background: var(--panel-muted);
|
||||
}
|
||||
|
||||
.workspace-management-fact span {
|
||||
font-size: 11px;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.1em;
|
||||
text-transform: uppercase;
|
||||
color: var(--ink-muted);
|
||||
}
|
||||
|
||||
.workspace-management-fact strong {
|
||||
font-size: 15px;
|
||||
line-height: 1.2;
|
||||
letter-spacing: -0.02em;
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
.workspace-management-guidance {
|
||||
padding: 14px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 10px;
|
||||
background: #fff;
|
||||
font-size: 13px;
|
||||
line-height: 1.55;
|
||||
color: var(--ink-soft);
|
||||
}
|
||||
|
||||
.workspace-management-actions {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.workspace-management-actions .btn-danger {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
max-width: 220px;
|
||||
}
|
||||
|
||||
.add-workspace-form,
|
||||
.team-section,
|
||||
.service-panel {
|
||||
@@ -1921,6 +2014,10 @@ header .logout-btn:hover,
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.workspace-management-facts {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.workspace-list-head {
|
||||
display: none;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user