From 1106218779233d3c7647f068e382c5e0338627e3 Mon Sep 17 00:00:00 2001 From: Skylar Date: Thu, 19 Sep 2024 11:04:06 -0700 Subject: [PATCH] feat: Topology graph legend (#27) * WIP types * feat: more detailed hover tooltips * feat: legend shows flow IDs * chore: update types * fix: z-index and legend label * fix: update types from upstream * fix: pin.json hash * feat: clarify legend and tooltip wording --- flake.lock | 8 +- flake.nix | 2 +- kontrol-frontend/pin.json | 6 +- .../src/components/CytoscapeGraph/Legend.tsx | 92 +++++++++++++++++++ .../src/components/CytoscapeGraph/index.tsx | 31 ++++--- .../CytoscapeGraph/mocks/response.ts | 79 ++++++++++++++-- .../CytoscapeGraph/plugins/tippy.css | 28 ++++-- .../CytoscapeGraph/plugins/tippy.ts | 20 +++- .../src/components/CytoscapeGraph/utils.ts | 2 +- kontrol-frontend/src/pages/FlowsCreate.tsx | 18 +++- kontrol-frontend/src/types.d.ts | 6 +- 11 files changed, 246 insertions(+), 46 deletions(-) create mode 100644 kontrol-frontend/src/components/CytoscapeGraph/Legend.tsx diff --git a/flake.lock b/flake.lock index b02d0f8..99ca6e0 100644 --- a/flake.lock +++ b/flake.lock @@ -91,17 +91,17 @@ "nixpkgs": "nixpkgs" }, "locked": { - "lastModified": 1724085514, - "narHash": "sha256-VPbUHxDzsXNFL5jPSDNi0fER4ZxT6yRQVvIbHfJzJ5I=", + "lastModified": 1726692242, + "narHash": "sha256-O+SKbXmHX6xXqgQrW2fFohyJpRf/3ypbeFUaD5vEuBk=", "owner": "kurtosis-tech", "repo": "kardinal", - "rev": "75f64efda4b9cce0e0f9c2a1268baddb8131304a", + "rev": "42f6be285e469fb70eef8836ec0bed7bf0c3775c", "type": "github" }, "original": { "owner": "kurtosis-tech", "repo": "kardinal", - "rev": "75f64efda4b9cce0e0f9c2a1268baddb8131304a", + "rev": "42f6be285e469fb70eef8836ec0bed7bf0c3775c", "type": "github" } }, diff --git a/flake.nix b/flake.nix index 6e8479c..93c3df2 100644 --- a/flake.nix +++ b/flake.nix @@ -5,7 +5,7 @@ gomod2nix.url = "github:nix-community/gomod2nix"; gomod2nix.inputs.nixpkgs.follows = "nixpkgs"; gomod2nix.inputs.flake-utils.follows = "flake-utils"; - kardinal.url = "github:kurtosis-tech/kardinal/75f64efda4b9cce0e0f9c2a1268baddb8131304a"; + kardinal.url = "github:kurtosis-tech/kardinal/42f6be285e469fb70eef8836ec0bed7bf0c3775c"; }; outputs = { self, diff --git a/kontrol-frontend/pin.json b/kontrol-frontend/pin.json index 483271d..a670437 100644 --- a/kontrol-frontend/pin.json +++ b/kontrol-frontend/pin.json @@ -1,7 +1,7 @@ { - "version": "0.8.0", + "version": "0.9.0", "x86_64-darwin": "", - "x86_64-linux": "sha256-4lx4eSRv+auM27K4RLyLVS3lk4+a5SCFrsyBrOY2qMI=", - "aarch64-darwin": "sha256-mSmC23SczyR2V7xoS7+bnN5g+X5EVJTy74pH3+Ld/C8=", + "x86_64-linux": "sha256-0L/RHC4vHimBUV1ZxAaqK4ePQZbFV2cbLIv/Xp/iKto=", + "aarch64-darwin": "sha256-7eJPigPZrdtTcZgrww6Xp529NQ3/H4i0E4Cbq9R2/Ns=", "aarch64-linux": "sha256-iHZRAxgNluI3bvXIc6hgkAs/xM9Z4ib5W1ifnvGs9SQ=" } diff --git a/kontrol-frontend/src/components/CytoscapeGraph/Legend.tsx b/kontrol-frontend/src/components/CytoscapeGraph/Legend.tsx new file mode 100644 index 0000000..042214d --- /dev/null +++ b/kontrol-frontend/src/components/CytoscapeGraph/Legend.tsx @@ -0,0 +1,92 @@ +import { NodeVersion } from "@/types"; +import { Flex } from "@chakra-ui/react"; +import { + Table, + Thead, + Tbody, + Tr, + Th, + Td, + TableContainer, +} from "@chakra-ui/react"; + +interface Props { + elements: cytoscape.ElementDefinition[]; +} + +const Legend = ({ elements }: Props) => { + const serviceVersions: NodeVersion[] = elements + .map((element) => element.data.versions) + .flat() + .filter(Boolean); + + const flowIds = serviceVersions.map((version) => version.flowId); + const uniqueFlowIds = [...new Set(flowIds)]; + + const servicesForFlowId = (flowId: string): string => { + const services = elements + .map((element) => { + const versions = element.data.versions; + if ( + versions != null && + versions.length > 0 && + versions.some((version: NodeVersion) => version.flowId === flowId) + ) { + return element; + } + return undefined; + }) + .filter(Boolean); + // If flow ID is baseline flow, dont show all services + if ( + services.some( + (service) => + service?.data.versions.length === 1 && + service?.data.versions[0].isBaseline, + ) + ) { + return "All services"; + } + return services.map((service) => service?.data.label).join(", "); + }; + + return ( + + + + + + + + + + + {uniqueFlowIds.map((flowId) => { + return ( + + + + + ); + })} + +
Flow IDDeployed Services
{flowId}{servicesForFlowId(flowId)}
+
+
+ ); +}; + +export default Legend; diff --git a/kontrol-frontend/src/components/CytoscapeGraph/index.tsx b/kontrol-frontend/src/components/CytoscapeGraph/index.tsx index 68d4c04..527b89b 100644 --- a/kontrol-frontend/src/components/CytoscapeGraph/index.tsx +++ b/kontrol-frontend/src/components/CytoscapeGraph/index.tsx @@ -5,6 +5,8 @@ import stylesheet, { trafficNodeSelector } from "./stylesheet"; import { useInterval } from "@react-hooks-library/core"; import dagrePlugin, { dagreLayout } from "./plugins/dagre"; import tippyPlugin, { createTooltip, TooltipInstance } from "./plugins/tippy"; +import { Flex } from "@chakra-ui/react"; +import Legend from "./Legend"; // register plugins with cytoscape cytoscape.use(dagrePlugin); @@ -166,20 +168,21 @@ const CytoscapeGraph = ({ ); return ( - + + + + ); }; diff --git a/kontrol-frontend/src/components/CytoscapeGraph/mocks/response.ts b/kontrol-frontend/src/components/CytoscapeGraph/mocks/response.ts index 227ebdc..e009593 100644 --- a/kontrol-frontend/src/components/CytoscapeGraph/mocks/response.ts +++ b/kontrol-frontend/src/components/CytoscapeGraph/mocks/response.ts @@ -44,49 +44,112 @@ const data: ClusterTopology = { id: "cartservice", label: "cartservice", type: "service", - versions: ["dev-hr7dwojzkk", "prod"], + versions: [ + { + flowId: "k8s-namespace-1", + imageTag: "kurtosistech/cartservice:main", + isBaseline: true, + }, + { + flowId: "dev-hr7dwojzkk", + imageTag: "kurtosistech/cartservice:demo-on-sale", + isBaseline: false, + }, + ], }, { id: "checkoutservice", label: "checkoutservice", type: "service", - versions: ["dev-hr7dwojzkk", "prod"], + versions: [ + { + flowId: "k8s-namespace-1", + imageTag: "kurtosistech/checkoutservice:main", + isBaseline: true, + }, + { + flowId: "dev-hr7dwojzkk", + imageTag: "kurtosistech/checkoutservice:demo-on-sale", + isBaseline: false, + }, + ], }, { id: "frontend", label: "frontend", type: "service", - versions: ["dev-hr7dwojzkk", "prod"], + versions: [ + { + flowId: "k8s-namespace-1", + imageTag: "kurtosistech/frontend:main", + isBaseline: true, + }, + { + flowId: "dev-hr7dwojzkk", + imageTag: "kurtosistech/frontend:demo-on-sale", + isBaseline: false, + }, + ], }, { id: "emailservice", label: "emailservice", type: "service", - versions: ["prod"], + versions: [ + { + flowId: "k8s-namespace-1", + imageTag: "kurtosistech/emailservice:main", + isBaseline: true, + }, + ], }, { id: "paymentservice", label: "paymentservice", type: "service", - versions: ["prod"], + versions: [ + { + flowId: "k8s-namespace-1", + imageTag: "kurtosistech/paymentservice:main", + isBaseline: true, + }, + ], }, { id: "postgres", label: "postgres", type: "service", - versions: ["prod"], + versions: [ + { + flowId: "k8s-namespace-1", + imageTag: "kurtosistech/postgres:main", + isBaseline: true, + }, + ], }, { id: "shippingservice", label: "shippingservice", type: "service", - versions: ["prod"], + versions: [ + { + flowId: "k8s-namespace-1", + imageTag: "kurtosistech/shippingservice:main", + isBaseline: true, + }, + ], }, { id: "ingress", label: "ingress", type: "gateway", - versions: ["prod"], + versions: [ + { + flowId: "k8s-namespace-1", + imageTag: "kurtosistech/gateway:main", + isBaseline: true, + }, + ], }, ], }; diff --git a/kontrol-frontend/src/components/CytoscapeGraph/plugins/tippy.css b/kontrol-frontend/src/components/CytoscapeGraph/plugins/tippy.css index 3860895..7d46774 100644 --- a/kontrol-frontend/src/components/CytoscapeGraph/plugins/tippy.css +++ b/kontrol-frontend/src/components/CytoscapeGraph/plugins/tippy.css @@ -8,6 +8,7 @@ box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); font-family: monospace; font-size: 16px; + width: 600px; } .tooltip ul { @@ -17,23 +18,23 @@ .tooltip::before { content: ""; position: absolute; - top: 50%; - left: -15px; /* Adjust this value to position the arrow correctly */ - margin-top: -8px; /* This value should be half of border-width to center the arrow */ + top: -7px; + left: 50%; + margin-top: -8px; border-width: 8px; border-style: solid; - border-color: transparent #c0bfbf transparent transparent; + border-color: transparent transparent #c0bfbf transparent; } .tooltip::after { content: ""; position: absolute; - top: 50%; - left: -14px; /* Adjust this value to position the arrow correctly */ - margin-top: -8px; /* This value should be half of border-width to center the arrow */ + top: -6px; + left: 50%; + margin-top: -8px; border-width: 8px; border-style: solid; - border-color: transparent white transparent transparent; + border-color: transparent transparent white transparent; } .dot { @@ -44,3 +45,14 @@ border-radius: 50%; margin-right: 8px; } + +.tooltip table { + font-family: monospace; +} + +.tooltip table td, .tooltip table th { + padding: 4px 8px; + font-size: 14px; + text-align: left; + border-bottom: 1px solid #f0f0f0; +} diff --git a/kontrol-frontend/src/components/CytoscapeGraph/plugins/tippy.ts b/kontrol-frontend/src/components/CytoscapeGraph/plugins/tippy.ts index 4d7beb8..5199a0e 100644 --- a/kontrol-frontend/src/components/CytoscapeGraph/plugins/tippy.ts +++ b/kontrol-frontend/src/components/CytoscapeGraph/plugins/tippy.ts @@ -1,5 +1,6 @@ import cytoscapePopper from "cytoscape-popper"; import tippy, { Instance } from "tippy.js"; +import { NodeVersion } from "@/types"; import "./tippy.css"; // @ts-expect-error WIP @@ -14,7 +15,7 @@ const tippyFactory: cytoscapePopper.PopperFactory = (ref, content) => { content: content, // your own preferences: arrow: true, - placement: "right", + placement: "bottom", hideOnClick: false, sticky: "reference", @@ -29,7 +30,8 @@ const tippyFactory: cytoscapePopper.PopperFactory = (ref, content) => { export const createTooltip = ( node: cytoscape.NodeSingular, ): Instance | null => { - const versions = node.data("versions"); + const versions: NodeVersion[] = node.data("versions"); + console.log("versions", versions); if (!versions || versions.length === 0) { return null; } @@ -37,7 +39,19 @@ export const createTooltip = ( content: () => { const elem = document.createElement("div"); // TODO: sanitize - elem.innerHTML = `Versions: `; + elem.innerHTML = ` + + + + + + + + + ${versions.map((v: NodeVersion) => ``).join("")} + +
Flow IDImage Tag
${v.flowId}${v.imageTag || "N/A"}
+ `; elem.classList.add("tooltip"); return elem; }, diff --git a/kontrol-frontend/src/components/CytoscapeGraph/utils.ts b/kontrol-frontend/src/components/CytoscapeGraph/utils.ts index 9830d1f..ba2be30 100644 --- a/kontrol-frontend/src/components/CytoscapeGraph/utils.ts +++ b/kontrol-frontend/src/components/CytoscapeGraph/utils.ts @@ -2,7 +2,7 @@ import CytoscapeComponent from "react-cytoscapejs"; import type { ClusterTopology, ExtendedNode, Node } from "@/types"; export const extendNodeData = (node: Node): ExtendedNode => { - const versions = node.versions ?? ["UNKNOWN"]; + const versions = node.versions ?? []; return { data: { ...node, diff --git a/kontrol-frontend/src/pages/FlowsCreate.tsx b/kontrol-frontend/src/pages/FlowsCreate.tsx index 4f0983d..7b0689a 100644 --- a/kontrol-frontend/src/pages/FlowsCreate.tsx +++ b/kontrol-frontend/src/pages/FlowsCreate.tsx @@ -7,7 +7,7 @@ import { Stack, Flex, Grid } from "@chakra-ui/react"; import StatefulService from "@/components/StatefulService"; import CytoscapeGraph, { utils } from "@/components/CytoscapeGraph"; import { ChangeEvent, useEffect, useState } from "react"; -import { ClusterTopology, Node } from "@/types"; +import { ClusterTopology, Node, NodeVersion } from "@/types"; import { useApi } from "@/contexts/ApiContext"; import { useNavigate } from "react-router-dom"; @@ -21,6 +21,18 @@ interface TemplateConfig { description?: string; } +// fake preview of a flow built on the base topology +const PREVIEW_DEV_NODE_VERSION: NodeVersion = { + flowId: "new-dev-flow", + imageTag: "TBD", + isBaseline: false, +}; +const PREVIEW_BASELINE_NODE_VERSION: NodeVersion = { + flowId: "baseline", + imageTag: "TBD", + isBaseline: true, +}; + const Page = () => { const navigate = useNavigate(); const { getTopology, postTemplateCreate } = useApi(); @@ -51,8 +63,8 @@ const Page = () => { ...node, versions: formState.service.find((o) => o.value === node.id) != null - ? ["prod", "new-dev-flow"] - : ["prod"], + ? [PREVIEW_BASELINE_NODE_VERSION, PREVIEW_DEV_NODE_VERSION] + : [PREVIEW_BASELINE_NODE_VERSION], }; }), }); diff --git a/kontrol-frontend/src/types.d.ts b/kontrol-frontend/src/types.d.ts index 941b57b..a4cf58a 100644 --- a/kontrol-frontend/src/types.d.ts +++ b/kontrol-frontend/src/types.d.ts @@ -1,9 +1,13 @@ import type { components } from "cli-kontrol-api/api/typescript/client/types"; -export type ClusterTopology = components["schemas"]["ClusterTopology"]; export type Node = components["schemas"]["Node"]; + export type Edge = components["schemas"]["Edge"]; +export type ClusterTopology = components["schemas"]["ClusterTopology"]; + +export type NodeVersion = components["schemas"]["NodeVersion"]; + export interface ExtendedNode { data: Node; classes: string;