graphing around

This commit is contained in:
nora 2022-03-26 18:37:19 +01:00
parent 9855defeae
commit 881cabe2f0
6 changed files with 199 additions and 115 deletions

View file

@ -17,5 +17,5 @@ td {
} }
.graph { .graph {
height: 50vh; height: 70vh;
} }

View file

@ -1,112 +0,0 @@
import React from 'react';
import { GraphView } from 'react-digraph';
import { Binding, Data, Exchange } from '../types';
const sample = {
nodes: [
{
id: 1,
title: 'Exchange A',
type: 'empty',
},
{
id: 2,
title: 'Queue A',
type: 'empty',
},
],
edges: [
{
source: 1,
target: 2,
type: 'emptyEdge',
},
{
source: 2,
target: 4,
type: 'emptyEdge',
},
],
};
const graphConfig = {
nodeTypes: {
exchange: {
// required to show empty nodes
typeText: 'Exchange',
shapeId: '#empty', // relates to the type property of a node
shape: (
<symbol viewBox="0 0 100 100" id="empty" key="0">
<circle cx="50" cy="50" r="45" />
</symbol>
),
},
queue: {
// required to show empty nodes
typeText: 'Queue',
shapeId: '#empty', // relates to the type property of a node
shape: (
<symbol viewBox="0 0 100 100" id="empty" key="0">
<circle cx="50" cy="50" r="45" />
</symbol>
),
},
},
nodeSubtypes: {},
edgeTypes: {},
};
type Props = {
data: Data;
};
const SPACE = 200;
const EntityGraph = ({ data }: Props) => {
const queueTotal = (data.queues.length * SPACE) / 2;
const queues = data.queues.map((q, i) => ({
id: q.name,
title: q.name,
y: 300,
x: i * SPACE - queueTotal,
type: 'queue',
}));
const exchTotal = (data.exchanges.length * SPACE) / 2;
const exchanges = data.exchanges.map((e, i) => ({
id: e.name,
title: e.name,
y: 0,
x: i * SPACE - exchTotal,
type: 'exchange',
}));
const nodes = [...queues, ...exchanges];
const edges = data.exchanges
.flatMap((e) => e.bindings.map((b) => [b, e] as [Binding, Exchange]))
.map(([b, e]) => ({
source: b.queue,
target: e.name,
label_to: `'${b.routingKey}'`,
type: 'emptyEdge',
}));
const nodeTypes = graphConfig.nodeTypes;
const nodeSubtypes = graphConfig.nodeSubtypes;
const edgeTypes = graphConfig.edgeTypes;
return (
<div className="graph">
<GraphView
nodeKey="id"
nodes={nodes}
edges={edges}
nodeTypes={nodeTypes}
nodeSubtypes={nodeSubtypes}
edgeTypes={edgeTypes}
/>
</div>
);
};
export default EntityGraph;

View file

