+
+ {
+ node.fx = node.x;
+ node.fy = node.y;
+ }}
+ nodeLabel={(node) => node.name}
+ nodeCanvasObjectMode={() => 'after'}
+ nodeCanvasObject={(node, ctx, globalScale) => {
+ const label = node.name;
+ const fontSize = 12 / globalScale;
+ ctx.font = `${fontSize}px Sans-Serif`;
+ ctx.textAlign = 'center';
+ ctx.textBaseline = 'middle';
+ ctx.fillStyle = 'black'; //node.color;
+ ctx.fillText(label, node.x, node.y + 6);
+ }}
+ linkCanvasObjectMode={() => 'after'}
+ linkLabel={(link) => `${link.source.name}->${link.target.name} max/avg/min ${link.latency_max}/${link.latency_avg}/${link.latency_min} ms`}
+ linkColor={(link) => {
+ return link.latency_avg > 1 ? (link.latency_avg > 100 ? 'red' : 'orange') : 'green'
+ }}
+ linkCanvasObject={(link, ctx) => {
+ const MAX_FONT_SIZE = 4;
+ const LABEL_NODE_MARGIN = 6 * 1.5;
+ const start = link.source;
+ const end = link.target;
+ // ignore unbound links
+ if (typeof start !== 'object' || typeof end !== 'object') return;
+ // calculate label positioning
+ function getQuadraticXY(t, sx, sy, cp1x, cp1y, ex, ey) {
+ return {
+ x: (1 - t) * (1 - t) * sx + 2 * (1 - t) * t * cp1x + t * t * ex,
+ y: (1 - t) * (1 - t) * sy + 2 * (1 - t) * t * cp1y + t * t * ey,
+ };
+ }
+ let textPos = Object.assign({},...['x', 'y'].map(c => ({
+ [c]: start[c] + (end[c] - start[c]) / 2 // calc middle point
+ })));
+ if (+link.curvature > 0) {
+ textPos = getQuadraticXY(
+ 0.5,
+ start.x,
+ start.y,
+ link.__controlPoints[0],
+ link.__controlPoints[1],
+ end.x,
+ end.y
+ );
+ }
+
+ const relLink = { x: end.x - start.x, y: end.y - start.y };
+ const maxTextLength = Math.sqrt(Math.pow(relLink.x, 2) + Math.pow(relLink.y, 2)) - LABEL_NODE_MARGIN * 2;
+ let textAngle = Math.atan2(relLink.y, relLink.x);
+ // maintain label vertical orientation for legibility
+ if (textAngle > Math.PI / 2) textAngle = -(Math.PI - textAngle);
+ if (textAngle < -Math.PI / 2) textAngle = -(-Math.PI - textAngle);
+ const label = `${link.latency_avg}ms`;
+ // estimate fontSize to fit in link length
+ ctx.font = '1px Sans-Serif';
+ const fontSize = Math.min(MAX_FONT_SIZE, maxTextLength / ctx.measureText(label).width);
+ ctx.font = `${fontSize}px Sans-Serif`;
+ const textWidth = ctx.measureText(label).width;
+ const bckgDimensions = [textWidth, fontSize].map(n => n + fontSize * 0.2); // some padding
+ // draw text label (with background rect)
+ ctx.save();
+ ctx.translate(textPos.x, textPos.y);
+ ctx.rotate(textAngle);
+ ctx.fillStyle = 'rgba(255, 255, 255, 0.8)';
+ ctx.fillRect(- bckgDimensions[0] / 2, - bckgDimensions[1] / 2, ...bckgDimensions);
+ ctx.textAlign = 'center';
+ ctx.textBaseline = 'middle';
+ ctx.fillStyle = 'darkgrey';
+ ctx.setLineDash([5, 5]);
+ ctx.fillText(label, 0, 0);
+ ctx.restore();
+ }}
+ />
+
+
+
--- latency > 100ms or failed
+
--- 1ms < latency < 100ms
+
--- latency < 1ms
+
+
+ )
+}
+
+export default PingGraph;
diff --git a/webui/src/services/capture.ts b/webui/src/services/capture.ts
new file mode 100644
index 00000000..7d8a3ca5
--- /dev/null
+++ b/webui/src/services/capture.ts
@@ -0,0 +1,32 @@
+import { request } from 'ice'
+
+export interface CaptureTask {
+ task_type: string
+ name: string
+ namespace: string
+}
+
+export interface CaptureResult {
+ task_id: number,
+ spec: CaptureTask,
+ status: string,
+ result: string,
+ message: string
+}
+
+export default {
+ async createCapture(task: CaptureTask): Promise