[CSM Portal] Config-driven, multi-resource dashboard widget system - #1316
Conversation
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.
|
Warning Review limit reached
Next review available in: 36 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the 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 configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (23)
📝 WalkthroughWalkthroughThe 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. ChangesDashboard configuration and API
Frontend dashboard flow
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
Possibly related PRs
Suggested labels: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
…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.
…o per-dashboard detail
…ion from the backend
…eal widgets for every dashboard
…through navigation, remove mock dashboards
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (8)
apps/csm-portal/backend/internal/dashboard/widgets.go (1)
60-69: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueRemove the unused
jsontags and widen theResolveFiltersdoc comment.
WidgetTemplateis never marshaled.handler.dashboardWidgetViewdefines the wire shape, sojson:",omitempty"onGroupByandListLimithas no effect and suggests a contract that does not exist. TheResolveFilterscomment also states substitution applies to strings "inside a []any", but the registry stores[]stringandsubstituteCurrentUserhandles[]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 valueSplit 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
assignedUserIdskey, butmy_critical_openasserts the opposite inside the same loop. Assert absence foropen_incident_teamdirectly, and keep themy_critical_openresolution check separate.- Lines 366-369 and 420-423:
for _, w := range result.Widgetsshadows the*httptest.ResponseRecordernamedw. 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 winTest 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
Operationsafter opening the listbox. Assert thatagents-landing-pilotreceivesoperations.🤖 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 winUse the current widget response contract in this test.
The fixture uses
displayType: "single_score".BeDashboardWidgetdoes not define this field. The widget renderer requiresresourceType,shape, andgridWidth.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 winAdd an accessible name to the dashboard
Select.The
Selecthas noInputLabel/labelIdpair and noaria-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 anInputLabelwithlabelId).♿ 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 winAdd a test for the unsupported-shape fallback.
No test covers
shape: "pie"orshape: "bar", which should render "Not yet supported." perDashboardWidgetTile.tsxLine 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 valueHardcoded
"agents_pilot"reintroduces a fixed-ID dependency.
scopeBasedchecks the literal string"agents_pilot"against the now backend-drivendashboardKey. 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 indashboardList. 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 anisDefault-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 winExtract shared label helpers to remove duplication.
The
primaryLabellogic forcase,incident,change_request, andproblemis identical: build a "number — subject" string. ThesecondaryLabellogic forcase,change_request, andproblemis also identical: humanizeitem.state. Extract two shared helper functions (e.g.,numberSubjectLabel(item)andstateSecondaryLabel(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, andproblem.🤖 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
📒 Files selected for processing (22)
apps/csm-portal/backend/cmd/server/main.goapps/csm-portal/backend/internal/dashboard/widgets.goapps/csm-portal/backend/internal/handler/dashboards.goapps/csm-portal/backend/internal/handler/dashboards_test.goapps/csm-portal/backend/internal/handler/response.goapps/csm-portal/backend/openapi.yamlapps/csm-portal/webapp/src/api/backend/types.tsapps/csm-portal/webapp/src/constants/apiConstants.tsapps/csm-portal/webapp/src/features/csm-dashboard/api/useDashboard.test.tsxapps/csm-portal/webapp/src/features/csm-dashboard/api/useDashboard.tsapps/csm-portal/webapp/src/features/csm-dashboard/api/useDashboardList.test.tsxapps/csm-portal/webapp/src/features/csm-dashboard/api/useDashboardList.tsapps/csm-portal/webapp/src/features/csm-dashboard/api/useWidgetData.tsapps/csm-portal/webapp/src/features/csm-dashboard/components/AbtDashboardHeader.tsxapps/csm-portal/webapp/src/features/csm-dashboard/components/AgentsLandingPagePilot.test.tsxapps/csm-portal/webapp/src/features/csm-dashboard/components/AgentsLandingPagePilot.tsxapps/csm-portal/webapp/src/features/csm-dashboard/components/DashboardWidgetTile.test.tsxapps/csm-portal/webapp/src/features/csm-dashboard/components/DashboardWidgetTile.tsxapps/csm-portal/webapp/src/features/csm-dashboard/config/widgetResourceConfig.tsapps/csm-portal/webapp/src/features/csm-dashboard/pages/CsmDashboardPage.test.tsxapps/csm-portal/webapp/src/features/csm-dashboard/pages/CsmDashboardPage.tsxapps/csm-portal/webapp/src/features/csm-dashboard/types/abtDashboard.ts
… var instead of Go code
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).
|
@coderabbitai resume |
✅ Action performedReviews resumed. |
|
For reference, here's the [
{
"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 |
…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."
Purpose
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.resourceTypecan 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.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). EachWidgetTemplateis{id, displayName, resourceType, shape, gridWidth, filters map[string]any, groupBy?, listLimit?}.shapesupportscount/listtoday;pie/barexist in the enum for a future dashboard but are not wired — no/searchendpoint in this codebase returns grouped/aggregate counts yet, so a bar/pie widget would have nothing real to render.ResolveFiltersrecursively 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) andGetDashboardDetail(metadata + widgets, replacing the earlier.../widgets-suffixed endpoint) — both require an authenticated user,GetDashboardDetail404s on an unknown dashboard id.features/csm-dashboard):useDashboardList/useDashboardfetch the list and the selected dashboard;CsmDashboardPage.tsxderives the initial selection from whichever dashboard hasisDefault: trueand always renders the real widget grid (the old mockDashboardPlaceholder/TILE_SETSpath is gone).widgetResourceConfig.tsis a per-resource-type table (search endpoint, list-row label extraction, click-through URL builder) — it reuses this app's existingcasesFiltersUrl.ts/incidentsFiltersUrl.ts/changeRequestsFiltersUrl.tstranslators 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.tsxrenders 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'sgridWidth.pie/barshapes 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
Release note
Documentation
Training
Certification
Marketing
Automation tests
Security checks
gosecinstead (0 issues) andgo vet/eslintclean.Samples
Related PRs
Migrations (if applicable)
Test environment
Learning
Summary by CodeRabbit