feat: Update no items message in Notification Logs tables to include pluralization

This commit is contained in:
Nawaz Dhandala
2025-08-12 14:57:25 +01:00
parent 50f16d0fdc
commit 51d42c8436
7 changed files with 108 additions and 85 deletions

View File

@@ -25,6 +25,6 @@ declare module "elkjs/lib/elk.bundled.js" {
}
export default class ELK {
layout(graph: ElkNode): Promise<ElkNode>;
public layout(graph: ElkNode): Promise<ElkNode>;
}
}

View File

@@ -36,9 +36,13 @@ export interface ServiceDependencyGraphProps {
const ServiceDependencyGraph: FunctionComponent<ServiceDependencyGraphProps> = (
props: ServiceDependencyGraphProps,
): ReactElement => {
const computeLuminance = (r: number, g: number, b: number): number => {
const transform = (v: number): number => {
const c = v / 255;
const computeLuminance: (r: number, g: number, b: number) => number = (
r: number,
g: number,
b: number,
): number => {
const transform: (v: number) => number = (v: number): number => {
const c: number = v / 255;
return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
};
const R: number = transform(r);
@@ -47,7 +51,7 @@ const ServiceDependencyGraph: FunctionComponent<ServiceDependencyGraphProps> = (
return 0.2126 * R + 0.7152 * G + 0.0722 * B;
};
const getContrastText = (bg?: string): string => {
const getContrastText: (bg?: string) => string = (bg?: string): string => {
if (!bg) {
return "#111827"; // gray-900
}
@@ -73,7 +77,7 @@ const ServiceDependencyGraph: FunctionComponent<ServiceDependencyGraphProps> = (
if (hex.length === 3) {
hex = hex
.split("")
.map((c) => {
.map((c: string): string => {
return c + c;
})
.join("");
@@ -91,21 +95,25 @@ const ServiceDependencyGraph: FunctionComponent<ServiceDependencyGraphProps> = (
const [rfNodes, setRfNodes] = useState<Node[]>([]);
const [rfEdges, setRfEdges] = useState<Edge[]>([]);
useEffect(() => {
const elk = new ELK();
useEffect((): void => {
const elk: ELK = new ELK();
// fixed node dimensions for layout (px)
const NODE_WIDTH = 220;
const NODE_HEIGHT = 56;
const NODE_WIDTH: number = 220;
const NODE_HEIGHT: number = 56;
const sortedServices = [...props.services].sort((a, b) => {
return a.name.localeCompare(b.name) || a.id.localeCompare(b.id);
});
const sortedDeps = [...props.dependencies].sort((a, b) => {
if (a.fromServiceId === b.fromServiceId) {
return a.toServiceId.localeCompare(b.toServiceId);
}
return a.fromServiceId.localeCompare(b.fromServiceId);
});
const sortedServices: Array<ServiceNodeData> = [...props.services].sort(
(a: ServiceNodeData, b: ServiceNodeData): number => {
return a.name.localeCompare(b.name) || a.id.localeCompare(b.id);
},
);
const sortedDeps: Array<ServiceEdgeData> = [...props.dependencies].sort(
(a: ServiceEdgeData, b: ServiceEdgeData): number => {
if (a.fromServiceId === b.fromServiceId) {
return a.toServiceId.localeCompare(b.toServiceId);
}
return a.fromServiceId.localeCompare(b.fromServiceId);
},
);
const elkGraph: ElkNode = {
id: "root",
@@ -116,7 +124,7 @@ const ServiceDependencyGraph: FunctionComponent<ServiceDependencyGraphProps> = (
"elk.spacing.nodeNode": "60",
"elk.edgeRouting": "POLYLINE",
},
children: sortedServices.map((svc: ServiceNodeData) => {
children: sortedServices.map((svc: ServiceNodeData): ElkNode => {
return {
id: svc.id,
width: NODE_WIDTH,
@@ -132,35 +140,39 @@ const ServiceDependencyGraph: FunctionComponent<ServiceDependencyGraphProps> = (
}),
};
const layout = async (): Promise<void> => {
const layout: () => Promise<void> = async (): Promise<void> => {
try {
const res: any = await elk.layout(elkGraph as any);
const placedNodes: Node[] = (res.children || []).map((child: any) => {
const svc: ServiceNodeData | undefined = sortedServices.find((s) => {
return s.id === child.id;
});
const background: string = svc?.color || "#ffffff";
const textColor: string = getContrastText(background);
return {
id: child.id || "",
data: { label: svc?.name || "" },
position: { x: child.x || 0, y: child.y || 0 },
sourcePosition: Position.Right,
targetPosition: Position.Left,
style: {
borderRadius: 8,
padding: 8,
border: "1px solid rgba(0,0,0,0.08)",
background,
color: textColor,
boxShadow: "0 1px 2px rgba(16,24,40,.05)",
width: NODE_WIDTH,
height: NODE_HEIGHT,
},
} as Node;
});
const placedNodes: Node[] = (res.children || []).map(
(child: any): Node => {
const svc: ServiceNodeData | undefined = sortedServices.find(
(s: ServiceNodeData): boolean => {
return s.id === child.id;
},
);
const background: string = svc?.color || "#ffffff";
const textColor: string = getContrastText(background);
return {
id: child.id || "",
data: { label: svc?.name || "" },
position: { x: child.x || 0, y: child.y || 0 },
sourcePosition: Position.Right,
targetPosition: Position.Left,
style: {
borderRadius: 8,
padding: 8,
border: "1px solid rgba(0,0,0,0.08)",
background,
color: textColor,
boxShadow: "0 1px 2px rgba(16,24,40,.05)",
width: NODE_WIDTH,
height: NODE_HEIGHT,
},
} as Node;
},
);
const stroke = "#94a3b8"; // slate-400
const stroke: string = "#94a3b8"; // slate-400
const placedEdges: Edge[] = sortedDeps.map(
(dep: ServiceEdgeData): Edge => {
return {
@@ -177,39 +189,41 @@ const ServiceDependencyGraph: FunctionComponent<ServiceDependencyGraphProps> = (
setRfNodes(placedNodes);
setRfEdges(placedEdges);
} catch (e) {
} catch {
// Fallback: deterministic grid by name
const sorted = sortedServices;
const COLS = 4;
const GAP_X = 260;
const GAP_Y = 120;
const nodes: Node[] = sorted.map((svc: ServiceNodeData, i: number) => {
const col = i % COLS;
const row = Math.floor(i / COLS);
const x = col * GAP_X;
const y = row * GAP_Y;
const background: string = svc.color || "#ffffff";
const textColor: string = getContrastText(background);
return {
id: svc.id,
data: { label: svc.name },
position: { x, y },
sourcePosition: Position.Right,
targetPosition: Position.Left,
style: {
borderRadius: 8,
padding: 8,
border: "1px solid rgba(0,0,0,0.08)",
background,
color: textColor,
boxShadow: "0 1px 2px rgba(16,24,40,.05)",
width: NODE_WIDTH,
height: NODE_HEIGHT,
},
};
});
const stroke = "#94a3b8";
const edges: Edge[] = sortedDeps.map((dep: ServiceEdgeData) => {
const sorted: Array<ServiceNodeData> = sortedServices;
const COLS: number = 4;
const GAP_X: number = 260;
const GAP_Y: number = 120;
const nodes: Node[] = sorted.map(
(svc: ServiceNodeData, i: number): Node => {
const col: number = i % COLS;
const row: number = Math.floor(i / COLS);
const x: number = col * GAP_X;
const y: number = row * GAP_Y;
const background: string = svc.color || "#ffffff";
const textColor: string = getContrastText(background);
return {
id: svc.id,
data: { label: svc.name },
position: { x, y },
sourcePosition: Position.Right,
targetPosition: Position.Left,
style: {
borderRadius: 8,
padding: 8,
border: "1px solid rgba(0,0,0,0.08)",
background,
color: textColor,
boxShadow: "0 1px 2px rgba(16,24,40,.05)",
width: NODE_WIDTH,
height: NODE_HEIGHT,
},
};
},
);
const stroke: string = "#94a3b8";
const edges: Edge[] = sortedDeps.map((dep: ServiceEdgeData): Edge => {
return {
id: `e-${dep.fromServiceId}-${dep.toServiceId}`,
source: dep.fromServiceId,
@@ -226,7 +240,6 @@ const ServiceDependencyGraph: FunctionComponent<ServiceDependencyGraphProps> = (
};
layout();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [props.services, props.dependencies]);
return (
@@ -250,7 +263,7 @@ const ServiceDependencyGraph: FunctionComponent<ServiceDependencyGraphProps> = (
connectOnClick={false}
>
<MiniMap
nodeColor={(n) => {
nodeColor={(n: Node): string => {
return (
(n.style as any)?.background ||
(n.data as any)?.color ||

View File

@@ -112,7 +112,9 @@ const CallLogsTable: FunctionComponent<CallLogsTableProps> = (
props.noItemsMessage ||
(props.singularName
? `No call logs for this ${props.singularName}.`
: "No call logs.")
: props.pluralName
? `No ${props.pluralName.toLowerCase()} call logs.`
: "No call logs.")
}
showRefreshButton={true}
columns={props.columns || defaultColumns}

View File

@@ -113,7 +113,9 @@ const EmailLogsTable: FunctionComponent<EmailLogsTableProps> = (
props.noItemsMessage ||
(props.singularName
? `No email logs for this ${props.singularName}.`
: "No email logs.")
: props.pluralName
? `No ${props.pluralName.toLowerCase()} email logs.`
: "No email logs.")
}
showRefreshButton={true}
columns={props.columns || defaultColumns}

View File

@@ -119,7 +119,9 @@ const PushLogsTable: FunctionComponent<PushLogsTableProps> = (
props.noItemsMessage ||
(props.singularName
? `No Push logs for this ${props.singularName}.`
: "No Push logs.")
: props.pluralName
? `No ${props.pluralName.toLowerCase()} Push logs.`
: "No Push logs.")
}
showRefreshButton={true}
columns={props.columns || defaultColumns}

View File

@@ -112,7 +112,9 @@ const SmsLogsTable: FunctionComponent<SmsLogsTableProps> = (
props.noItemsMessage ||
(props.singularName
? `No SMS logs for this ${props.singularName}.`
: "No SMS logs.")
: props.pluralName
? `No ${props.pluralName.toLowerCase()} SMS logs.`
: "No SMS logs.")
}
showRefreshButton={true}
columns={props.columns || defaultColumns}

View File

@@ -138,7 +138,9 @@ const WorkspaceLogsTable: FunctionComponent<WorkspaceLogsTableProps> = (
props.noItemsMessage ||
(props.singularName
? `No Workspace logs for this ${props.singularName}.`
: "No Workspace logs.")
: props.pluralName
? `No ${props.pluralName.toLowerCase()} Workspace logs.`
: "No Workspace logs.")
}
showRefreshButton={true}
columns={props.columns || defaultColumns}