Skip to content

[CSM Portal] Config-driven, multi-resource dashboard widget system - #1316

Merged
Rashmika998 merged 16 commits into
wso2-open-operations:mainfrom
rksk:dashboard-widget-pilot
Aug 1, 2026
Merged

[CSM Portal] Config-driven, multi-resource dashboard widget system#1316
Rashmika998 merged 16 commits into
wso2-open-operations:mainfrom
rksk:dashboard-widget-pilot

Conversation

@rksk

@rksk rksk commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Purpose

The CSM portal's dashboard had no config-driven widget system and no way to list/switch dashboards from the backend — every KPI tile was hand-coded against /cases/search, the four non-engineer dashboards (Operations, IAM CS, Security center, Team performance) were FE-only mock tiles with pinned numbers, and the dashboard switcher was disabled. This PR replaces all of that with a single generic, config-driven widget system spanning any resource in the platform, not just cases.

Goals

  • GET /dashboards: a BE-driven list of every dashboard (id, display name, which one is default) — powers a real, always-enabled dashboard switcher instead of the previous hardcoded/disabled one.
  • GET /dashboards/{dashboardId}: a dashboard's metadata plus its widget templates in one call — each widget carries its resource type, display shape, a 12-column grid width, and its filter criteria (current-user placeholders resolved server-side). The backend never resolves widget data itself.
  • A widget's resourceType can be any of 9 kinds the platform already exposes search for: case, incident, change request, account, project, user, time card, problem, product vulnerability — not just cases. Filters are opaque JSON per resource, so a new widget is pure configuration, no new backend code.
  • The frontend fetches each widget's own data independently (its own React Query call, own skeleton, own error state — one widget's failure never affects a sibling) and renders either a count or a compact list depending on the widget's shape.
  • Click-through: each widget tile is a link to its resource's real listing page, with the widget's filters translated into that page's own URL query-param scheme where one exists today (cases, incidents, change requests).
  • Every dashboard now ships with real, if modest, widgets — the FE-side mock placeholder tiles (pinned numbers) are removed entirely, not kept as a fallback.

Approach

  • internal/dashboard/widgets.go: a static, ordered registry of 5 dashboards (agents_pilot/Engineer overview [default], Operations, IAM CS, Security center, Team performance) covering 5 different resource types across their widgets (case, incident, change_request, time_card, product_vulnerability). Each WidgetTemplate is {id, displayName, resourceType, shape, gridWidth, filters map[string]any, groupBy?, listLimit?}. shape supports count/list today; pie/bar exist in the enum for a future dashboard but are not wired — no /search endpoint in this codebase returns grouped/aggregate counts yet, so a bar/pie widget would have nothing real to render. ResolveFilters recursively substitutes a __current_user__ placeholder anywhere it appears in a widget's filter JSON, generic across all 9 resource types.
  • internal/handler/dashboards.go: GetDashboards (list) and GetDashboardDetail (metadata + widgets, replacing the earlier .../widgets-suffixed endpoint) — both require an authenticated user, GetDashboardDetail 404s on an unknown dashboard id.
  • Frontend (features/csm-dashboard): useDashboardList/useDashboard fetch the list and the selected dashboard; CsmDashboardPage.tsx derives the initial selection from whichever dashboard has isDefault: true and always renders the real widget grid (the old mock DashboardPlaceholder/TILE_SETS path is gone). widgetResourceConfig.ts is a per-resource-type table (search endpoint, list-row label extraction, click-through URL builder) — it reuses this app's existing casesFiltersUrl.ts/incidentsFiltersUrl.ts/changeRequestsFiltersUrl.ts translators for the 3 resources that already have URL-persisted filters on their listing pages, and falls back to an unfiltered navigate for the other 6 (account/project/user/time_card/problem/product_vulnerability), since those pages don't have a URL filter scheme yet. DashboardWidgetTile.tsx renders count or list shape and wraps the tile in a router link; the widget grid is a real 12-column CSS grid honoring each widget's gridWidth.
  • Known, accepted limitations (not defects): (1) pie/bar shapes are schema-ready but unimplemented pending a future aggregate/group-by endpoint; (2) click-through for account/project/user/time_card/problem/product_vulnerability widgets lands on the right page but can't pre-apply filters yet, since those listing pages have no URL filter scheme of their own today; (3) the ServiceNow DEV test identity used for verification has no active SN-agent record, so any widget filtering "assigned to me" returns a real, isolated per-widget error against that specific test account — verified as a test-environment identity-mapping gap, not a code defect (a team-wide widget with no user-identity filter resolves real data correctly against the same account).

No UI-facing screenshot is attached since this is backend-plus-data-flow work verified directly against real ServiceNow DEV data (see Automation tests) rather than a visual redesign.

User stories

As a CS engineer, I can switch between dashboards (Engineer overview, Operations, IAM CS, Security center, Team performance) from a real dropdown, land on my default dashboard automatically, see real counts/lists for each dashboard's widgets, and click a widget to jump straight to the filtered listing page behind it — instead of hardcoded mock KPI tiles with pinned numbers on every dashboard but one.

Release note

The CSM portal dashboard now has a config-driven widget system covering cases, incidents, change requests, accounts, projects, users, time cards, problems, and product vulnerabilities. All 5 dashboards (Engineer overview, Operations, IAM CS, Security center, Team performance) render real widgets via a BE-driven dashboard list and switcher; clicking a widget navigates to its filtered listing page where supported.

Documentation

N/A — internal architecture change; no product documentation references the previous dashboard's internals or the mock placeholder tiles being replaced.

Training

N/A — no training content references the CSM dashboard's internal implementation.

