mirror of
https://github.com/Noratrieb/haesli.git
synced 2026-01-14 19:55:03 +01:00
graphing around
This commit is contained in:
parent
9855defeae
commit
881cabe2f0
6 changed files with 199 additions and 115 deletions
|
|
@ -17,5 +17,5 @@ td {
|
||||||
}
|
}
|
||||||
|
|
||||||
.graph {
|
.graph {
|
||||||
height: 50vh;
|
height: 70vh;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
|
||||||
|
|
@ -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,
|
||||||
])}
|
])}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
|
|
|
||||||
167
haesli_dashboard/frontend/src/components/entity-graph.tsx
Normal file
167
haesli_dashboard/frontend/src/components/entity-graph.tsx
Normal 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;
|
||||||
|
|
@ -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 = {
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue