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 (
+
+
+
+
+
+ Flow ID |
+ Deployed Services |
+
+
+
+ {uniqueFlowIds.map((flowId) => {
+ return (
+
+ {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:
${versions.map((v: string) => `- ${v.toString()}
`).join("")}
`;
+ elem.innerHTML = `
+
+
+
+ Flow ID |
+ Image Tag |
+
+
+
+ ${versions.map((v: NodeVersion) => `${v.flowId} | ${v.imageTag || "N/A"} |
`).join("")}
+
+
+ `;
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;