Certification

N/A — no certification exam content covers CSM dashboard internals.

Marketing

N/A — internal capability, not a standalone customer-facing feature announcement.

Automation tests

  • Unit tests

    Backend: go build/go vet/gofmt -l clean, go test ./... -v and make test all packages ok, including dashboards_test.go covering: dashboard-list auth/ordering/isDefault, dashboard-detail auth/404/current-user substitution across resource types, a product-vulnerability widget's scalar (non-array) filter value, and every registry dashboard having at least one widget. Frontend: npx tsc -b and eslint clean; vitest run src/features/csm-dashboard — 5 files / 17 tests passing; full suite vitest run — 703 passing, the same 9 pre-existing CaseActionBar.test.tsx failures unrelated to this change (confirmed present before this PR, not introduced by it).

  • Integration tests

    Verified live against real ServiceNow DEV (wso2sndev) through the full stack (real Asgardeo login, local Ballerina, Go entity-service, this BFF, this webapp): the dashboard switcher lists all 5 dashboards and defaults to Engineer overview; Security center resolves real live data across 2 different resource types (product vulnerabilities and cases) with zero errors; Operations resolves real live data across 3 different resource types (cases, incidents, change requests) with zero errors; each widget's rendered link carries the correctly translated filter query string for cases/incidents/change-requests (e.g. severity values translated from the entity-service's catastrophic|critical|... enum to the case-list's own S0/S1 URL codes).

Security checks

  • Followed secure coding standards in http://wso2.com/technical-reports/wso2-secure-engineering-guidelines? yes
  • Ran FindSecurityBugs plugin and verified report? N/A — this is a Go/TypeScript change, not Java; ran gosec instead (0 issues) and go vet/eslint clean.
  • Confirmed that this PR doesn't commit any keys, passwords, tokens, usernames, or other secrets? yes

Samples

N/A — no new sample apps or code samples are part of this change.

Related PRs

None.

Migrations (if applicable)

N/A — no schema or data migration; dashboard/widget definitions are static in-code configuration, not persisted data.

Test environment

Verified on macOS (host), Go (module toolchain per go.mod), Node/pnpm per package.json, against real ServiceNow DEV (wso2sndev.service-now.com) and real Asgardeo (dev tenant). No browser-matrix testing performed (internal CS-engineer tool, single supported browser per existing project convention).

Learning

Explored this repo's existing useCaseCountsMatrix.ts fan-out pattern (independent /cases/search calls with pagination.limit:1 per bucket) as the precedent for per-widget independent fetching. Surveyed every existing POST /*/search endpoint's real filter schema in openapi.yaml (rather than assuming) before designing the generic resource-type schema, which surfaced that SR/SRA are case.types values rather than separate resources, and that only cases/incidents/change-requests currently have URL-persisted filters on their listing pages.

Summary by CodeRabbit

  • New Features
    • Added configurable dashboards with dashboard selection and backend-provided names.
    • Added responsive widget tiles supporting counts and lists across cases, incidents, changes, problems, accounts, projects, users, time cards, and vulnerabilities.
    • Added dashboard filters, current-user filtering, configurable layouts, limits, loading states, empty states, and error handling.
    • Added navigation from widget results to relevant records.
  • Bug Fixes
    • Dashboard data now refreshes and remains consistent when switching dashboards or loading widget results.

rksk added 6 commits August 1, 2026 10:50
Static widget registry (My Patches, My Reminders, Open Incident (Team))
resolved through the existing /cases/search filter shape via a new
GET /dashboards/{dashboardId}/widgets endpoint, so the case-table
dashboards currently hosted on the backing data source can be replaced
incrementally without a new filter DSL or entity-service change.
Wires the new GET /dashboards/agents_pilot/widgets endpoint into one
shared query hook, renders its 3 single_score widgets as tiles with
per-tile skeleton/error states, and adds the pilot as a clearly
delimited add-on section below the existing CSM dashboard.
GetDashboardWidgets aborted the whole response on the first widget's
upstream search error, taking down unrelated widgets (e.g. a team-wide
widget with no dependency on the failing one). Each widget now resolves
independently and reports its own error without affecting siblings.
Replace MyAssignedCases/CaseCountsMatrix/CaseCompositionCharts with the
config-driven pilot widget section as the engineer dashboard's sole
content, instead of keeping it as an add-on below the old sections.
… data

GET /dashboards/{id}/widgets no longer calls the entity-service or computes
per-widget counts server-side. It returns each widget's display metadata plus
its resolved CaseSearchFilters (current-user placeholder substituted), so the
frontend runs each widget's own /cases/search independently.
…earch call

The widgets endpoint now returns only display metadata and filter criteria
(commit 40e6763), so each tile independently fetches its own count via
POST /cases/search instead of reading a pre-resolved value off the shared
list response. One tile's fetch failure no longer requires special-casing
against a shared query's success.
@coderabbitai

coderabbitai Bot commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@rksk, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 36 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: f5f5c409-1b62-4563-a21a-98945db07902

📥 Commits

Reviewing files that changed from the base of the PR and between e3bccb4 and 5c126a5.

