diff --git a/apps/csm-portal/backend/.env.example b/apps/csm-portal/backend/.env.example
index b00df0428e..2aa24b1dbf 100644
--- a/apps/csm-portal/backend/.env.example
+++ b/apps/csm-portal/backend/.env.example
@@ -53,8 +53,10 @@ SCIM_SCOPES=
# restarting the backend process. Left unset or malformed, GET /dashboards
# returns an empty list and GET /dashboards/{id} 404s for every id; startup
# and every other endpoint work normally (an error is logged, not a crash).
-# Example (the pilot's 5 dashboards):
-# DASHBOARDS_CONFIG='[{"id":"agents_pilot","displayName":"Engineer overview","isDefault":true,"targetTeam":"cs_engineers","widgets":[{"id":"my_patches","displayName":"My Patches","resourceType":"case","shape":"count","gridWidth":3,"filters":{"filters":[{"field":"assignedUserId","op":"in","values":["__current_user__"]},{"field":"tag","op":"in","values":["patch"]},{"field":"state","op":"in","values":["open","work_in_progress","waiting_on_wso2","reopened","awaiting_info"]}]}},{"id":"my_reminders","displayName":"My Reminders","resourceType":"case","shape":"count","gridWidth":3,"filters":{"filters":[{"field":"assignedUserId","op":"in","values":["__current_user__"]},{"field":"state","op":"in","values":["awaiting_info","solution_proposed"]}]}},{"id":"open_incident_team","displayName":"Open Incident (Team)","resourceType":"case","shape":"count","gridWidth":3,"filters":{"filters":[{"field":"tag","op":"in","values":["s_dip"]},{"field":"state","op":"in","values":["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":{"filters":[{"field":"assignedUserId","op":"in","values":["__current_user__"]},{"field":"severity","op":"in","values":["catastrophic","critical"]},{"field":"state","op":"in","values":["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":{"filters":[{"field":"severity","op":"in","values":["catastrophic","critical"]},{"field":"state","op":"in","values":["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":{"filters":[{"field":"tag","op":"in","values":["iam"]},{"field":"state","op":"in","values":["open","work_in_progress","awaiting_info"]}]}},{"id":"asgardeo_open_cases","displayName":"Asgardeo Open Cases","resourceType":"case","shape":"count","gridWidth":6,"filters":{"filters":[{"field":"tag","op":"in","values":["asgardeo"]},{"field":"state","op":"in","values":["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":{"filters":[{"field":"type","op":"in","values":["security_report_analysis"]},{"field":"state","op":"in","values":["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":{"filters":[{"field":"severity","op":"in","values":["catastrophic","critical"]},{"field":"state","op":"in","values":["open","work_in_progress"]}]}}]}]'
+# Example (illustrative only — a real deployment's value is set directly in
+# its own environment, not committed here, and is not required to resemble
+# this shape beyond the schema):
+# DASHBOARDS_CONFIG='[{"id":"sample-dashboard","displayName":"Sample Dashboard","isDefault":true,"targetTeam":"sample-team","widgets":[{"id":"my-open-cases","displayName":"My Open Cases","resourceType":"case","shape":"count","gridWidth":3,"filters":{"filters":[{"field":"assignedUserId","op":"in","values":["__current_user__"]},{"field":"state","op":"in","values":["open","work_in_progress"]}]}},{"id":"recent-cases","displayName":"Recent Cases","resourceType":"case","shape":"list","gridWidth":6,"listLimit":5,"filters":{"filters":[{"field":"tag","op":"in","values":["example-tag"]},{"field":"tag","op":"notIn","values":["excluded-example-tag"]}]}},{"id":"pending-time-cards","displayName":"Pending Time Cards","resourceType":"time_card","shape":"count","gridWidth":3,"filters":{"states":["pending"]}}]},{"id":"sample-team-dashboard","displayName":"Sample Team Dashboard","targetTeam":"sample-team","isTeamBased":true,"widgets":[{"id":"team-open-cases","displayName":"Team Open Cases","section":"Overview","resourceType":"case","shape":"count","gridWidth":4,"filters":{"filters":[{"field":"severity","op":"in","values":["critical","high"]},{"field":"state","op":"in","values":["open","work_in_progress"]}]}},{"id":"unassigned-cases","displayName":"Unassigned Cases","section":"Overview","resourceType":"case","shape":"count","gridWidth":4,"filters":{"filters":[{"field":"assignedUserId","op":"isEmpty"},{"field":"state","op":"in","values":["open"]}]}},{"id":"cases-by-severity","displayName":"Cases by Severity","description":"Share of active cases at each severity level.","resourceType":"case","shape":"pie","gridWidth":4,"filters":{"filters":[{"field":"state","op":"in","values":["open","work_in_progress"]}]},"slices":[{"label":"Critical","color":"error","filters":{"filters":[{"field":"severity","op":"in","values":["critical"]}]}},{"label":"Mine","filters":{"filters":[{"field":"assignedUserId","op":"in","values":["__current_user__"]}]}}]}]}]'
# Auth — set to false for local testing (skips JWT signature verification)
AUTH_JWKS_ENDPOINT=
diff --git a/apps/csm-portal/backend/internal/dashboard/widgets_test.go b/apps/csm-portal/backend/internal/dashboard/widgets_test.go
index 53755a464c..20147acd1a 100644
--- a/apps/csm-portal/backend/internal/dashboard/widgets_test.go
+++ b/apps/csm-portal/backend/internal/dashboard/widgets_test.go
@@ -44,21 +44,21 @@ func TestParseDashboardsConfig_MalformedShape(t *testing.T) {
func TestParseDashboardsConfig_ValidRoundTrip(t *testing.T) {
const raw = `[
{
- "id": "agents_pilot",
- "displayName": "Engineer overview",
+ "id": "sample-dashboard",
+ "displayName": "Sample Dashboard",
"isDefault": true,
- "targetTeam": "cs_engineers",
+ "targetTeam": "sample-team",
"widgets": [
{
- "id": "my_patches",
- "displayName": "My Patches",
+ "id": "my-open-cases",
+ "displayName": "My Open Cases",
"resourceType": "case",
"shape": "count",
"gridWidth": 3,
"filters": {
"filters": [
{"field": "assignedUserId", "op": "in", "values": ["__current_user__"]},
- {"field": "tag", "op": "in", "values": ["patch"]},
+ {"field": "tag", "op": "in", "values": ["example-tag"]},
{"field": "state", "op": "in", "values": ["open", "work_in_progress"]}
]
}
@@ -73,25 +73,25 @@ func TestParseDashboardsConfig_ValidRoundTrip(t *testing.T) {
}
d := got[0]
- if d.ID != "agents_pilot" {
- t.Errorf("Dashboard.ID = %q, want %q", d.ID, "agents_pilot")
+ if d.ID != "sample-dashboard" {
+ t.Errorf("Dashboard.ID = %q, want %q", d.ID, "sample-dashboard")
}
- if d.DisplayName != "Engineer overview" {
- t.Errorf("Dashboard.DisplayName = %q, want %q", d.DisplayName, "Engineer overview")
+ if d.DisplayName != "Sample Dashboard" {
+ t.Errorf("Dashboard.DisplayName = %q, want %q", d.DisplayName, "Sample Dashboard")
}
if !d.IsDefault {
t.Errorf("Dashboard.IsDefault = false, want true")
}
- if d.TargetTeam != "cs_engineers" {
- t.Errorf("Dashboard.TargetTeam = %q, want %q", d.TargetTeam, "cs_engineers")
+ if d.TargetTeam != "sample-team" {
+ t.Errorf("Dashboard.TargetTeam = %q, want %q", d.TargetTeam, "sample-team")
}
if len(d.Widgets) != 1 {
t.Fatalf("len(Dashboard.Widgets) = %d, want 1", len(d.Widgets))
}
w := d.Widgets[0]
- if w.ID != "my_patches" {
- t.Errorf("WidgetTemplate.ID = %q, want %q", w.ID, "my_patches")
+ if w.ID != "my-open-cases" {
+ t.Errorf("WidgetTemplate.ID = %q, want %q", w.ID, "my-open-cases")
}
if w.ResourceType != ResourceCase {
t.Errorf("WidgetTemplate.ResourceType = %q, want %q", w.ResourceType, ResourceCase)
@@ -147,12 +147,12 @@ func TestParseDashboardsConfig_ValidRoundTrip(t *testing.T) {
func TestParseDashboardsConfig_PieWidgetSlicesAndDescription(t *testing.T) {
const raw = `[
{
- "id": "team_performance",
- "displayName": "Team performance",
+ "id": "sample-team-dashboard",
+ "displayName": "Sample Team Dashboard",
"widgets": [
{
- "id": "cases_by_severity",
- "displayName": "Cases by severity",
+ "id": "cases-by-severity",
+ "displayName": "Cases by Severity",
"description": "Share of active cases at each severity level.",
"resourceType": "case",
"shape": "pie",
@@ -199,11 +199,11 @@ func TestParseDashboardsConfig_PieWidgetSlicesAndDescription(t *testing.T) {
func TestParseDashboardsConfig_WidgetSection(t *testing.T) {
const raw = `[
{
- "id": "team_performance",
- "displayName": "Team performance",
+ "id": "sample-team-dashboard",
+ "displayName": "Sample Team Dashboard",
"widgets": [
- {"id": "team_open_cases", "displayName": "Team Open P0/P1", "resourceType": "case", "shape": "count", "gridWidth": 6, "filters": {}},
- {"id": "incident_wow", "displayName": "Incident WOW", "section": "SLA Violation", "resourceType": "incident", "shape": "count", "gridWidth": 6, "filters": {}}
+ {"id": "team-open-cases", "displayName": "Team Open Cases", "resourceType": "case", "shape": "count", "gridWidth": 6, "filters": {}},
+ {"id": "escalated-incidents", "displayName": "Escalated Incidents", "section": "Escalations", "resourceType": "incident", "shape": "count", "gridWidth": 6, "filters": {}}
]
}
]`
@@ -214,9 +214,9 @@ func TestParseDashboardsConfig_WidgetSection(t *testing.T) {
}
if section := got[0].Widgets[0].Section; section != "" {
- t.Errorf("team_open_cases.Section = %q, want empty (no section configured)", section)
+ t.Errorf("team-open-cases.Section = %q, want empty (no section configured)", section)
}
- if section := got[0].Widgets[1].Section; section != "SLA Violation" {
- t.Errorf("incident_wow.Section = %q, want %q", section, "SLA Violation")
+ if section := got[0].Widgets[1].Section; section != "Escalations" {
+ t.Errorf("escalated-incidents.Section = %q, want %q", section, "Escalations")
}
}
diff --git a/apps/csm-portal/backend/internal/handler/dashboards_test.go b/apps/csm-portal/backend/internal/handler/dashboards_test.go
index 1b03de2d10..886308be43 100644
--- a/apps/csm-portal/backend/internal/handler/dashboards_test.go
+++ b/apps/csm-portal/backend/internal/handler/dashboards_test.go
@@ -31,42 +31,34 @@ import (
"github.com/wso2-open-operations/cs-tools/apps/csm-portal/backend/internal/dashboard"
)
-// testDashboardsConfigJSON is the pilot's 5-dashboard registry, identical to
-// the DASHBOARDS_CONFIG example documented in .env.example. dashboard.Dashboards
-// is populated from DASHBOARDS_CONFIG only in cmd/server/main.go, which tests
-// never run, so TestMain below seeds it directly via the same parse function
-// production uses — every assertion in this file exercises the real
-// production parsing/lookup/resolution path, just with the config supplied
-// in-process instead of via the environment.
+// testDashboardsConfigJSON is a small, entirely dummy 2-dashboard fixture —
+// generic names/ids only, no real WSO2 CS-team structure — extended slightly
+// beyond the DASHBOARDS_CONFIG example documented in .env.example (which has
+// the same two dashboards) with two additional widgets (an "incident"-typed
+// widget with empty filters and a "change_request"-typed widget) purely to
+// keep this file's resourceType-diversity and scalar-filter test coverage,
+// since the trimmed .env.example example doesn't need those shapes to make
+// its point. dashboard.Dashboards is populated from DASHBOARDS_CONFIG only in
+// cmd/server/main.go, which tests never run, so TestMain below seeds it
+// directly via the same parse function production uses — every assertion in
+// this file exercises the real production parsing/lookup/resolution path,
+// just with the config supplied in-process instead of via the environment.
const testDashboardsConfigJSON = `[
- {"id":"agents_pilot","displayName":"Engineer overview","isDefault":true,"targetTeam":"cs_engineers","widgets":[
- {"id":"my_patches","displayName":"My Patches","resourceType":"case","shape":"count","gridWidth":3,"filters":{"filters":[{"field":"assignedUserId","op":"in","values":["__current_user__"]},{"field":"tag","op":"in","values":["patch"]},{"field":"state","op":"in","values":["open","work_in_progress","waiting_on_wso2","reopened","awaiting_info"]}]}},
- {"id":"my_reminders","displayName":"My Reminders","resourceType":"case","shape":"count","gridWidth":3,"filters":{"filters":[{"field":"assignedUserId","op":"in","values":["__current_user__"]},{"field":"state","op":"in","values":["awaiting_info","solution_proposed"]}]}},
- {"id":"open_incident_team","displayName":"Open Incident (Team)","resourceType":"case","shape":"count","gridWidth":3,"filters":{"filters":[{"field":"tag","op":"in","values":["s_dip"]},{"field":"state","op":"in","values":["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":{"filters":[{"field":"assignedUserId","op":"in","values":["__current_user__"]},{"field":"severity","op":"in","values":["catastrophic","critical"]},{"field":"state","op":"in","values":["open","work_in_progress"]}]}}
+ {"id":"sample-dashboard","displayName":"Sample Dashboard","isDefault":true,"targetTeam":"sample-team","widgets":[
+ {"id":"my-open-cases","displayName":"My Open Cases","resourceType":"case","shape":"count","gridWidth":3,"filters":{"filters":[{"field":"assignedUserId","op":"in","values":["__current_user__"]},{"field":"state","op":"in","values":["open","work_in_progress"]}]}},
+ {"id":"recent-cases","displayName":"Recent Cases","resourceType":"case","shape":"list","gridWidth":6,"listLimit":5,"filters":{"filters":[{"field":"tag","op":"in","values":["example-tag"]},{"field":"tag","op":"notIn","values":["excluded-example-tag"]}]}},
+ {"id":"pending-time-cards","displayName":"Pending Time Cards","resourceType":"time_card","shape":"count","gridWidth":3,"filters":{"states":["pending"]}},
+ {"id":"open-vulnerabilities","displayName":"Open Vulnerabilities","resourceType":"product_vulnerability","shape":"count","gridWidth":3,"filters":{"priority":"high"}}
]},
- {"id":"operations","displayName":"Operations","targetTeam":"cs_operations","widgets":[
- {"id":"p0_p1_open","displayName":"P0/P1 Open","resourceType":"case","shape":"count","gridWidth":4,"filters":{"filters":[{"field":"severity","op":"in","values":["catastrophic","critical"]},{"field":"state","op":"in","values":["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":{"filters":[{"field":"tag","op":"in","values":["iam"]},{"field":"state","op":"in","values":["open","work_in_progress","awaiting_info"]}]}},
- {"id":"asgardeo_open_cases","displayName":"Asgardeo Open Cases","resourceType":"case","shape":"count","gridWidth":6,"filters":{"filters":[{"field":"tag","op":"in","values":["asgardeo"]},{"field":"state","op":"in","values":["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":{"filters":[{"field":"type","op":"in","values":["security_report_analysis"]},{"field":"state","op":"in","values":["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":{"filters":[{"field":"severity","op":"in","values":["catastrophic","critical"]},{"field":"state","op":"in","values":["open","work_in_progress"]}]}},
- {"id":"cases_by_severity","displayName":"Cases by severity","description":"Share of active cases at each severity level.","resourceType":"case","shape":"pie","gridWidth":6,"filters":{"filters":[{"field":"state","op":"in","values":["open","work_in_progress"]}]},"slices":[
+ {"id":"sample-team-dashboard","displayName":"Sample Team Dashboard","targetTeam":"sample-team","isTeamBased":true,"widgets":[
+ {"id":"team-open-cases","displayName":"Team Open Cases","section":"Overview","resourceType":"case","shape":"count","gridWidth":4,"filters":{"filters":[{"field":"severity","op":"in","values":["critical","high"]},{"field":"state","op":"in","values":["open","work_in_progress"]}]}},
+ {"id":"unassigned-cases","displayName":"Unassigned Cases","section":"Overview","resourceType":"case","shape":"count","gridWidth":4,"filters":{"filters":[{"field":"assignedUserId","op":"isEmpty"},{"field":"state","op":"in","values":["open"]}]}},
+ {"id":"cases-by-severity","displayName":"Cases by Severity","description":"Share of active cases at each severity level.","resourceType":"case","shape":"pie","gridWidth":4,"filters":{"filters":[{"field":"state","op":"in","values":["open","work_in_progress"]}]},"slices":[
{"label":"Critical","color":"error","filters":{"filters":[{"field":"severity","op":"in","values":["critical"]}]}},
{"label":"Mine","filters":{"filters":[{"field":"assignedUserId","op":"in","values":["__current_user__"]}]}}
]},
- {"id":"incident_wow","displayName":"Incident WOW","section":"SLA Violation","resourceType":"incident","shape":"count","gridWidth":6,"filters":{}}
+ {"id":"escalated-incidents","displayName":"Escalated Incidents","section":"Escalations","resourceType":"incident","shape":"count","gridWidth":4,"filters":{}},
+ {"id":"pending-change-requests","displayName":"Pending Change Requests","resourceType":"change_request","shape":"count","gridWidth":4,"filters":{"states":["customer_approval"]}}
]}
]`
@@ -99,8 +91,8 @@ func filterValuesByField(filters map[string]any, field string) ([]any, bool) {
// themselves via the same ParseDashboardsConfig function.
func TestMain(m *testing.M) {
dashboard.Dashboards = dashboard.ParseDashboardsConfig(testDashboardsConfigJSON)
- if len(dashboard.Dashboards) != 5 {
- panic(fmt.Sprintf("TestMain: seeding dashboard.Dashboards failed, got %d dashboards, want 5", len(dashboard.Dashboards)))
+ if len(dashboard.Dashboards) != 2 {
+ panic(fmt.Sprintf("TestMain: seeding dashboard.Dashboards failed, got %d dashboards, want 2", len(dashboard.Dashboards)))
}
os.Exit(m.Run())
}
@@ -225,25 +217,26 @@ func TestGetDashboards(t *testing.T) {
}
}
+ wantTeamBased := map[string]bool{"sample-team-dashboard": true}
teamBasedCount := 0
for _, res := range results {
if res.IsTeamBased {
teamBasedCount++
- if res.ID != "team_performance" {
- t.Errorf("unexpected team-based dashboard %q, want team_performance", res.ID)
+ if !wantTeamBased[res.ID] {
+ t.Errorf("unexpected team-based dashboard %q, want sample-team-dashboard", res.ID)
}
}
}
- if teamBasedCount != 1 {
- t.Errorf("teamBasedCount = %d, want exactly 1 (team_performance)", teamBasedCount)
+ if teamBasedCount != len(wantTeamBased) {
+ t.Errorf("teamBasedCount = %d, want exactly %d (sample-team-dashboard)", teamBasedCount, len(wantTeamBased))
}
defaultCount := 0
for _, res := range results {
if res.IsDefault {
defaultCount++
- if res.ID != "agents_pilot" {
- t.Errorf("unexpected default dashboard %q, want agents_pilot", res.ID)
+ if res.ID != "sample-dashboard" {
+ t.Errorf("unexpected default dashboard %q, want sample-dashboard", res.ID)
}
}
}
@@ -256,8 +249,8 @@ func TestGetDashboards(t *testing.T) {
// TestAllDashboardsHaveWidgets is the "no more mock/empty placeholders"
// guarantee: every dashboard in the registry now has real widgets.
func TestAllDashboardsHaveWidgets(t *testing.T) {
- if len(dashboard.Dashboards) != 5 {
- t.Fatalf("len(dashboard.Dashboards) = %d, want 5", len(dashboard.Dashboards))
+ if len(dashboard.Dashboards) != 2 {
+ t.Fatalf("len(dashboard.Dashboards) = %d, want 2", len(dashboard.Dashboards))
}
for _, d := range dashboard.Dashboards {
if len(d.Widgets) == 0 {
@@ -269,7 +262,7 @@ func TestAllDashboardsHaveWidgets(t *testing.T) {
func TestGetDashboardDetail(t *testing.T) {
t.Run("requires authenticated user", func(t *testing.T) {
h := NewDashboardHandler(&mockEntityUserClient{})
- r := withDashboardID(httptest.NewRequest(http.MethodGet, "/dashboards/agents_pilot", nil), "agents_pilot")
+ r := withDashboardID(httptest.NewRequest(http.MethodGet, "/dashboards/sample-dashboard", nil), "sample-dashboard")
w := httptest.NewRecorder()
h.GetDashboardDetail(w, r)
assertStatus(t, w, http.StatusUnauthorized)
@@ -286,9 +279,9 @@ func TestGetDashboardDetail(t *testing.T) {
assertErrorMessage(t, w, ErrMsgNotFound)
})
- t.Run("agents_pilot returns metadata and its four widgets", func(t *testing.T) {
+ t.Run("sample-dashboard returns metadata and its four widgets", func(t *testing.T) {
h := NewDashboardHandler(&mockEntityUserClient{})
- r := withUser(withDashboardID(httptest.NewRequest(http.MethodGet, "/dashboards/agents_pilot", nil), "agents_pilot"))
+ r := withUser(withDashboardID(httptest.NewRequest(http.MethodGet, "/dashboards/sample-dashboard", nil), "sample-dashboard"))
w := httptest.NewRecorder()
h.GetDashboardDetail(w, r)
@@ -296,7 +289,7 @@ func TestGetDashboardDetail(t *testing.T) {
assertContentType(t, w, "application/json")
body := w.Body.Bytes()
- t.Logf("GET /dashboards/agents_pilot response: %s", body)
+ t.Logf("GET /dashboards/sample-dashboard response: %s", body)
// Decode into the real production type (dashboardDetailView, defined
// in dashboards.go), not a duplicate ad hoc struct — a JSON tag
@@ -308,17 +301,17 @@ func TestGetDashboardDetail(t *testing.T) {
t.Fatalf("decode response body: %v; raw: %s", err, body)
}
- if result.ID != "agents_pilot" {
- t.Errorf("ID = %q, want %q", result.ID, "agents_pilot")
+ if result.ID != "sample-dashboard" {
+ t.Errorf("ID = %q, want %q", result.ID, "sample-dashboard")
}
- if result.DisplayName != "Engineer overview" {
- t.Errorf("DisplayName = %q, want %q", result.DisplayName, "Engineer overview")
+ if result.DisplayName != "Sample Dashboard" {
+ t.Errorf("DisplayName = %q, want %q", result.DisplayName, "Sample Dashboard")
}
if !result.IsDefault {
t.Errorf("IsDefault = %v, want true", result.IsDefault)
}
- if result.TargetTeam != "cs_engineers" {
- t.Errorf("TargetTeam = %q, want %q", result.TargetTeam, "cs_engineers")
+ if result.TargetTeam != "sample-team" {
+ t.Errorf("TargetTeam = %q, want %q", result.TargetTeam, "sample-team")
}
if len(result.Widgets) != 4 {
t.Fatalf("len(result.Widgets) = %d, want 4", len(result.Widgets))
@@ -358,10 +351,10 @@ func TestGetDashboardDetail(t *testing.T) {
shape dashboard.Shape
gridWidth int
}{
- "my_patches": {dashboard.ResourceCase, dashboard.ShapeCount, 3},
- "my_reminders": {dashboard.ResourceCase, dashboard.ShapeCount, 3},
- "open_incident_team": {dashboard.ResourceCase, dashboard.ShapeCount, 3},
- "my_critical_open": {dashboard.ResourceCase, dashboard.ShapeList, 3},
+ "my-open-cases": {dashboard.ResourceCase, dashboard.ShapeCount, 3},
+ "recent-cases": {dashboard.ResourceCase, dashboard.ShapeList, 6},
+ "pending-time-cards": {dashboard.ResourceTimeCard, dashboard.ShapeCount, 3},
+ "open-vulnerabilities": {dashboard.ResourceProductVulnerability, dashboard.ShapeCount, 3},
}
for id, want := range wantResourceShape {
idx, ok := byID[id]
@@ -380,61 +373,46 @@ func TestGetDashboardDetail(t *testing.T) {
}
}
- if idx := byID["my_critical_open"]; result.Widgets[idx].ListLimit != 5 {
- t.Errorf("widget my_critical_open listLimit = %d, want 5", result.Widgets[idx].ListLimit)
+ if idx := byID["recent-cases"]; result.Widgets[idx].ListLimit != 5 {
+ t.Errorf("widget recent-cases listLimit = %d, want 5", result.Widgets[idx].ListLimit)
}
- for _, id := range []string{"my_patches", "my_reminders"} {
- idx, ok := byID[id]
- if !ok {
- t.Fatalf("missing widget %q in response", id)
- }
- filters := result.Widgets[idx].Filters
- assigned, present := filterValuesByField(filters, "assignedUserId")
- if !present {
- t.Fatalf("widget %s filters has no assignedUserId field entry", id)
- }
- if len(assigned) != 1 || assigned[0] != resolvedCurrentUserID {
- t.Errorf("widget %s assignedUserId values = %v, want [%q]", id, assigned, resolvedCurrentUserID)
- }
- for _, uid := range assigned {
- if uid == "__current_user__" {
- t.Errorf("widget %s assignedUserId values leaked the unresolved placeholder", id)
- }
- }
- }
-
- // open_incident_team has no assignedUserId filter entry in its
- // template and must not gain one during substitution:
- // substituteCurrentUser only rewrites values already present, it
- // never adds entries.
- teamIdx, ok := byID["open_incident_team"]
+ // my-open-cases carries an assignedUserId filter (the current user's
+ // open cases) — verify __current_user__ resolved to a concrete id,
+ // not the raw placeholder.
+ openIdx, ok := byID["my-open-cases"]
if !ok {
- t.Fatalf("missing widget %q in response", "open_incident_team")
+ t.Fatalf("missing widget %q in response", "my-open-cases")
+ }
+ assigned, present := filterValuesByField(result.Widgets[openIdx].Filters, "assignedUserId")
+ if !present {
+ t.Fatalf("widget my-open-cases filters has no assignedUserId field entry")
}
- teamFilters := result.Widgets[teamIdx].Filters
- if v, present := filterValuesByField(teamFilters, "assignedUserId"); present {
- t.Errorf("widget open_incident_team filters unexpectedly has an assignedUserId field entry: %v", v)
+ if len(assigned) != 1 || assigned[0] != resolvedCurrentUserID {
+ t.Errorf("widget my-open-cases assignedUserId values = %v, want [%q]", assigned, resolvedCurrentUserID)
+ }
+ for _, uid := range assigned {
+ if uid == "__current_user__" {
+ t.Errorf("widget my-open-cases assignedUserId values leaked the unresolved placeholder")
+ }
}
- // my_critical_open DOES carry an assignedUserId filter (the current
- // user's critical/high cases) — verify it resolved cleanly.
- criticalIdx, ok := byID["my_critical_open"]
+ // recent-cases has no assignedUserId filter entry in its template and
+ // must not gain one during substitution: substituteCurrentUser only
+ // rewrites values already present, it never adds entries.
+ recentIdx, ok := byID["recent-cases"]
if !ok {
- t.Fatalf("missing widget %q in response", "my_critical_open")
+ t.Fatalf("missing widget %q in response", "recent-cases")
}
- assigned, present := filterValuesByField(result.Widgets[criticalIdx].Filters, "assignedUserId")
- if !present {
- t.Fatalf("widget my_critical_open filters has no assignedUserId field entry")
- }
- if len(assigned) != 1 || assigned[0] != resolvedCurrentUserID {
- t.Errorf("widget my_critical_open assignedUserId values = %v, want [%q]", assigned, resolvedCurrentUserID)
+ recentFilters := result.Widgets[recentIdx].Filters
+ if v, present := filterValuesByField(recentFilters, "assignedUserId"); present {
+ t.Errorf("widget recent-cases filters unexpectedly has an assignedUserId field entry: %v", v)
}
})
- t.Run("operations dashboard has three resource-type-diverse widgets", func(t *testing.T) {
+ t.Run("sample-team-dashboard has resource-type-diverse widgets (case, incident, change_request)", func(t *testing.T) {
h := NewDashboardHandler(&mockEntityUserClient{})
- r := withUser(withDashboardID(httptest.NewRequest(http.MethodGet, "/dashboards/operations", nil), "operations"))
+ r := withUser(withDashboardID(httptest.NewRequest(http.MethodGet, "/dashboards/sample-team-dashboard", nil), "sample-team-dashboard"))
w := httptest.NewRecorder()
h.GetDashboardDetail(w, r)
@@ -447,14 +425,14 @@ func TestGetDashboardDetail(t *testing.T) {
if err := json.Unmarshal(body, &result); err != nil {
t.Fatalf("decode response body: %v; raw: %s", err, body)
}
- if result.ID != "operations" {
- t.Errorf("ID = %q, want %q", result.ID, "operations")
+ if result.ID != "sample-team-dashboard" {
+ t.Errorf("ID = %q, want %q", result.ID, "sample-team-dashboard")
}
- if result.TargetTeam != "cs_operations" {
- t.Errorf("TargetTeam = %q, want %q", result.TargetTeam, "cs_operations")
+ if result.TargetTeam != "sample-team" {
+ t.Errorf("TargetTeam = %q, want %q", result.TargetTeam, "sample-team")
}
- if len(result.Widgets) != 3 {
- t.Fatalf("len(result.Widgets) = %d, want 3", len(result.Widgets))
+ if len(result.Widgets) != 5 {
+ t.Fatalf("len(result.Widgets) = %d, want 5", len(result.Widgets))
}
byID := make(map[string]dashboardWidgetView)
@@ -463,9 +441,9 @@ func TestGetDashboardDetail(t *testing.T) {
}
wantTypes := map[string]dashboard.ResourceType{
- "p0_p1_open": dashboard.ResourceCase,
- "open_critical_incidents": dashboard.ResourceIncident,
- "crs_awaiting_approval": dashboard.ResourceChangeRequest,
+ "team-open-cases": dashboard.ResourceCase,
+ "escalated-incidents": dashboard.ResourceIncident,
+ "pending-change-requests": dashboard.ResourceChangeRequest,
}
for id, wantType := range wantTypes {
got, ok := byID[id]
@@ -477,23 +455,23 @@ func TestGetDashboardDetail(t *testing.T) {
}
}
- incident, ok := byID["open_critical_incidents"]
+ changeRequest, ok := byID["pending-change-requests"]
if !ok {
- t.Fatalf("missing widget %q in response", "open_critical_incidents")
+ t.Fatalf("missing widget %q in response", "pending-change-requests")
}
- prioritiesRaw, present := incident.Filters["priorities"]
+ statesRaw, present := changeRequest.Filters["states"]
if !present {
- t.Fatalf("open_critical_incidents filters has no priorities key: %v", incident.Filters)
+ t.Fatalf("pending-change-requests filters has no states key: %v", changeRequest.Filters)
}
- priorities, ok := prioritiesRaw.([]any)
- if !ok || len(priorities) != 2 || priorities[0] != "CRITICAL" || priorities[1] != "HIGH" {
- t.Errorf("open_critical_incidents filters.priorities = %v, want [CRITICAL HIGH] unmodified", prioritiesRaw)
+ states, ok := statesRaw.([]any)
+ if !ok || len(states) != 1 || states[0] != "customer_approval" {
+ t.Errorf("pending-change-requests filters.states = %v, want [customer_approval] unmodified", statesRaw)
}
})
- t.Run("security dashboard's product_vulnerability widget has a scalar string filter", func(t *testing.T) {
+ t.Run("sample-dashboard's product_vulnerability widget has a scalar string filter", func(t *testing.T) {
h := NewDashboardHandler(&mockEntityUserClient{})
- r := withUser(withDashboardID(httptest.NewRequest(http.MethodGet, "/dashboards/security", nil), "security"))
+ r := withUser(withDashboardID(httptest.NewRequest(http.MethodGet, "/dashboards/sample-dashboard", nil), "sample-dashboard"))
w := httptest.NewRecorder()
h.GetDashboardDetail(w, r)
@@ -501,14 +479,14 @@ func TestGetDashboardDetail(t *testing.T) {
assertContentType(t, w, "application/json")
body := w.Body.Bytes()
- t.Logf("GET /dashboards/security response: %s", body)
+ t.Logf("GET /dashboards/sample-dashboard response: %s", body)
var result dashboardDetailView
if err := json.Unmarshal(body, &result); err != nil {
t.Fatalf("decode response body: %v; raw: %s", err, body)
}
- if len(result.Widgets) != 3 {
- t.Fatalf("len(result.Widgets) = %d, want 3", len(result.Widgets))
+ if len(result.Widgets) != 4 {
+ t.Fatalf("len(result.Widgets) = %d, want 4", len(result.Widgets))
}
byID := make(map[string]dashboardWidgetView)
@@ -516,31 +494,31 @@ func TestGetDashboardDetail(t *testing.T) {
byID[wd.WidgetID] = wd
}
- critical, ok := byID["critical_vulns"]
+ vulns, ok := byID["open-vulnerabilities"]
if !ok {
- t.Fatalf("missing widget %q in response", "critical_vulns")
+ t.Fatalf("missing widget %q in response", "open-vulnerabilities")
}
- if critical.ResourceType != dashboard.ResourceProductVulnerability {
- t.Errorf("critical_vulns resourceType = %q, want %q", critical.ResourceType, dashboard.ResourceProductVulnerability)
+ if vulns.ResourceType != dashboard.ResourceProductVulnerability {
+ t.Errorf("open-vulnerabilities resourceType = %q, want %q", vulns.ResourceType, dashboard.ResourceProductVulnerability)
}
- priority, present := critical.Filters["priority"]
+ priority, present := vulns.Filters["priority"]
if !present {
- t.Fatalf("critical_vulns filters has no priority key: %v", critical.Filters)
+ t.Fatalf("open-vulnerabilities filters has no priority key: %v", vulns.Filters)
}
- if s, ok := priority.(string); !ok || s != "critical" {
- t.Errorf("critical_vulns filters.priority = %v (%T), want string %q", priority, priority, "critical")
+ if s, ok := priority.(string); !ok || s != "high" {
+ t.Errorf("open-vulnerabilities filters.priority = %v (%T), want string %q", priority, priority, "high")
}
})
- t.Run("team_performance's pie widget resolves description, slices, and per-slice current-user placeholders", func(t *testing.T) {
+ t.Run("sample-team-dashboard's pie widget resolves description, slices, and per-slice current-user placeholders", func(t *testing.T) {
h := NewDashboardHandler(&mockEntityUserClient{})
- r := withUser(withDashboardID(httptest.NewRequest(http.MethodGet, "/dashboards/team_performance", nil), "team_performance"))
+ r := withUser(withDashboardID(httptest.NewRequest(http.MethodGet, "/dashboards/sample-team-dashboard", nil), "sample-team-dashboard"))
w := httptest.NewRecorder()
h.GetDashboardDetail(w, r)
assertStatus(t, w, http.StatusOK)
body := w.Body.Bytes()
- t.Logf("GET /dashboards/team_performance response: %s", body)
+ t.Logf("GET /dashboards/sample-team-dashboard response: %s", body)
var result dashboardDetailView
if err := json.Unmarshal(body, &result); err != nil {
@@ -551,15 +529,15 @@ func TestGetDashboardDetail(t *testing.T) {
for _, wd := range result.Widgets {
byID[wd.WidgetID] = wd
}
- pie, ok := byID["cases_by_severity"]
+ pie, ok := byID["cases-by-severity"]
if !ok {
- t.Fatalf("missing widget %q in response", "cases_by_severity")
+ t.Fatalf("missing widget %q in response", "cases-by-severity")
}
if pie.Description != "Share of active cases at each severity level." {
- t.Errorf("cases_by_severity Description = %q, want the configured subtitle", pie.Description)
+ t.Errorf("cases-by-severity Description = %q, want the configured subtitle", pie.Description)
}
if len(pie.Slices) != 2 {
- t.Fatalf("len(cases_by_severity.Slices) = %d, want 2", len(pie.Slices))
+ t.Fatalf("len(cases-by-severity.Slices) = %d, want 2", len(pie.Slices))
}
var critical, mine *dashboardPieSliceView
@@ -572,7 +550,7 @@ func TestGetDashboardDetail(t *testing.T) {
}
}
if critical == nil {
- t.Fatalf("missing the %q slice in cases_by_severity.Slices", "Critical")
+ t.Fatalf("missing the %q slice in cases-by-severity.Slices", "Critical")
}
if critical.Color != "error" {
t.Errorf("Critical slice Color = %q, want %q", critical.Color, "error")
@@ -582,7 +560,7 @@ func TestGetDashboardDetail(t *testing.T) {
}
if mine == nil {
- t.Fatalf("missing the %q slice in cases_by_severity.Slices", "Mine")
+ t.Fatalf("missing the %q slice in cases-by-severity.Slices", "Mine")
}
assigned, ok := filterValuesByField(mine.Filters, "assignedUserId")
if !ok || len(assigned) != 1 || assigned[0] != resolvedCurrentUserID {
@@ -601,19 +579,19 @@ func TestGetDashboardDetail(t *testing.T) {
var rawPie map[string]json.RawMessage
for _, obj := range rawWidgets {
var id string
- if err := json.Unmarshal(obj["widgetId"], &id); err == nil && id == "cases_by_severity" {
+ if err := json.Unmarshal(obj["widgetId"], &id); err == nil && id == "cases-by-severity" {
rawPie = obj
}
}
if rawPie == nil {
- t.Fatalf("cases_by_severity not found among raw widgets: %v", rawWidgets)
+ t.Fatalf("cases-by-severity not found among raw widgets: %v", rawWidgets)
}
- assertJSONKeysSuperset(t, rawPie, append(append([]string(nil), dashboardWidgetJSONKeys...), "description", "slices"), "cases_by_severity")
+ assertJSONKeysSuperset(t, rawPie, append(append([]string(nil), dashboardWidgetJSONKeys...), "description", "slices"), "cases-by-severity")
})
- t.Run("team_performance's incident_wow widget carries its configured section, unset for widgets with no section", func(t *testing.T) {
+ t.Run("sample-team-dashboard's escalated-incidents widget carries its configured section, unset for widgets with no section", func(t *testing.T) {
h := NewDashboardHandler(&mockEntityUserClient{})
- r := withUser(withDashboardID(httptest.NewRequest(http.MethodGet, "/dashboards/team_performance", nil), "team_performance"))
+ r := withUser(withDashboardID(httptest.NewRequest(http.MethodGet, "/dashboards/sample-team-dashboard", nil), "sample-team-dashboard"))
w := httptest.NewRecorder()
h.GetDashboardDetail(w, r)
@@ -627,20 +605,20 @@ func TestGetDashboardDetail(t *testing.T) {
byID[wd.WidgetID] = wd
}
- wow, ok := byID["incident_wow"]
+ escalated, ok := byID["escalated-incidents"]
if !ok {
- t.Fatalf("missing widget %q in response", "incident_wow")
+ t.Fatalf("missing widget %q in response", "escalated-incidents")
}
- if wow.Section != "SLA Violation" {
- t.Errorf("incident_wow.Section = %q, want %q", wow.Section, "SLA Violation")
+ if escalated.Section != "Escalations" {
+ t.Errorf("escalated-incidents.Section = %q, want %q", escalated.Section, "Escalations")
}
- teamOpenCases, ok := byID["team_open_cases"]
+ casesBySeverity, ok := byID["cases-by-severity"]
if !ok {
- t.Fatalf("missing widget %q in response", "team_open_cases")
+ t.Fatalf("missing widget %q in response", "cases-by-severity")
}
- if teamOpenCases.Section != "" {
- t.Errorf("team_open_cases.Section = %q, want empty (no section configured)", teamOpenCases.Section)
+ if casesBySeverity.Section != "" {
+ t.Errorf("cases-by-severity.Section = %q, want empty (no section configured)", casesBySeverity.Section)
}
})
@@ -682,7 +660,7 @@ func TestResolveCurrentUserID_UsesEntityUsersMeNotJWTClaim(t *testing.T) {
},
}
h := NewDashboardHandler(mock)
- r := withUser(withDashboardID(httptest.NewRequest(http.MethodGet, "/dashboards/agents_pilot", nil), "agents_pilot"))
+ r := withUser(withDashboardID(httptest.NewRequest(http.MethodGet, "/dashboards/sample-dashboard", nil), "sample-dashboard"))
w := httptest.NewRecorder()
h.GetDashboardDetail(w, r)
assertStatus(t, w, http.StatusOK)
@@ -696,12 +674,12 @@ func TestResolveCurrentUserID_UsesEntityUsersMeNotJWTClaim(t *testing.T) {
byID[wd.WidgetID] = wd
}
- assigned, ok := filterValuesByField(byID["my_patches"].Filters, "assignedUserId")
+ assigned, ok := filterValuesByField(byID["my-open-cases"].Filters, "assignedUserId")
if !ok || len(assigned) != 1 {
- t.Fatalf("my_patches assignedUserId values = %v, want a 1-element array", assigned)
+ t.Fatalf("my-open-cases assignedUserId values = %v, want a 1-element array", assigned)
}
if assigned[0] != entityResolvedID {
- t.Errorf("my_patches assignedUserId values[0] = %v, want the entity-resolved id %q (not the JWT claim %q)",
+ t.Errorf("my-open-cases assignedUserId values[0] = %v, want the entity-resolved id %q (not the JWT claim %q)",
assigned[0], entityResolvedID, testUser.UserID)
}
}
@@ -717,7 +695,7 @@ func TestResolveCurrentUserID_FallsBackToJWTClaimOnEntityError(t *testing.T) {
},
}
h := NewDashboardHandler(mock)
- r := withUser(withDashboardID(httptest.NewRequest(http.MethodGet, "/dashboards/agents_pilot", nil), "agents_pilot"))
+ r := withUser(withDashboardID(httptest.NewRequest(http.MethodGet, "/dashboards/sample-dashboard", nil), "sample-dashboard"))
w := httptest.NewRecorder()
h.GetDashboardDetail(w, r)
assertStatus(t, w, http.StatusOK)
@@ -730,8 +708,8 @@ func TestResolveCurrentUserID_FallsBackToJWTClaimOnEntityError(t *testing.T) {
for _, wd := range result.Widgets {
byID[wd.WidgetID] = wd
}
- assigned, ok := filterValuesByField(byID["my_patches"].Filters, "assignedUserId")
+ assigned, ok := filterValuesByField(byID["my-open-cases"].Filters, "assignedUserId")
if !ok || len(assigned) != 1 || assigned[0] != testUser.UserID {
- t.Errorf("my_patches assignedUserId values = %v, want the JWT-claim fallback [%q]", assigned, testUser.UserID)
+ t.Errorf("my-open-cases assignedUserId values = %v, want the JWT-claim fallback [%q]", assigned, testUser.UserID)
}
}
diff --git a/apps/csm-portal/backend/openapi.yaml b/apps/csm-portal/backend/openapi.yaml
index 3b6b1ce833..1743478b33 100644
--- a/apps/csm-portal/backend/openapi.yaml
+++ b/apps/csm-portal/backend/openapi.yaml
@@ -1677,7 +1677,7 @@ paths:
parameters:
- name: dashboardId
in: path
- description: ID of the dashboard (e.g. "agents_pilot")
+ description: ID of the dashboard (e.g. "sample-dashboard")
required: true
schema:
type: string
diff --git a/apps/csm-portal/webapp/src/features/csm-cases/components/CasesList.tsx b/apps/csm-portal/webapp/src/features/csm-cases/components/CasesList.tsx
index ead2051e9e..1642d5805e 100644
--- a/apps/csm-portal/webapp/src/features/csm-cases/components/CasesList.tsx
+++ b/apps/csm-portal/webapp/src/features/csm-cases/components/CasesList.tsx
@@ -326,7 +326,6 @@ export default function CasesList({
)}
- {c.updatedAtIsCreatedFallback && "Created "}
diff --git a/apps/csm-portal/webapp/src/features/csm-cases/types/csmCases.ts b/apps/csm-portal/webapp/src/features/csm-cases/types/csmCases.ts
index 43728afbbc..beee91935f 100644
--- a/apps/csm-portal/webapp/src/features/csm-cases/types/csmCases.ts
+++ b/apps/csm-portal/webapp/src/features/csm-cases/types/csmCases.ts
@@ -82,11 +82,9 @@ export interface CsmCaseRow {
*/
hasSla?: boolean;
createdAt: string;
+ /** Falls back to {@link createdAt} when the backend hasn't returned
+ * `updatedOn` for this row; rendered unprefixed either way. */
updatedAt: string;
- /** True when the backend didn't return `updatedOn` and {@link updatedAt}
- * was filled in from {@link createdAt} instead — the list renders that
- * fallback labeled "Created", never silently as "Updated". */
- updatedAtIsCreatedFallback?: boolean;
}
export interface CsmCasesListResponse {
diff --git a/apps/csm-portal/webapp/src/features/csm-cases/utils/caseSearchPayload.ts b/apps/csm-portal/webapp/src/features/csm-cases/utils/caseSearchPayload.ts
index 3d82da88c7..5e7c22c838 100644
--- a/apps/csm-portal/webapp/src/features/csm-cases/utils/caseSearchPayload.ts
+++ b/apps/csm-portal/webapp/src/features/csm-cases/utils/caseSearchPayload.ts
@@ -185,6 +185,5 @@ export function mapCaseSearchViewToRow(
hasSla: false,
createdAt: c.createdOn ?? "",
updatedAt: c.updatedOn ?? c.createdOn ?? "",
- updatedAtIsCreatedFallback: !c.updatedOn && !!c.createdOn,
};
}
diff --git a/apps/csm-portal/webapp/src/features/csm-dashboard/api/useGetMyAssignedOpenCases.ts b/apps/csm-portal/webapp/src/features/csm-dashboard/api/useGetMyAssignedOpenCases.ts
index 027cd25e78..75e789fcac 100644
--- a/apps/csm-portal/webapp/src/features/csm-dashboard/api/useGetMyAssignedOpenCases.ts
+++ b/apps/csm-portal/webapp/src/features/csm-dashboard/api/useGetMyAssignedOpenCases.ts
@@ -155,7 +155,6 @@ export function useGetMyAssignedOpenCases(
hasSla: false,
createdAt: c.createdOn ?? "",
updatedAt: c.updatedOn ?? c.createdOn ?? "",
- updatedAtIsCreatedFallback: !c.updatedOn && !!c.createdOn,
};
});
diff --git a/apps/csm-portal/webapp/src/features/csm-dashboard/components/AbtDashboardHeader.test.tsx b/apps/csm-portal/webapp/src/features/csm-dashboard/components/AbtDashboardHeader.test.tsx
index 02de5f8678..e6804caf34 100644
--- a/apps/csm-portal/webapp/src/features/csm-dashboard/components/AbtDashboardHeader.test.tsx
+++ b/apps/csm-portal/webapp/src/features/csm-dashboard/components/AbtDashboardHeader.test.tsx
@@ -53,6 +53,8 @@ describe("AbtDashboardHeader", () => {
dashboardKey="agents_pilot"
onDashboardChange={vi.fn()}
dashboardList={DASHBOARD_LIST}
+ selectedTeamId={undefined}
+ onTeamChange={vi.fn()}
/>,
);
@@ -74,6 +76,8 @@ describe("AbtDashboardHeader", () => {
dashboardKey="team_performance"
onDashboardChange={vi.fn()}
dashboardList={DASHBOARD_LIST}
+ selectedTeamId={undefined}
+ onTeamChange={vi.fn()}
/>,
);
@@ -92,4 +96,28 @@ describe("AbtDashboardHeader", () => {
expect(within(listbox).getByText("CS Operations")).toBeInTheDocument();
expect(within(listbox).getByText("All teams")).toBeInTheDocument();
});
+
+ it("calls onTeamChange when a team is picked", async () => {
+ postMock.mockResolvedValue({
+ teams: [{ id: "cs_team_leads", name: "CS Team Leads" }],
+ });
+ const onTeamChange = vi.fn();
+
+ renderWithClient(
+ ,
+ );
+
+ const [teamSelect] = screen.getAllByRole("combobox");
+ fireEvent.mouseDown(teamSelect);
+ const listbox = await screen.findByRole("listbox");
+ fireEvent.click(await within(listbox).findByText("CS Team Leads"));
+
+ expect(onTeamChange).toHaveBeenCalledWith("cs_team_leads");
+ });
});
diff --git a/apps/csm-portal/webapp/src/features/csm-dashboard/components/AbtDashboardHeader.tsx b/apps/csm-portal/webapp/src/features/csm-dashboard/components/AbtDashboardHeader.tsx
index f7ebed01cd..6e949b9eea 100644
--- a/apps/csm-portal/webapp/src/features/csm-dashboard/components/AbtDashboardHeader.tsx
+++ b/apps/csm-portal/webapp/src/features/csm-dashboard/components/AbtDashboardHeader.tsx
@@ -15,7 +15,7 @@
// under the License.
import { Box, FormControl, MenuItem, Select, Typography } from "@wso2/oxygen-ui";
-import { useState, type JSX } from "react";
+import { type JSX } from "react";
import type { BeDashboardListItem } from "@api/backend/types";
import type { DashboardKey } from "@features/csm-dashboard/types/abtDashboard";
import { useTeams } from "@features/csm-dashboard/api/useTeams";
@@ -25,29 +25,37 @@ interface AbtDashboardHeaderProps {
onDashboardChange: (key: DashboardKey) => void;
/** Every dashboard in the BE registry (GET /dashboards), for the switcher. */
dashboardList: BeDashboardListItem[];
+ /** The currently selected team, controlled by the parent (URL-synced) —
+ * `undefined` when no team is selected, or when the current dashboard
+ * isn't `isTeamBased` (the parent never passes a stale team id through in
+ * that case). */
+ selectedTeamId: string | undefined;
+ onTeamChange: (teamId: string | undefined) => void;
}
/**
* Dashboard header: title, the dashboard switcher, and (for a dashboard
* flagged `isTeamBased`) a team selector sourced from `POST /teams/search`.
- * Team selection is UI state only today — it does not yet scope any
- * widget's data (see `Dashboard.isTeamBased` on the backend); wiring a
- * selected team into widget filters is a later increment. The earlier My
- * ABT / All customers toggle was removed entirely — ABT scoping was never
- * implemented and dashboards carry no other special behavior beyond which
- * one (and, for team-based ones, which team) is selected.
+ * Both the selected dashboard and the selected team are owned by the parent
+ * (`CsmDashboardPage`), which keeps them in sync with the URL so a specific
+ * dashboard/team view is shareable. Team selection is UI state only today —
+ * it does not yet scope any widget's data (see `Dashboard.isTeamBased` on
+ * the backend); wiring a selected team into widget filters is a later
+ * increment. The earlier My ABT / All customers toggle was removed
+ * entirely — ABT scoping was never implemented and dashboards carry no
+ * other special behavior beyond which one (and, for team-based ones, which
+ * team) is selected.
*/
export default function AbtDashboardHeader({
dashboardKey,
onDashboardChange,
dashboardList,
+ selectedTeamId,
+ onTeamChange,
}: AbtDashboardHeaderProps): JSX.Element {
const currentOption = dashboardList.find((o) => o.id === dashboardKey);
const isTeamBased = currentOption?.isTeamBased ?? false;
- const [selectedTeamId, setSelectedTeamId] = useState(
- undefined,
- );
const teams = useTeams(isTeamBased);
return (
@@ -71,7 +79,7 @@ export default function AbtDashboardHeader({