Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions apps/csm-portal/webapp/src/api/backend/mappers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import type {
BeAttachment,
BeCaseComment,
BeCreatableCommentType,
BeCasePriority,
BeCaseSeverity,
BeCaseState,
} from "@api/backend/types";
import type {
Expand All @@ -47,7 +47,7 @@ import type {
* doubles as the source.
*/
export function severityFromPriority(
priority: BeCasePriority | undefined,
priority: BeCaseSeverity | undefined,
): Severity {
switch (priority) {
case "catastrophic":
Expand All @@ -65,7 +65,7 @@ export function severityFromPriority(
}
}

export function priorityFromSeverity(severity: Severity): BeCasePriority {
export function priorityFromSeverity(severity: Severity): BeCaseSeverity {
switch (severity) {
case "S0":
return "catastrophic";
Expand Down
95 changes: 70 additions & 25 deletions apps/csm-portal/webapp/src/api/backend/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export interface BeSearchResponseBase {
// Cases
// ---------------------------------------------------------------------------

export type BeCasePriority =
export type BeCaseSeverity =
| "catastrophic"
| "critical"
| "high"
Expand All @@ -63,11 +63,20 @@ export type BeCaseIssueType =
| "security_or_compliance"
| "total_outage";

/** Case type (entity `typeKey`). Only `support` is creatable from the portal. */
export type BeCaseType =
| "support"
| "service_request"
| "security_report_analysis"
| "announcement"
| "engagement";

export type BeCaseState =
| "open"
| "work_in_progress"
| "waiting_on_wso2"
| "awaiting_info"
| "reopened"
| "solution_proposed"
| "closed";

Expand All @@ -78,7 +87,7 @@ export type BeCaseState =
*/
export type BeCaseWorkState = "ongoing" | "paused";

export type BeCaseSortField = "created_at" | "updated_at" | "closed_at";
export type BeCaseSortField = "createdOn" | "updatedOn" | "severity" | "state";

export interface BeCase {
id: string;
Expand All @@ -90,8 +99,9 @@ export interface BeCase {
deployedProductId?: string;
subject?: string;
description?: string;
priority?: BeCasePriority;
severity?: BeCaseSeverity;
issueType?: BeCaseIssueType;
type?: BeCaseType;
state?: BeCaseState;
createdAt?: string;
updatedAt?: string;
Expand All @@ -112,6 +122,18 @@ export interface BeEntityRef {
name: string;
}

/**
* The assigned CS engineer embedded in case views. Carries `email` so the FE
* can tell whether the case is assigned to the signed-in user (the only stable
* identity the FE has from the JWT). `email` may be `null` depending on the data
* source / endpoint — the GET populates it where available.
*/
export interface BeAssignedEngineerRef {
id: string;
name?: string;
email?: string | null;
}

/** A referenced deployed product (its display name is the product + version). */
export interface BeDeployedProductRef {
id: string;
Expand Down Expand Up @@ -142,15 +164,16 @@ export interface BeCaseView {
internalId?: string;
subject?: string;
description?: string;
priority?: BeCasePriority;
severity?: BeCaseSeverity;
issueType?: BeCaseIssueType;
type?: BeCaseType;
state?: BeCaseState;
/** Work sub-state; only meaningful while `state` is `work_in_progress`. */
workState?: BeCaseWorkState | null;
nextStates?: BeCaseState[];
createdBy?: BeUserRef;
/** The CS engineer the case is assigned to; null when unassigned. */
assignedEngineer?: BeEntityRef | null;
assignedEngineer?: BeAssignedEngineerRef | null;
account?: BeCaseAccountRef;
project?: BeEntityRef;
deployment?: BeEntityRef;
Expand All @@ -161,13 +184,15 @@ export interface BeCaseView {
}

export interface BeCaseCreatePayload {
/** Case type. The portal only creates `support` cases. */
typeKey: "support";
projectId: string;
deploymentId: string;
deployedProductId: string;
subject: string;
description: string;
priority: BeCasePriority;
issueType: BeCaseIssueType;
severityKey: BeCaseSeverity;
issueTypeKey: BeCaseIssueType;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

/** The case summary embedded in the `POST /cases` success envelope. */
Expand All @@ -188,20 +213,23 @@ export interface BeCaseCreateResponse {

/**
* Request body for `PATCH /cases/{id}` (mirrors the entity `UpdateCaseRequest`).
* **Exactly one** of `state` / `priority` / `assigneeEmail` / `watchList` is
* sent per call — the backend rejects zero or more than one. Encoded as a
* discriminated union (each variant `?: never`s the others) so the
* exactly-one-field contract is enforced at compile time, not just in docs.
* `assigneeEmail` and `watchList` are supported **only** for the ServiceNow
* data source.
* **Exactly one** of `stateKey` / `severityKey` / `workStateKey` /
* `assigneeEmail` / `watchList` is sent per call — the backend rejects zero or
* more than one. Encoded as a discriminated union (each variant `?: never`s the
* others) so the exactly-one-field contract is enforced at compile time, not
* just in docs. `assigneeEmail` and `watchList` are supported **only** for the
* ServiceNow data source. `workStateKey` is only accepted while the case is
* `work_in_progress`.
*/
export type BeCaseUpdatePayload =
| { state: BeCaseState; priority?: never; assigneeEmail?: never; watchList?: never }
| { state?: never; priority: BeCasePriority; assigneeEmail?: never; watchList?: never }
| { stateKey: BeCaseState; severityKey?: never; workStateKey?: never; assigneeEmail?: never; watchList?: never }
| { stateKey?: never; severityKey: BeCaseSeverity; workStateKey?: never; assigneeEmail?: never; watchList?: never }
/** Work sub-state toggle (`ongoing` / `paused`) for an in-progress case. */
| { stateKey?: never; severityKey?: never; workStateKey: BeCaseWorkState; assigneeEmail?: never; watchList?: never }
/** Email of the engineer to assign (ServiceNow only). */
| { state?: never; priority?: never; assigneeEmail: string; watchList?: never }
| { stateKey?: never; severityKey?: never; workStateKey?: never; assigneeEmail: string; watchList?: never }
/** Full replacement watch list as emails (ServiceNow only). */
| { state?: never; priority?: never; assigneeEmail?: never; watchList: string[] };
| { stateKey?: never; severityKey?: never; workStateKey?: never; assigneeEmail?: never; watchList: string[] };

/** A user in the case watch list, as echoed by `PATCH /cases/{id}`. */
export interface BeWatchListUser {
Expand All @@ -217,7 +245,8 @@ export interface BeUpdatedCase {
updatedOn?: string;
updatedBy?: string;
state?: BeCaseState;
priority?: BeCasePriority;
severity?: BeCaseSeverity;
workState?: BeCaseWorkState | null;
watchList?: BeWatchListUser[];
assignedTo?: BeEntityRef | null;
}
Expand All @@ -236,10 +265,14 @@ export interface BeCaseSearchFilters {
/** Optional project filter; omit for a cross-project search. */
projectIds?: string[];
deploymentIds?: string[];
deployedProductIds?: string[];
typeKeys?: BeCaseType[];
stateKeys?: BeCaseState[];
priorityKeys?: BeCasePriority[];
severityKeys?: BeCaseSeverity[];
issueTypeKeys?: BeCaseIssueType[];
/** Filter to cases created by these emails. */
createdBy?: string[];
/** When true, the caller's email (from the JWT) is appended to `createdBy`. */
createdByMe?: boolean;
}

export interface BeCaseSearchPayload {
Expand All @@ -263,20 +296,32 @@ export interface BeCaseSearchView {
number?: string;
/** Project-scoped WSO2 case reference (customer portal calls this the same). */
internalId?: string;
subject?: string;
/**
* Case title. NOTE: the search response uses `title` (not `subject` like the
* `GET /cases/{id}` CaseView) — a backend naming inconsistency. The BFF
* openapi documents this as `subject`, but the entity returns `title`.
*/
title?: string;
description?: string;
priority?: BeCasePriority;
severity?: BeCaseSeverity;
issueType?: BeCaseIssueType;
/** Case type (entity `caseType` on the search view). */
caseType?: BeCaseType;
state?: BeCaseState;
/** Work sub-state; only meaningful while `state` is `work_in_progress`. */
workState?: BeCaseWorkState | null;
/** The CS engineer the case is assigned to; null when unassigned. */
assignedEngineer?: BeAssignedEngineerRef | null;
createdOn?: string;
/** Often absent on the search view (unlike the GET view); tolerate it missing. */
updatedOn?: string;
closedAt?: string | null;
createdBy?: BeUserRef;
/** Created-by is a bare email string here (not a UserRef like the GET view). */
createdBy?: string;
project?: BeEntityRef;
deployment?: BeEntityRef;
deployedProduct?: BeDeployedProductRef;
/** Embedded as `{ id, name }` (name already includes the version), not the
* `displayName`-shaped ref the GET view uses. */
deployedProduct?: BeEntityRef;
}

export interface BeCaseSearchResponse extends BeSearchResponseBase {
Expand Down
52 changes: 52 additions & 0 deletions apps/csm-portal/webapp/src/components/StateChip.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// Copyright (c) 2026 WSO2 LLC. (https://www.wso2.com).
//
// WSO2 LLC. licenses this file to you under the Apache License,
// Version 2.0 (the "License"); you may not use this file except
// in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.

import type { JSX } from "react";
import {
stateColor,
stateLabel,
} from "@features/csm-dashboard/utils/abtDashboard";
import SemanticChip from "@components/SemanticChip";

interface StateChipProps {
state: string;
size?: "small" | "medium";
/** Render a pointer cursor when the chip sits inside a link / clickable row. */
clickable?: boolean;
}

/**
* The single source of truth for rendering a case lifecycle-state badge. Renders
* as a solid {@link SemanticChip} so the state reads as a status pill rather than
* quiet text — but, unlike {@link SeverityChip}, it is NOT bold, so severity
* stays the top scan signal in a row. Colour follows the "whose move is it"
* STATE_COLOR semantics; the neutral `default` states (e.g. awaiting info) have
* no accessible solid fill and fall back to an outlined chip.
*/
export default function StateChip({
state,
size = "small",
clickable = false,
}: StateChipProps): JSX.Element {
return (
<SemanticChip
role={stateColor(state)}
label={stateLabel(state)}
size={size}
clickable={clickable}
/>
);
}
2 changes: 1 addition & 1 deletion apps/csm-portal/webapp/src/config/csmNavItems.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ export const CSM_NAV_ITEMS: CsmNavItem[] = [
{ id: "time-cards", label: "Time cards", path: "/time-cards", icon: Clock },
{ id: "accounts", label: "Accounts", path: "/accounts", icon: Building },
{ id: "projects", label: "Projects", path: "/projects", icon: FolderOpen },
{ id: "admin", label: "Administration", path: "/admin", icon: Settings },
{ id: "admin", label: "Settings", path: "/admin", icon: Settings },
];

/** The nav item whose path is (a prefix of) `pathname`, if any. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ export default function CsmAdminLayout(): JSX.Element {

return (
<Box sx={{ display: "flex", flexDirection: "column", gap: 3 }}>
<Typography variant="h5">Administration</Typography>
<Typography variant="h5">Settings</Typography>

<Tabs
value={active}
Expand Down
Loading