@ -1,7 +1,7 @@
import React, { FC, useCallback, useEffect, useState } from 'react'; import React, { FC, useCallback, useEffect, useState } from 'react';
import Table from './table'; import Table from './table';
import type { Data } from '../types'; import type { Data } from '../types';
import EntityGraph from './EntityGraph'; import EntityGraph from './entity-graph';
const fetchData = async (prefix: string): Promise<Data> => { const fetchData = async (prefix: string): Promise<Data> => {
const url = `${prefix}api/data`; const url = `${prefix}api/data`;
@ -74,12 +74,19 @@ const DataPage: FC<Props> = ({ prefix }) => {
<h2>Queues</h2> <h2>Queues</h2>
{data ? ( {data ? (
<Table <Table
headers={['Queue ID', 'Name', 'Durable', 'Message Count']} headers={[
'Queue ID',
'Name',
'Durable',
'Message Count',
'Consumer Count',
]}
rows={data.queues.map((queue) => [ rows={data.queues.map((queue) => [
queue.id, queue.id,
queue.name, queue.name,
queue.durable ? 'Yes' : 'No', queue.durable ? 'Yes' : 'No',
queue.messages, queue.messages,
queue.consumers.length,
])} ])}
/> />
) : ( ) : (

View file

@ -0,0 +1,167 @@
import React from 'react';
import { GraphView } from 'react-digraph';
import { Binding, Data, Exchange } from '../types';
const shape = (
<symbol viewBox="0 0 100 100" id="empty" key="0">
<circle cx="50" cy="50" r="30" />
</symbol>
);
const graphConfig = {
nodeTypes: {
exchange: {
// required to show empty nodes
typeText: 'Exchange',
shapeId: '#empty', // relates to the type property of a node
shape,
},
queue: {
// required to show empty nodes
typeText: 'Queue',
shapeId: '#empty', // relates to the type property of a node
shape,
},
consumer: {
// required to show empty nodes
typeText: 'Consumer',
shapeId: '#empty', // relates to the type property of a node
shape,
},
channel: {
// required to show empty nodes
typeText: 'Channel',
shapeId: '#empty', // relates to the type property of a node
shape,
},
connection: {
// required to show empty nodes
typeText: 'Connection',
shapeId: '#empty', // relates to the type property of a node
shape,
},
},
nodeSubtypes: {},
edgeTypes: {},
};
type Props = {
data: Data;
};
const SPACE_H = 120;
const SPACE_V = 150;
const EntityGraph = ({ data }: Props) => {
const exchTotal = (data.exchanges.length * SPACE_H) / 2;
const exchanges = data.exchanges.map((e, i) => ({
id: e.name,
title: e.name,
y: 0,
x: i * SPACE_H - exchTotal,
type: 'exchange',
}));
const queueTotal = (data.queues.length * SPACE_H) / 2;
const queues = data.queues.map((q, i) => ({
id: q.name,
title: q.name,
y: SPACE_V,
x: i * SPACE_H - queueTotal,
type: 'queue',
}));
const consumersData = data.queues.flatMap((q) =>
q.consumers.map((c) => [q, c] as const)
);
const consumerTotal = (consumersData.length * SPACE_H) / 2;
const consumers = consumersData.map(([q, c], i) => ({
id: c.tag,
title: c.tag,
y: SPACE_V * 2,
x: i * SPACE_H - consumerTotal,
type: 'consumer',
}));
const channelsData = data.connections.flatMap((c) => c.channels);
const channelTotal = (channelsData.length * SPACE_H) / 2;
const channels = channelsData.map((c, i) => ({
id: c.id,
title: c.number,
y: SPACE_V * 3,
x: i * SPACE_H - channelTotal,
type: 'channel',
}));
const connectionTotal = (data.connections.length * SPACE_H) / 2;
const connections = data.connections.map((c, i) => ({
id: c.id,
title: c.peerAddr,
y: SPACE_V * 4,
x: i * SPACE_H - connectionTotal,
type: 'connection',
}));
const nodes = [
...queues,
...exchanges,
...consumers,
...channels,
...connections,
];
const bindingEdges = data.exchanges
.flatMap((e) => e.bindings.map((b) => [b, e] as const))
.map(([b, e]) => ({
source: b.queue,
target: e.name,
label_to: `'${b.routingKey}'`,
type: 'emptyEdge',
}));
const consumerEdges = consumersData.map(([q, c]) => ({
source: c.tag,
target: q.name,
type: 'emptyEdge',
}));
const channelConsumerEdges = consumersData.map(([, c]) => ({
source: c.channel,
target: c.tag,
type: 'emptyEdge',
}));
const connectionChannelEdges = data.connections.flatMap((c) =>
c.channels.map((ch) => ({
source: c.id,
target: ch.id,
type: 'emptyEdge',
}))
);
const edges = [
...bindingEdges,
...consumerEdges,
...channelConsumerEdges,
...connectionChannelEdges,
];
const nodeTypes = graphConfig.nodeTypes;
const nodeSubtypes = graphConfig.nodeSubtypes;
const edgeTypes = graphConfig.edgeTypes;
return (
<div className="graph">
<GraphView
nodeKey="id"
nodes={nodes}
edges={edges}
nodeTypes={nodeTypes}
nodeSubtypes={nodeSubtypes}
edgeTypes={edgeTypes}
/>
</div>
);
};
export default EntityGraph;

View file

@ -9,11 +9,17 @@ export type Connection = {
channels: ReadonlyArray<Channel>; channels: ReadonlyArray<Channel>;
}; };
export type Consumer = {
tag: string;
channel: string;
};
export type Queue = { export type Queue = {
id: string; id: string;
name: string; name: string;
durable: boolean; durable: boolean;
messages: number; messages: number;
consumers: ReadonlyArray<Consumer>;
}; };
export type Binding = { export type Binding = {

View file

@ -79,6 +79,13 @@ struct Queue {
name: String, name: String,
durable: bool, durable: bool,
messages: usize, messages: usize,
consumers: Vec<Consumer>,
}
#[derive(Serialize)]
struct Consumer {
tag: String,
channel: String,
} }
#[derive(Serialize)] #[derive(Serialize)]
@ -124,6 +131,15 @@ async fn get_data(global_data: GlobalData) -> impl IntoResponse {
name: queue.name.to_string(), name: queue.name.to_string(),
durable: queue.durable, durable: queue.durable,
messages: queue.messages.len(), messages: queue.messages.len(),
consumers: queue
.consumers
.lock()
.values()
.map(|consumer| Consumer {
tag: consumer.tag.to_string(),
channel: consumer.channel.id.to_string(),
})
.collect(),
}) })
.collect(); .collect();