Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

vis: add TreeDisplay #1495

Merged
merged 19 commits into from
Mar 14, 2024
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
42 changes: 41 additions & 1 deletion packages/ott-vis-panel/src/aggregate.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import type { SystemState } from "ott-vis";
import { aggMonolithRooms, countRoomClients, groupMonolithsByRegion } from "./aggregate";
import {
aggMonolithRooms,
countRoomClients,
dedupeMonoliths,
dedupeRooms,
groupMonolithsByRegion,
} from "./aggregate";

const sampleSystemState: SystemState = [
{
Expand Down Expand Up @@ -116,4 +122,38 @@ describe("aggregation helpers", () => {
cdg: ["0c85b46e-d343-46a3-ae4f-5f2aa1a8bdac", "f21df607-b572-4bdd-aa2f-3fead21bba86"],
});
});

it("dedupes rooms", () => {
const rooms = [
{ name: "foo", clients: 1 },
{ name: "bar", clients: 2 },
{ name: "foo", clients: 1 },
];
expect(dedupeRooms(rooms)).toEqual([
{ name: "foo", clients: 2 },
{ name: "bar", clients: 2 },
]);
});

it("dedupes rooms using sample data", () => {
const rooms = sampleSystemState.flatMap(b => b.monoliths.flatMap(m => m.rooms));
expect(dedupeRooms(rooms)).toEqual([
{ name: "foo", clients: 3 },
{ name: "bar", clients: 2 },
{ name: "baz", clients: 3 },
{ name: "qux", clients: 4 },
]);
});

it("dedupes monoliths", () => {
const monoliths = [
{ id: "a", region: "x", rooms: [{ name: "foo", clients: 2 }] },
{ id: "b", region: "x", rooms: [] },
{ id: "a", region: "x", rooms: [{ name: "foo", clients: 1 }] },
];
expect(dedupeMonoliths(monoliths)).toEqual([
{ id: "a", region: "x", rooms: [{ name: "foo", clients: 3 }] },
{ id: "b", region: "x", rooms: [] },
]);
});
});
62 changes: 61 additions & 1 deletion packages/ott-vis-panel/src/aggregate.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { SystemState } from "ott-vis";
import type { Monolith, Room, SystemState } from "ott-vis";

/**
* Builds a map of room names to the number of clients in each room from the room state.
Expand Down Expand Up @@ -46,3 +46,63 @@
}
return Object.fromEntries(Object.entries(regionMonoliths).map(([k, v]) => [k, Array.from(v)]));
}

/**
* Reduces two room states into a single room state.
* @param rA
* @param rB
* @returns
*/
function reduceRoom(rA: Room, rB: Room): Room {
if (rA.name !== rB.name) {
throw new Error("Cannot reduce rooms with different names");

Check warning on line 58 in packages/ott-vis-panel/src/aggregate.ts

View check run for this annotation

Codecov / codecov/patch

packages/ott-vis-panel/src/aggregate.ts#L58

Added line #L58 was not covered by tests
}
// FIXME: (perf) This is a potentially hot path, and we should avoid creating a new object here.
return {
name: rA.name,
clients: rA.clients + rB.clients,
};
}

function reduceMonolith(mA: Monolith, mB: Monolith): Monolith {
if (mA.id !== mB.id) {
throw new Error("Cannot reduce monoliths with different ids");

Check warning on line 69 in packages/ott-vis-panel/src/aggregate.ts

View check run for this annotation

Codecov / codecov/patch

packages/ott-vis-panel/src/aggregate.ts#L69

Added line #L69 was not covered by tests
}
// FIXME: (perf) This is a potentially hot path, and we should avoid creating a new object here.
return {
id: mA.id,
region: mA.region,
rooms: dedupeRooms([...mA.rooms, ...mB.rooms]),
};
}

export function dedupeItems<T>(

Check warning on line 79 in packages/ott-vis-panel/src/aggregate.ts

View check run for this annotation

Codecov / codecov/patch

packages/ott-vis-panel/src/aggregate.ts#L79

Added line #L79 was not covered by tests
items: T[],
getKey: (item: T) => string,
reduce: (a: T, b: T) => T
): T[] {
const itemMap = new Map<string, T>();
for (const item of items) {
const key = getKey(item);
let existingItem = itemMap.get(key);
if (!existingItem) {
existingItem = item;
itemMap.set(key, existingItem);
continue;
}
itemMap.set(key, reduce(existingItem, item));
}
return Array.from(itemMap.values());
}

/**
* Takes a list of rooms and produces a new list of rooms such that each room only appears once.
* @param rooms List of all rooms across all balancers
*/
export function dedupeRooms(rooms: Room[]): Room[] {
return dedupeItems(rooms, room => room.name, reduceRoom);
}

export function dedupeMonoliths(monoliths: Monolith[]): Monolith[] {
return dedupeItems(monoliths, monolith => monolith.id, reduceMonolith);
}
3 changes: 3 additions & 0 deletions packages/ott-vis-panel/src/components/CorePanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import GlobalView from "./views/GlobalView";
import RegionView from "./views/RegionView";
import { LoadingState } from "@grafana/schema";
import { useEventBus, type BusEvent } from "eventbus";
import TreeDisplay from "./TreeDisplay";

interface Props extends PanelProps<CoreOptions> {}

Expand Down Expand Up @@ -49,6 +50,8 @@ export const CorePanel: React.FC<Props> = ({ options, data, width, height }) =>
return <GlobalView height={height} width={width} systemState={systemState} />;
} else if (options.view === "region") {
return <RegionView height={height} width={width} systemState={systemState} />;
} else if (options.view === "tree") {
return <TreeDisplay height={height} width={width} systemState={systemState} />;
} else {
return <div>Invalid view</div>;
}
Expand Down
35 changes: 35 additions & 0 deletions packages/ott-vis-panel/src/components/TreeDisplay.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import * as d3 from "d3";
import { sizeOfTree } from "./TreeDisplay";

describe("TreeDisplay", () => {
it("should find the size of any d3 tree", () => {
interface FooTree {
name: string;
children: FooTree[];
}
const tree: FooTree = {
name: "root",
children: [
{
name: "child1",
children: [
{
name: "child1.1",
children: [],
},
],
},
{
name: "child2",
children: [],
},
],
};
const treeLayout = d3.tree<any>().nodeSize([10, 10]);
const root = d3.hierarchy(tree);
treeLayout(root);
const [width, height] = sizeOfTree(root);
expect(width).toBeGreaterThan(0);
expect(height).toBeGreaterThan(0);
});
});
Loading
Loading