Skip to content

Commit

Permalink
vis: add TreeDisplay (#1495)
Browse files Browse the repository at this point in the history
* minimally working tree layout

* add some new aggregation utils

* format

* add comment

* refactor how building the full tree works

* fix type errors

* merge duplicate monoliths and manually connect balancers to monoliths

* better balancer node positioning

* fix monolith trees overlapping

* adjust radius of clients

* fix tree placement for monoliths with no rooms

* hook up event bus to tree display

* add attributes for source and target node ids for links

* format

* fix zoom conflicting with other panels

* add text to nodes in tree

* make TreeDisplay remember current zoom position across rerenders

* add tree panel in provisioned dashboard

* format
  • Loading branch information
dyc3 authored Mar 14, 2024
1 parent 101f40e commit 6acf2dc
Show file tree
Hide file tree
Showing 7 changed files with 538 additions and 6 deletions.
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 @@ export function groupMonolithsByRegion(state: SystemState): Record<string, strin
}
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");
}
// 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");
}
// 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>(
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

0 comments on commit 6acf2dc

Please sign in to comment.