📒 Files selected for processing (23)
  • apps/csm-portal/backend/.env.example
  • apps/csm-portal/backend/cmd/server/main.go
  • apps/csm-portal/backend/internal/dashboard/widgets.go
  • apps/csm-portal/backend/internal/dashboard/widgets_test.go
  • apps/csm-portal/backend/internal/handler/dashboards.go
  • apps/csm-portal/backend/internal/handler/dashboards_test.go
  • apps/csm-portal/backend/openapi.yaml
  • apps/csm-portal/webapp/src/api/backend/types.ts
  • apps/csm-portal/webapp/src/constants/apiConstants.ts
  • apps/csm-portal/webapp/src/features/csm-dashboard/api/useDashboard.test.tsx
  • apps/csm-portal/webapp/src/features/csm-dashboard/api/useDashboardList.test.tsx
  • apps/csm-portal/webapp/src/features/csm-dashboard/api/useDashboardList.ts
  • apps/csm-portal/webapp/src/features/csm-dashboard/api/useTeams.test.tsx
  • apps/csm-portal/webapp/src/features/csm-dashboard/api/useTeams.ts
  • apps/csm-portal/webapp/src/features/csm-dashboard/api/useWidgetData.ts
  • apps/csm-portal/webapp/src/features/csm-dashboard/components/AbtDashboardHeader.test.tsx
  • apps/csm-portal/webapp/src/features/csm-dashboard/components/AbtDashboardHeader.tsx
  • apps/csm-portal/webapp/src/features/csm-dashboard/components/DashboardWidgetTile.test.tsx
  • apps/csm-portal/webapp/src/features/csm-dashboard/components/DashboardWidgetTile.tsx
  • apps/csm-portal/webapp/src/features/csm-dashboard/config/widgetResourceConfig.ts
  • apps/csm-portal/webapp/src/features/csm-dashboard/pages/CsmDashboardPage.test.tsx
  • apps/csm-portal/webapp/src/features/csm-dashboard/pages/CsmDashboardPage.tsx
  • apps/csm-portal/webapp/src/features/csm-dashboard/types/abtDashboard.ts
📝 Walkthrough

Walkthrough

The PR adds backend dashboard definitions and authenticated metadata endpoints. The frontend now loads dashboard definitions, fetches widget data, supports dashboard switching, and renders configured count or list widgets with resource-specific navigation.

Changes

Dashboard configuration and API

