Skip to content

Commit 5e3fbcb

Browse files
authored
Format entity counts (#1477)
1 parent 51f96b7 commit 5e3fbcb

File tree

5 files changed

+127
-4
lines changed

5 files changed

+127
-4
lines changed

packages/graph-explorer/src/core/StateProvider/graphSession/useRestoreGraphSession.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ import { toast } from "sonner";
33

44
import { fetchEntityDetails, notifyOnIncompleteRestoration } from "@/connector";
55
import { useAddToGraph } from "@/hooks";
6-
import { formatEntityCounts, logger } from "@/utils";
6+
import { useEntityCountFormatterCallback } from "@/hooks/useEntityCountFormatter";
7+
import { logger } from "@/utils";
78
import { createDisplayError } from "@/utils/createDisplayError";
89

910
import type { GraphSessionStorageModel } from "./storage";
@@ -14,6 +15,7 @@ import type { GraphSessionStorageModel } from "./storage";
1415
export function useRestoreGraphSession() {
1516
const queryClient = useQueryClient();
1617
const addToGraph = useAddToGraph();
18+
const formatEntityCounts = useEntityCountFormatterCallback();
1719

1820
const mutation = useMutation({
1921
mutationFn: async (graph: GraphSessionStorageModel) => {
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import { vi } from "vitest";
2+
3+
import { renderHookWithJotai } from "@/utils/testing";
4+
5+
import {
6+
useEntityCountFormatterCallback,
7+
useFormattedEntityCounts,
8+
} from "./useEntityCountFormatter";
9+
10+
// Mock useTranslations
11+
vi.mock("./useTranslations", () => ({
12+
default: () => (key: string) => {
13+
const translations: Record<string, string> = {
14+
node: "node",
15+
nodes: "nodes",
16+
edge: "edge",
17+
edges: "edges",
18+
};
19+
return translations[key] || key;
20+
},
21+
}));
22+
23+
describe("useFormattedEntityCounts", () => {
24+
it("should format single node and single edge", () => {
25+
const { result } = renderHookWithJotai(() =>
26+
useFormattedEntityCounts(1, 1),
27+
);
28+
expect(result.current).toBe("1 node and 1 edge");
29+
});
30+
31+
it("should format multiple nodes and multiple edges", () => {
32+
const { result } = renderHookWithJotai(() =>
33+
useFormattedEntityCounts(5, 3),
34+
);
35+
expect(result.current).toBe("5 nodes and 3 edges");
36+
});
37+
38+
it("should format only nodes when edge count is zero", () => {
39+
const { result } = renderHookWithJotai(() =>
40+
useFormattedEntityCounts(2, 0),
41+
);
42+
expect(result.current).toBe("2 nodes");
43+
});
44+
45+
it("should format only edges when node count is zero", () => {
46+
const { result } = renderHookWithJotai(() =>
47+
useFormattedEntityCounts(0, 4),
48+
);
49+
expect(result.current).toBe("4 edges");
50+
});
51+
52+
it("should return empty string when both counts are zero", () => {
53+
const { result } = renderHookWithJotai(() =>
54+
useFormattedEntityCounts(0, 0),
55+
);
56+
expect(result.current).toBe("");
57+
});
58+
59+
it("should format large numbers with locale formatting", () => {
60+
const { result } = renderHookWithJotai(() =>
61+
useFormattedEntityCounts(1000, 2500),
62+
);
63+
expect(result.current).toBe("1,000 nodes and 2,500 edges");
64+
});
65+
});
66+
67+
describe("useEntityCountFormatterCallback", () => {
68+
it("should return a function that formats counts", () => {
69+
const { result } = renderHookWithJotai(() =>
70+
useEntityCountFormatterCallback(),
71+
);
72+
73+
expect(typeof result.current).toBe("function");
74+
expect(result.current(1, 1)).toBe("1 node and 1 edge");
75+
});
76+
77+
it("should handle different count combinations", () => {
78+
const { result } = renderHookWithJotai(() =>
79+
useEntityCountFormatterCallback(),
80+
);
81+
82+
const format = result.current;
83+
84+
expect(format(0, 0)).toBe("");
85+
expect(format(1, 0)).toBe("1 node");
86+
expect(format(0, 1)).toBe("1 edge");
87+
expect(format(10, 5)).toBe("10 nodes and 5 edges");
88+
});
89+
});
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import useTranslations from "./useTranslations";
2+
3+
export function useFormattedEntityCounts(
4+
vertexCount: number,
5+
edgeCount: number,
6+
) {
7+
const format = useEntityCountFormatterCallback();
8+
return format(vertexCount, edgeCount);
9+
}
10+
11+
export function useEntityCountFormatterCallback() {
12+
const t = useTranslations();
13+
14+
const format = (vertexCount: number, edgeCount: number) => {
15+
const nodeMessage =
16+
vertexCount === 0
17+
? null
18+
: `${vertexCount.toLocaleString()} ${vertexCount === 1 ? t("node") : t("nodes")}`.toLocaleLowerCase();
19+
const edgeMessage =
20+
edgeCount === 0
21+
? null
22+
: `${edgeCount.toLocaleString()} ${edgeCount === 1 ? t("edge") : t("edges")}`.toLocaleLowerCase();
23+
24+
return [nodeMessage, edgeMessage]
25+
.filter(message => message != null)
26+
.join(" and ");
27+
};
28+
return format;
29+
}

packages/graph-explorer/src/modules/GraphViewer/GraphViewerEmptyState.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ import {
1010
useAvailablePreviousSession,
1111
useRestoreGraphSession,
1212
} from "@/core";
13-
import { cn, formatEntityCounts } from "@/utils";
13+
import { useFormattedEntityCounts } from "@/hooks/useEntityCountFormatter";
14+
import { cn } from "@/utils";
1415

1516
export function GraphViewerEmptyState(props: ComponentPropsWithRef<"div">) {
1617
const availablePrevSession = useAvailablePreviousSession();
@@ -35,7 +36,7 @@ function RestorePreviousSessionEmptyState({
3536
} & ComponentPropsWithRef<"div">) {
3637
const restore = useRestoreGraphSession();
3738

38-
const entityCounts = formatEntityCounts(
39+
const entityCounts = useFormattedEntityCounts(
3940
prevSession.vertices.size,
4041
prevSession.edges.size,
4142
);

packages/graph-explorer/src/modules/GraphViewer/ImportGraphButton.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,9 @@ import { FileButton, PanelHeaderActionButton, Spinner } from "@/components";
88
import { fetchEntityDetails, notifyOnIncompleteRestoration } from "@/connector";
99
import { configurationAtom, type ConnectionWithId, useExplorer } from "@/core";
1010
import { useAddToGraph } from "@/hooks";
11+
import { useEntityCountFormatterCallback } from "@/hooks/useEntityCountFormatter";
1112
import { getTranslation } from "@/hooks/useTranslations";
12-
import { formatEntityCounts, logger } from "@/utils";
13+
import { logger } from "@/utils";
1314
import { fromFileToJson } from "@/utils/fileData";
1415

1516
import {
@@ -40,6 +41,7 @@ function useImportGraphMutation() {
4041
const queryClient = useQueryClient();
4142
const explorer = useExplorer();
4243
const addToGraph = useAddToGraph();
44+
const formatEntityCounts = useEntityCountFormatterCallback();
4345
const allConfigs = useAtomValue(configurationAtom);
4446
const allConnections = allConfigs
4547
.values()

0 commit comments

Comments
 (0)