diff --git a/internal/cloudcp/portal/dist/build_manifest.json b/internal/cloudcp/portal/dist/build_manifest.json index a3864c0737..1666413e3a 100644 --- a/internal/cloudcp/portal/dist/build_manifest.json +++ b/internal/cloudcp/portal/dist/build_manifest.json @@ -1,5 +1,5 @@ { - "source_hash": "0401fbaf26ce16d3985c1cca3d6116f9fc2f3cb72e756d1bb8bca91d0d8633e2", + "source_hash": "46e32b3f10be76cc7ecaf28e84ea41b97ba549984641bbc401f9b9cd5484880a", "build_inputs": [ "package.json", "tsconfig.json", diff --git a/internal/cloudcp/portal/dist/portal_app.css b/internal/cloudcp/portal/dist/portal_app.css index 7ce8fca438..6164526aae 100644 --- a/internal/cloudcp/portal/dist/portal_app.css +++ b/internal/cloudcp/portal/dist/portal_app.css @@ -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; diff --git a/internal/cloudcp/portal/dist/portal_app.js b/internal/cloudcp/portal/dist/portal_app.js index d2e4ae10dd..a9a3dc13f3 100644 --- a/internal/cloudcp/portal/dist/portal_app.js +++ b/internal/cloudcp/portal/dist/portal_app.js @@ -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 = ''; } - return '

' + escapeHTML(workspace.display_name) + '

' + metaParts.join("") + '
' + escapeHTML(workspaceRowNote(workspace)) + '
' + healthBadgeHTML(workspace) + '
' + escapeHTML(titleCase(state || "Unknown")) + '
' + openAction + manageAction + "
"; + return '

' + escapeHTML(workspace.display_name) + '

' + metaParts.join("") + '
' + escapeHTML(workspaceRowNote(workspace)) + '
' + healthBadgeHTML(workspace) + '
' + escapeHTML(titleCase(state || "Unknown")) + '
' + openAction + manageAction + "
"; } function renderAccountOverviewSection(account) { var workspaces = Array.isArray(account.workspaces) ? account.workspaces : []; @@ -1844,7 +1848,7 @@ workspaceDeskActions += ''; } workspaceDeskActions += ''; - workspaceManagement = '

Review one workspace at a time

Select a workspace from the fleet to review its lifecycle state and run explicit management actions.

Choose a workspace from the fleet to open its management desk.

Keep account-wide actions close

Use the management desk for workspace-level decisions, and keep billing or team changes one click away.

' + workspaceDeskActions + '
  • Lifecycle and health summary for the selected workspace
  • The next explicit lifecycle action for that workspace
  • Account-level actions when you need billing or team changes instead
'; + workspaceManagement = '

Review one workspace at a time

Select a workspace from the fleet to review its lifecycle state and run explicit management actions.

Choose a workspace from the fleet to open its management desk.

Keep account-wide actions close

Use the desk for one workspace at a time while keeping billing, team, and create-workspace actions within reach.

' + workspaceDeskActions + '
1. Pick the workspaceSelect a fleet row to bring its health, lifecycle, and next action into this desk.
2. Check posture firstTreat checking or needs-attention workspaces as review work, not routine operations.
3. Escalate account changes separatelyBilling and team changes stay account-wide even when you are focused on one workspace.
'; } var workspaceHTML = workspaces.length ? '
Review the hosted fleet, open a workspace, and keep lifecycle actions explicit.
' + String(workspaces.length) + ' total' + String(healthyCount) + ' healthy' + String(checkingCount) + ' checking' + String(unhealthyCount) + ' needs attention
WorkspaceHealthLifecycleActions
' + workspaces.map(function(workspace) { return renderWorkspaceCard(account, workspace, accountAPIBasePath); diff --git a/internal/cloudcp/portal/frontend/src/account_view.ts b/internal/cloudcp/portal/frontend/src/account_view.ts index b96f024eb5..73825a08f2 100644 --- a/internal/cloudcp/portal/frontend/src/account_view.ts +++ b/internal/cloudcp/portal/frontend/src/account_view.ts @@ -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('[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; diff --git a/internal/cloudcp/portal/frontend/src/shell_view.test.ts b/internal/cloudcp/portal/frontend/src/shell_view.test.ts index 25896acb2a..38d1486a9c 100644 --- a/internal/cloudcp/portal/frontend/src/shell_view.test.ts +++ b/internal/cloudcp/portal/frontend/src/shell_view.test.ts @@ -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'); diff --git a/internal/cloudcp/portal/frontend/src/shell_view.ts b/internal/cloudcp/portal/frontend/src/shell_view.ts index 2897cc2e90..cbffc5bc81 100644 --- a/internal/cloudcp/portal/frontend/src/shell_view.ts +++ b/internal/cloudcp/portal/frontend/src/shell_view.ts @@ -309,7 +309,7 @@ function renderWorkspaceCard(account: PortalAccountSummary, workspace: PortalWor } return ( - '
' + + '
' + '
' + '
' + '

' + escapeHTML(workspace.display_name) + '

' + @@ -511,16 +511,16 @@ function renderAccountWorkspaceSection(account: PortalAccountSummary, accountAPI '
' + '' + '

Keep account-wide actions close

' + - '

Use the management desk for workspace-level decisions, and keep billing or team changes one click away.

' + + '

Use the desk for one workspace at a time while keeping billing, team, and create-workspace actions within reach.

' + '
' + workspaceDeskActions + '
' + '
' + '
' + - '' + - '
    ' + - '
  • Lifecycle and health summary for the selected workspace
  • ' + - '
  • The next explicit lifecycle action for that workspace
  • ' + - '
  • Account-level actions when you need billing or team changes instead
  • ' + - '
' + + '' + + '
' + + '
1. Pick the workspaceSelect a fleet row to bring its health, lifecycle, and next action into this desk.
' + + '
2. Check posture firstTreat checking or needs-attention workspaces as review work, not routine operations.
' + + '
3. Escalate account changes separatelyBilling and team changes stay account-wide even when you are focused on one workspace.
' + + '
' + '
' + '
' + '
' + diff --git a/internal/cloudcp/portal/frontend/src/styles.css b/internal/cloudcp/portal/frontend/src/styles.css index e0649ddf1b..3437f513e4 100644 --- a/internal/cloudcp/portal/frontend/src/styles.css +++ b/internal/cloudcp/portal/frontend/src/styles.css @@ -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;