Layer / File(s) Summary
Dashboard registry and filter resolution
apps/csm-portal/backend/internal/dashboard/widgets.go
Defines dashboard and widget contracts, registers five dashboards, resolves current-user placeholders, and preserves template filters.
Dashboard API exposure
apps/csm-portal/backend/cmd/server/main.go, apps/csm-portal/backend/internal/handler/*, apps/csm-portal/backend/openapi.yaml
Adds authenticated dashboard list and detail routes, response schemas, OpenAPI definitions, and backend coverage for authorization, metadata, filters, and registry entries.

Frontend dashboard flow

Layer / File(s) Summary
Frontend contracts and data hooks
apps/csm-portal/webapp/src/api/backend/types.ts, apps/csm-portal/webapp/src/constants/apiConstants.ts, apps/csm-portal/webapp/src/features/csm-dashboard/api/*, apps/csm-portal/webapp/src/features/csm-dashboard/config/*
Adds dashboard types, React Query hooks, widget data normalization, resource endpoints, label extraction, and navigation filter translation.
Dashboard selection and widget rendering
apps/csm-portal/webapp/src/features/csm-dashboard/pages/*, apps/csm-portal/webapp/src/features/csm-dashboard/components/*, apps/csm-portal/webapp/src/features/csm-dashboard/types/*
Replaces static dashboard options with backend-driven selection and renders configured widgets with loading, error, count, list, and navigation states.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant CsmDashboardPage
  participant DashboardHandler
  participant useDashboard
  participant DashboardWidgetTile
  participant SearchEndpoint
  CsmDashboardPage->>DashboardHandler: GET /dashboards
  DashboardHandler-->>CsmDashboardPage: dashboard list
  CsmDashboardPage->>useDashboard: load selected dashboard
  useDashboard->>DashboardHandler: GET /dashboards/{dashboardId}
  DashboardHandler-->>useDashboard: widget templates
  useDashboard->>DashboardWidgetTile: provide widget configuration
  DashboardWidgetTile->>SearchEndpoint: POST widget filters
  SearchEndpoint-->>DashboardWidgetTile: widget data
Loading

Possibly related PRs

Suggested labels: Type/New Feature

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 47.62% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: a config-driven dashboard widget system that supports multiple resources.
Description check ✅ Passed The description covers the required template sections with detailed purpose, implementation, testing, security, limitations, and environment information.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

rksk added 5 commits August 1, 2026 17:57
…docs

- Drop the dead Pagination struct ResolveFilters computed and the handler
  discarded; it now returns CaseSearchFilters directly, matching what
  actually ships (pagination is enforced client-side).
- Fix two stale doc comments left over from the dashboard-replacement
  commit (AgentsLandingPagePilot.tsx, CsmDashboardPage.tsx) that still
  described the pilot as an add-on beside sections it now replaces.
- Decode the widget-list test against the real dashboardWidgetView type
  instead of a duplicated ad hoc struct, and assert the response's JSON
  keys match openapi.yaml's DashboardWidget schema, so a field rename or
  add/remove can't drift past the test suite silently.
@rksk rksk changed the title [CSM Portal] Config-driven dashboard widget pilot (3 widgets, agents_pilot) [CSM Portal] Config-driven, multi-resource dashboard widget system Aug 1, 2026
@rksk
rksk marked this pull request as ready for review August 1, 2026 15:34

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (8)
apps/csm-portal/backend/internal/dashboard/widgets.go (1)

60-69: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove the unused json tags and widen the ResolveFilters doc comment.

WidgetTemplate is never marshaled. handler.dashboardWidgetView defines the wire shape, so json:",omitempty" on GroupBy and ListLimit has no effect and suggests a contract that does not exist. The ResolveFilters comment also states substitution applies to strings "inside a []any", but the registry stores []string and substituteCurrentUser handles []string, []any, and bare strings.

♻️ Proposed cleanup
-	GroupBy      string `json:",omitempty"` // only meaningful for Shape pie/bar — see the caveat on those consts; unused by every widget below
-	ListLimit    int    `json:",omitempty"` // only meaningful for Shape list; how many records to show
+	GroupBy      string // only meaningful for Shape pie/bar — see the caveat on those consts; unused by every widget below
+	ListLimit    int    // only meaningful for Shape list; how many records to show
 // ResolveFilters returns tpl's filters with CurrentUserPlaceholder substituted
-// by currentUserID wherever it appears as a string inside a []any (the only
-// place a per-user value belongs in a filters object — e.g. assignedUserIds,
-// userIds). It does not mutate tpl.Filters.
+// by currentUserID wherever it appears as a string, including inside []string
+// and []any values (e.g. assignedUserIds, userIds). It does not mutate
+// tpl.Filters.

Also applies to: 210-213

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/csm-portal/backend/internal/dashboard/widgets.go` around lines 60 - 69,
Remove the json:",omitempty" tags from WidgetTemplate.GroupBy and
WidgetTemplate.ListLimit, since dashboardWidgetView owns the serialized wire
shape. Update the ResolveFilters documentation to describe substitution for bare
strings and values within both []string and []any, matching
substituteCurrentUser’s supported inputs.
apps/csm-portal/backend/internal/handler/dashboards_test.go (1)

311-338: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Split the mixed-assertion loop and rename the shadowing loop variable.

Two readability points in otherwise correct tests:

  • Lines 311-338: the loop comment states widgets must not gain an assignedUserIds key, but my_critical_open asserts the opposite inside the same loop. Assert absence for open_incident_team directly, and keep the my_critical_open resolution check separate.
  • Lines 366-369 and 420-423: for _, w := range result.Widgets shadows the *httptest.ResponseRecorder named w. Rename the loop variable so a later assertion on the recorder inside the loop cannot silently target the wrong value.
♻️ Proposed refactor
-		// Widgets with no assignedUserIds field in their template must not
-		// gain one during substitution: substituteCurrentUser only rewrites
-		// values already present, it never adds keys.
-		for _, id := range []string{"open_incident_team", "my_critical_open"} {
-			idx, ok := byID[id]
-			if !ok {
-				t.Fatalf("missing widget %q in response", id)
-			}
-			if id == "my_critical_open" {
-				// my_critical_open DOES carry assignedUserIds (the current
-				// user's critical/high cases) — verify it resolved cleanly
-				// instead of asserting absence.
-				filters := result.Widgets[idx].Filters
-				assignedRaw, present := filters["assignedUserIds"]
-				if !present {
-					t.Fatalf("widget %s filters has no assignedUserIds key", id)
-				}
-				assigned, ok := assignedRaw.([]any)
-				if !ok || len(assigned) != 1 || assigned[0] != testUser.UserID {
-					t.Errorf("widget %s assignedUserIds = %v, want [%q]", id, assignedRaw, testUser.UserID)
-				}
-				continue
-			}
-			filters := result.Widgets[idx].Filters
-			if _, present := filters["assignedUserIds"]; present {
-				t.Errorf("widget %s filters unexpectedly has an assignedUserIds key: %v", id, filters["assignedUserIds"])
-			}
-		}
+		// open_incident_team has no assignedUserIds field in its template and
+		// must not gain one during substitution: substituteCurrentUser only
+		// rewrites values already present, it never adds keys.
+		teamFilters := result.Widgets[byID["open_incident_team"]].Filters
+		if v, present := teamFilters["assignedUserIds"]; present {
+			t.Errorf("widget open_incident_team filters unexpectedly has an assignedUserIds key: %v", v)
+		}
+
+		// my_critical_open DOES carry assignedUserIds (the current user's
+		// critical/high cases) — verify it resolved cleanly.
+		criticalFilters := result.Widgets[byID["my_critical_open"]].Filters
+		assignedRaw, present := criticalFilters["assignedUserIds"]
+		if !present {
+			t.Fatalf("widget my_critical_open filters has no assignedUserIds key")
+		}
+		if assigned, ok := assignedRaw.([]any); !ok || len(assigned) != 1 || assigned[0] != testUser.UserID {
+			t.Errorf("widget my_critical_open assignedUserIds = %v, want [%q]", assignedRaw, testUser.UserID)
+		}
 		byID := make(map[string]dashboardWidgetView)
-		for _, w := range result.Widgets {
-			byID[w.WidgetID] = w
-		}
+		for _, widget := range result.Widgets {
+			byID[widget.WidgetID] = widget
+		}

Also applies to: 366-369

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/csm-portal/backend/internal/handler/dashboards_test.go` around lines 311
- 338, Split the mixed loop in the dashboard test: assert that
open_incident_team lacks assignedUserIds directly, and keep the my_critical_open
resolution assertion in a separate block. In the loops over result.Widgets near
the affected assertions, rename the iteration variable from w to avoid shadowing
the *httptest.ResponseRecorder named w, updating references accordingly.
apps/csm-portal/webapp/src/features/csm-dashboard/pages/CsmDashboardPage.test.tsx (1)

68-89: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Test dashboard switching after opening the switcher.

This test verifies that the options render. It does not verify that selecting an option changes the dashboard.

Select Operations after opening the listbox. Assert that agents-landing-pilot receives operations.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/csm-portal/webapp/src/features/csm-dashboard/pages/CsmDashboardPage.test.tsx`
around lines 68 - 89, Extend the test around the opened dashboard switcher to
select the “Operations” option and verify that the dashboard display element
identified by “agents-landing-pilot” updates to “operations”. Keep the existing
option-rendering assertions and enabled-switcher checks unchanged.
apps/csm-portal/webapp/src/features/csm-dashboard/api/useDashboard.test.tsx (1)

46-76: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Use the current widget response contract in this test.

The fixture uses displayType: "single_score". BeDashboardWidget does not define this field. The widget renderer requires resourceType, shape, and gridWidth.

Create a fixture typed as BeDashboard. Include the current widget fields in both the mock and assertion.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/csm-portal/webapp/src/features/csm-dashboard/api/useDashboard.test.tsx`
around lines 46 - 76, Update the useDashboard test fixture and expected widget
data to use a BeDashboard-typed response, replacing the unsupported displayType
field with the renderer-required resourceType, shape, and gridWidth fields.
Apply the current widget response contract consistently in both
getMock.mockResolvedValue and the result.current.data.widgets assertion.
apps/csm-portal/webapp/src/features/csm-dashboard/components/AbtDashboardHeader.tsx (1)

109-123: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Add an accessible name to the dashboard Select.

The Select has no InputLabel/labelId pair and no aria-label. It was previously disabled, so the missing label had less impact. Now it is an active control, and a screen reader user has no accessible name for the dropdown.

Add aria-label="Select dashboard" (or wire up an InputLabel with labelId).

♿ Proposed fix
         <FormControl size="small" sx={{ minWidth: 200 }}>
           <Select
             value={dashboardKey}
             onChange={(e) =>
               onDashboardChange(e.target.value as DashboardKey)
             }
             displayEmpty
+            aria-label="Select dashboard"
           >
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/csm-portal/webapp/src/features/csm-dashboard/components/AbtDashboardHeader.tsx`
around lines 109 - 123, Add an accessible name to the active Select in the
dashboard header by setting aria-label to “Select dashboard”; keep the existing
dashboardKey value, onChange behavior, and menu options unchanged.
apps/csm-portal/webapp/src/features/csm-dashboard/components/DashboardWidgetTile.test.tsx (1)

43-156: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a test for the unsupported-shape fallback.

No test covers shape: "pie" or shape: "bar", which should render "Not yet supported." per DashboardWidgetTile.tsx Line 98-104. Since pie/bar rendering is called out as a known limitation of this PR, a small test guards the fallback message against accidental removal.

it('renders a "Not yet supported" message for shape: pie', async () => {
  postMock.mockResolvedValue({ total: 0, cases: [], limit: 1, offset: 0, hasMore: false });

  renderWithClient(
    <DashboardWidgetTile
      widgetId="unsupported"
      displayName="Unsupported Widget"
      resourceType="case"
      shape="pie"
      filters={{}}
    />,
  );

  await waitFor(() =>
    expect(screen.getByText("Not yet supported.")).toBeInTheDocument(),
  );
});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/csm-portal/webapp/src/features/csm-dashboard/components/DashboardWidgetTile.test.tsx`
around lines 43 - 156, Add a test in the DashboardWidgetTile test suite covering
an unsupported shape such as "pie". Mock the search response, render
DashboardWidgetTile with that shape, and wait for the "Not yet supported."
fallback text to appear, preserving the existing test setup and assertions.
apps/csm-portal/webapp/src/features/csm-dashboard/pages/CsmDashboardPage.tsx (1)

53-57: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Hardcoded "agents_pilot" reintroduces a fixed-ID dependency.

scopeBased checks the literal string "agents_pilot" against the now backend-driven dashboardKey. If the backend renames or removes that dashboard ID, this check silently stops matching — the ABT scope toggle disappears with no error, since nothing here validates that the ID still exists in dashboardList. The comment on Line 53-56 documents this as an intentional trade-off given the current single-dashboard scope-relevance, so this is a low-priority note rather than a defect: consider adding an isDefault-style flag from the backend (e.g. scopeRelevant) if a second scope-relevant dashboard is ever added, to avoid growing a list of hardcoded IDs.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/csm-portal/webapp/src/features/csm-dashboard/pages/CsmDashboardPage.tsx`
around lines 53 - 57, Replace the hardcoded "agents_pilot" check in the
scopeBased calculation with a backend-provided scope-relevance flag on the
dashboard data, such as scopeRelevant or an equivalent isDefault-style field.
Ensure the ABT scope toggle is driven by that flag and remains correct if
dashboard IDs are renamed or additional scope-relevant dashboards are
introduced.
apps/csm-portal/webapp/src/features/csm-dashboard/config/widgetResourceConfig.ts (1)

160-224: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract shared label helpers to remove duplication.

The primaryLabel logic for case, incident, change_request, and problem is identical: build a "number — subject" string. The secondaryLabel logic for case, change_request, and problem is also identical: humanize item.state. Extract two shared helper functions (e.g., numberSubjectLabel(item) and stateSecondaryLabel(item)) and reuse them across the four config entries.

♻️ Proposed refactor
+function numberSubjectLabel(item: WidgetItem): string {
+  return (
+    [asString(item.number), asString(item.subject)].filter(Boolean).join(" — ") ||
+    "—"
+  );
+}
+
+function stateSecondaryLabel(item: WidgetItem): string | undefined {
+  const state = asString(item.state);
+  return state ? humanizeState(state) : undefined;
+}
+
 export const WIDGET_RESOURCE_CONFIG: Record<
   BeWidgetResourceType,
   WidgetResourceConfig
 > = {
   case: {
     searchEndpoint: "/cases/search",
     itemsKey: "cases",
-    primaryLabel: (item) =>
-      [asString(item.number), asString(item.subject)]
-        .filter(Boolean)
-        .join(" — ") || "—",
-    secondaryLabel: (item) => {
-      const state = asString(item.state);
-      return state ? humanizeState(state) : undefined;
-    },
+    primaryLabel: numberSubjectLabel,
+    secondaryLabel: stateSecondaryLabel,
     buildHref: (filters) => casesHref(translateCaseDashboardFilters(filters)),
   },

Apply the same substitution to incident, change_request, and problem.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/csm-portal/webapp/src/features/csm-dashboard/config/widgetResourceConfig.ts`
around lines 160 - 224, Extract shared helpers for the repeated
number-and-subject primary label and state-based secondary label logic, using
symbols such as numberSubjectLabel and stateSecondaryLabel. Replace the
duplicated primaryLabel implementations in case, incident, change_request, and
problem, and replace the duplicated state secondaryLabel implementations in
case, change_request, and problem; preserve incident’s priority secondary label
unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/csm-portal/backend/internal/handler/dashboards_test.go`:
- Around line 64-67: Update the documentation comment above
assertJSONKeysSuperset to use the correct helper name and describe that the
object must contain every key in want, making the object a superset of the
requested keys.

In `@apps/csm-portal/backend/openapi.yaml`:
- Around line 1666-1672: Update the dashboard endpoint description to remove the
cases-only “POST /cases/search” reference and describe widget filters as
targeting their configured resource types, consistent with the
DashboardWidget.filters description. Preserve the existing explanation that
callers resolve each widget’s data through the corresponding search request and
that templates are a static registry.

In `@apps/csm-portal/webapp/src/features/csm-dashboard/api/useDashboardList.ts`:
- Around line 37-40: Update the queryFn in useDashboardList so a null response
from api.get indicates a missing dashboard endpoint and throws an error instead
of returning an empty array. Preserve the existing array response behavior,
allowing the query to enter its error state and preventing CsmDashboardPage from
remaining in the loading state.

In
`@apps/csm-portal/webapp/src/features/csm-dashboard/components/DashboardWidgetTile.tsx`:
- Around line 57-58: Guard the widget configuration lookup in
DashboardWidgetTile so an unrecognized resourceType does not access
config.buildHref or throw during render. Reuse the existing runtime validation
flow to treat missing WIDGET_RESOURCE_CONFIG entries as invalid, and ensure the
same guard covers the WidgetListBody and useWidgetData paths that access this
configuration.

---

Nitpick comments:
In `@apps/csm-portal/backend/internal/dashboard/widgets.go`:
- Around line 60-69: Remove the json:",omitempty" tags from
WidgetTemplate.GroupBy and WidgetTemplate.ListLimit, since dashboardWidgetView
owns the serialized wire shape. Update the ResolveFilters documentation to
describe substitution for bare strings and values within both []string and
[]any, matching substituteCurrentUser’s supported inputs.

In `@apps/csm-portal/backend/internal/handler/dashboards_test.go`:
- Around line 311-338: Split the mixed loop in the dashboard test: assert that
open_incident_team lacks assignedUserIds directly, and keep the my_critical_open
resolution assertion in a separate block. In the loops over result.Widgets near
the affected assertions, rename the iteration variable from w to avoid shadowing
the *httptest.ResponseRecorder named w, updating references accordingly.

In `@apps/csm-portal/webapp/src/features/csm-dashboard/api/useDashboard.test.tsx`:
- Around line 46-76: Update the useDashboard test fixture and expected widget
data to use a BeDashboard-typed response, replacing the unsupported displayType
field with the renderer-required resourceType, shape, and gridWidth fields.
Apply the current widget response contract consistently in both
getMock.mockResolvedValue and the result.current.data.widgets assertion.

In
`@apps/csm-portal/webapp/src/features/csm-dashboard/components/AbtDashboardHeader.tsx`:
- Around line 109-123: Add an accessible name to the active Select in the
dashboard header by setting aria-label to “Select dashboard”; keep the existing
dashboardKey value, onChange behavior, and menu options unchanged.

In
`@apps/csm-portal/webapp/src/features/csm-dashboard/components/DashboardWidgetTile.test.tsx`:
- Around line 43-156: Add a test in the DashboardWidgetTile test suite covering
an unsupported shape such as "pie". Mock the search response, render
DashboardWidgetTile with that shape, and wait for the "Not yet supported."
fallback text to appear, preserving the existing test setup and assertions.

In
`@apps/csm-portal/webapp/src/features/csm-dashboard/config/widgetResourceConfig.ts`:
- Around line 160-224: Extract shared helpers for the repeated
number-and-subject primary label and state-based secondary label logic, using
symbols such as numberSubjectLabel and stateSecondaryLabel. Replace the
duplicated primaryLabel implementations in case, incident, change_request, and
problem, and replace the duplicated state secondaryLabel implementations in
case, change_request, and problem; preserve incident’s priority secondary label
unchanged.

In
`@apps/csm-portal/webapp/src/features/csm-dashboard/pages/CsmDashboardPage.test.tsx`:
- Around line 68-89: Extend the test around the opened dashboard switcher to
select the “Operations” option and verify that the dashboard display element
identified by “agents-landing-pilot” updates to “operations”. Keep the existing
option-rendering assertions and enabled-switcher checks unchanged.

In
`@apps/csm-portal/webapp/src/features/csm-dashboard/pages/CsmDashboardPage.tsx`:
- Around line 53-57: Replace the hardcoded "agents_pilot" check in the
scopeBased calculation with a backend-provided scope-relevance flag on the
dashboard data, such as scopeRelevant or an equivalent isDefault-style field.
Ensure the ABT scope toggle is driven by that flag and remains correct if
dashboard IDs are renamed or additional scope-relevant dashboards are
introduced.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 0da5eec1-0d7d-4701-b8de-3bf52ae8d682

📥 Commits

Reviewing files that changed from the base of the PR and between e0a8f41 and e3bccb4.

📒 Files selected for processing (22)
  • apps/csm-portal/backend/cmd/server/main.go
  • apps/csm-portal/backend/internal/dashboard/widgets.go
  • apps/csm-portal/backend/internal/handler/dashboards.go
  • apps/csm-portal/backend/internal/handler/dashboards_test.go
  • apps/csm-portal/backend/internal/handler/response.go
  • apps/csm-portal/backend/openapi.yaml
  • apps/csm-portal/webapp/src/api/backend/types.ts
  • apps/csm-portal/webapp/src/constants/apiConstants.ts
  • apps/csm-portal/webapp/src/features/csm-dashboard/api/useDashboard.test.tsx
  • apps/csm-portal/webapp/src/features/csm-dashboard/api/useDashboard.ts
  • apps/csm-portal/webapp/src/features/csm-dashboard/api/useDashboardList.test.tsx
  • apps/csm-portal/webapp/src/features/csm-dashboard/api/useDashboardList.ts
  • apps/csm-portal/webapp/src/features/csm-dashboard/api/useWidgetData.ts
  • apps/csm-portal/webapp/src/features/csm-dashboard/components/AbtDashboardHeader.tsx
  • apps/csm-portal/webapp/src/features/csm-dashboard/components/AgentsLandingPagePilot.test.tsx
  • apps/csm-portal/webapp/src/features/csm-dashboard/components/AgentsLandingPagePilot.tsx
  • apps/csm-portal/webapp/src/features/csm-dashboard/components/DashboardWidgetTile.test.tsx
  • apps/csm-portal/webapp/src/features/csm-dashboard/components/DashboardWidgetTile.tsx
  • apps/csm-portal/webapp/src/features/csm-dashboard/config/widgetResourceConfig.ts
  • apps/csm-portal/webapp/src/features/csm-dashboard/pages/CsmDashboardPage.test.tsx
  • apps/csm-portal/webapp/src/features/csm-dashboard/pages/CsmDashboardPage.tsx
  • apps/csm-portal/webapp/src/features/csm-dashboard/types/abtDashboard.ts

Comment thread apps/csm-portal/backend/internal/handler/dashboards_test.go Outdated
Comment thread apps/csm-portal/backend/openapi.yaml Outdated
rksk added 4 commits August 1, 2026 21:14
Dashboards are selected purely by dropdown now; ABT scoping was never
implemented and no dashboard has any other per-dashboard behavior beyond
which one is selected.
A dashboard can now be flagged isTeamBased; when selected, the header shows
a team selector sourced from POST /teams/search alongside the dashboard
switcher. Selecting a team is UI state only for now - it does not yet scope
any widget's data, which is a deliberately deferred follow-up.
- Guard against an unrecognized resourceType from the (now runtime-JSON)
  dashboard config crashing DashboardWidgetTile's render or useWidgetData's
  query; render/report "unsupported" instead.
- useDashboardList: a null response (api.get's 404 sentinel) now surfaces as
  a query error instead of silently becoming an empty dashboard registry;
  CsmDashboardPage shows an error state instead of an infinite skeleton.
- Fix a doc-comment name mismatch (assertJSONKeysSubset -> Superset), split
  a mixed-assertion test loop, and de-shadow a loop variable named the same
  as an outer *httptest.ResponseRecorder.
- Correct two stale openapi.yaml descriptions still referencing
  "POST /cases/search" and "not user-configurable" from before the generic
  multi-resource schema and the DASHBOARDS_CONFIG move.
- Fix a stale useDashboard.test.tsx fixture still using the removed
  displayType field instead of resourceType/shape/gridWidth.
- Extract shared numberSubjectLabel/stateSecondaryLabel helpers in
  widgetResourceConfig.ts, removing duplication across 4 resource configs.
- Add aria-label to both dashboard header Selects (the dashboard switcher
  and the new team selector).
- Add coverage: dashboard switching via the switcher, the dashboard-list
  error state, the pie/bar not-yet-supported fallback, and the unrecognized-
  resourceType guard.

Not applied: a suggestion to drop json tags from WidgetTemplate.GroupBy/
ListLimit is stale post-DASHBOARDS_CONFIG -- those tags are load-bearing
now (ParseDashboardsConfig unmarshals directly into WidgetTemplate).
@rksk

rksk commented Aug 1, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai resume

@coderabbitai

coderabbitai Bot commented Aug 1, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Reviews resumed.

@rksk

rksk commented Aug 1, 2026

Copy link
Copy Markdown
Contributor Author

For reference, here's the DASHBOARDS_CONFIG value documented in .env.example (the pilot's 5 dashboards), pretty-printed:

[
  {
    "id": "engineer_overview",
    "displayName": "Engineer overview",
    "isDefault": true,
    "targetTeam": "cs_engineers",
    "widgets": [
      {
        "id": "my_patches",
        "displayName": "My Patches",
        "resourceType": "case",
        "shape": "count",
        "gridWidth": 3,
        "filters": {
          "assignedUserIds": ["__current_user__"],
          "tags": ["patch"],
          "states": ["open", "work_in_progress", "waiting_on_wso2", "reopened", "awaiting_info"]
        }
      },
      {
        "id": "my_reminders",
        "displayName": "My Reminders",
        "resourceType": "case",
        "shape": "count",
        "gridWidth": 3,
        "filters": {
          "assignedUserIds": ["__current_user__"],
          "states": ["awaiting_info", "solution_proposed"]
        }
      },
      {
        "id": "open_incident_team",
        "displayName": "Open Incident (Team)",
        "resourceType": "case",
        "shape": "count",
        "gridWidth": 3,
        "filters": {
          "tags": ["s_dip"],
          "states": ["work_in_progress", "open", "waiting_on_wso2", "reopened"]
        }
      },
      {
        "id": "my_critical_open",
        "displayName": "My Critical & High Cases",
        "resourceType": "case",
        "shape": "list",
        "gridWidth": 3,
        "listLimit": 5,
        "filters": {
          "assignedUserIds": ["__current_user__"],
          "severities": ["catastrophic", "critical"],
          "states": ["open", "work_in_progress"]
        }
      }
    ]
  },
  {
    "id": "operations",
    "displayName": "Operations",
    "targetTeam": "cs_operations",
    "widgets": [
      {
        "id": "p0_p1_open",
        "displayName": "P0/P1 Open",
        "resourceType": "case",
        "shape": "count",
        "gridWidth": 4,
        "filters": {
          "severities": ["catastrophic", "critical"],
          "states": ["open", "work_in_progress"]
        }
      },
      {
        "id": "open_critical_incidents",
        "displayName": "Open Critical Incidents",
        "resourceType": "incident",
        "shape": "count",
        "gridWidth": 4,
        "filters": { "priorities": ["CRITICAL", "HIGH"] }
      },
      {
        "id": "crs_awaiting_approval",
        "displayName": "CRs Awaiting Approval",
        "resourceType": "change_request",
        "shape": "count",
        "gridWidth": 4,
        "filters": { "states": ["customer_approval"] }
      }
    ]
  },
  {
    "id": "iam",
    "displayName": "IAM CS",
    "targetTeam": "iam_cs",
    "widgets": [
      {
        "id": "iam_open_cases",
        "displayName": "IAM Open Cases",
        "resourceType": "case",
        "shape": "count",
        "gridWidth": 6,
        "filters": {
          "tags": ["iam"],
          "states": ["open", "work_in_progress", "awaiting_info"]
        }
      },
      {
        "id": "asgardeo_open_cases",
        "displayName": "Asgardeo Open Cases",
        "resourceType": "case",
        "shape": "count",
        "gridWidth": 6,
        "filters": {
          "tags": ["asgardeo"],
          "states": ["open", "work_in_progress", "awaiting_info"]
        }
      }
    ]
  },
  {
    "id": "security",
    "displayName": "Security center",
    "targetTeam": "security",
    "widgets": [
      {
        "id": "critical_vulns",
        "displayName": "Critical Vulnerabilities",
        "resourceType": "product_vulnerability",
        "shape": "count",
        "gridWidth": 4,
        "filters": { "priority": "critical" }
      },
      {
        "id": "high_vulns",
        "displayName": "High Vulnerabilities",
        "resourceType": "product_vulnerability",
        "shape": "count",
        "gridWidth": 4,
        "filters": { "priority": "high" }
      },
      {
        "id": "sra_cases_open",
        "displayName": "Open SRAs",
        "resourceType": "case",
        "shape": "count",
        "gridWidth": 4,
        "filters": {
          "types": ["security_report_analysis"],
          "states": ["open", "work_in_progress", "awaiting_info"]
        }
      }
    ]
  },
  {
    "id": "team_performance",
    "displayName": "Team performance",
    "targetTeam": "cs_team_leads",
    "isTeamBased": true,
    "widgets": [
      {
        "id": "time_cards_pending_approval",
        "displayName": "Time Cards Pending Approval",
        "resourceType": "time_card",
        "shape": "count",
        "gridWidth": 6,
        "filters": { "states": ["pending"] }
      },
      {
        "id": "team_open_cases",
        "displayName": "Team Open P0/P1",
        "resourceType": "case",
        "shape": "count",
        "gridWidth": 6,
        "filters": {
          "severities": ["catastrophic", "critical"],
          "states": ["open", "work_in_progress"]
        }
      }
    ]
  }
]

This is the same value the one-line DASHBOARDS_CONFIG='...' in .env.example carries — reproduced here formatted for easier review.

…t the JWT claim

The dashboard handler substituted user.UserID (the raw JWT userid claim,
whatever identity value the gateway/IdP embeds) into a widget's
assignedUserIds. That is not the platform's own SN/Postgres-backed user id --
GET /users/me resolves a different id via the entity service. Real cases
against wso2sndev with a JWT-claim id looked like this:

  "no active user found for sys_id 'f2d9bf5b-...'"

This had been treated as an accepted ServiceNow DEV environment/test-identity
limitation throughout this task. It was not: the id substituted was simply
wrong. DashboardHandler now calls the same entity GetUserMe the users.go
handler already uses for GET /users/me and substitutes that resolved id
instead, falling back to the JWT claim only if the entity lookup itself
fails (so a transient entity-service error still returns 200, not 500 --
dashboards are best-effort, not core functionality).

Verified live against real wso2sndev: My Patches (56), My Reminders (3), and
My Critical & High Cases (a real 5-case list) all now resolve real data --
previously all three failed with "Could not load this widget."
@Rashmika998
Rashmika998 merged commit 5ecbfc6 into wso2-open-operations:main Aug 1, 2026
1 check passed
@rksk
rksk deleted the dashboard-widget-pilot branch August 1, 2026 17:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants