Skip to content
This repository has been archived by the owner on Oct 21, 2024. It is now read-only.

Commit

Permalink
Merge branch 'main' into laurent/flow-create-error-handler
Browse files Browse the repository at this point in the history
  • Loading branch information
laurentluce committed Sep 25, 2024
2 parents 3b0131f + a428b37 commit 9e05c17
Show file tree
Hide file tree
Showing 7 changed files with 149 additions and 34 deletions.
31 changes: 17 additions & 14 deletions kontrol-frontend/src/components/CytoscapeGraph/Legend.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Flex } from "@chakra-ui/react";
import { Flex, IconButton } from "@chakra-ui/react";
import { useApi } from "@/contexts/ApiContext";
import {
Table,
Expand All @@ -9,14 +9,16 @@ import {
Td,
TableContainer,
} from "@/components/Table";
import { FiExternalLink } from "react-icons/fi";
import { FiExternalLink, FiEye, FiEyeOff } from "react-icons/fi";
import { useEffect, useState } from "react";
import { ClusterTopology, NodeVersion, Node } from "@/types";
import { Link } from "@chakra-ui/react";
import { useFlowsContext } from "@/contexts/FlowsContext";

const Legend = () => {
const { flows, getFlows, getTopology } = useApi();
const { getTopology } = useApi();
const [topology, setTopology] = useState<ClusterTopology | null>(null);
const { flows, flowVisibility, setFlowVisibility } = useFlowsContext();

const servicesForFlowId = (flowId: string, isBaseline: boolean): Node[] => {
if (topology == null) {
Expand All @@ -34,11 +36,14 @@ const Legend = () => {
};

useEffect(() => {
getFlows();
getTopology().then(setTopology);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

const sortedFlows = flows.sort((a, b) => {
return a["flow-id"].localeCompare(b["flow-id"]);
});

return (
<Flex
position={"absolute"}
Expand All @@ -57,11 +62,11 @@ const Legend = () => {
<Th>Flow ID</Th>
<Th>Baseline</Th>
<Th>URL</Th>
{/* <Th>Show/Hide</Th> */}
<Th>Show/Hide</Th>
</Tr>
</Thead>
<Tbody>
{flows.map((flow) => {
{sortedFlows.map((flow) => {
// TODO: Update these when this PR is merged:
// https://github.com/kurtosis-tech/kardinal/pull/234
const flowId = flow["flow-id"];
Expand All @@ -84,21 +89,20 @@ const Legend = () => {
<FiExternalLink size={12} />
</Link>
</Td>
{/* TODO: Uncomment when this feature is fully implemented
*
<Td textAlign={"right"}>
{highlightedFlowId === flowId ? (
{flowVisibility[flowId] === true ? (
<IconButton
aria-label="Hide"
h={"24px"}
minH={"24px"}
w={"24px"}
minW={"24px"}
color={isBaseline ? "gray.300" : "gray.600"}
icon={<FiEyeOff size={16} />}
icon={<FiEye size={16} />}
disabled={isBaseline}
onClick={() => {
// setHighlightedFlowId(null);
if (isBaseline) return; // cant hide baseline flow
setFlowVisibility(flowId, false);
}}
/>
) : (
Expand All @@ -108,17 +112,16 @@ const Legend = () => {
minH={"24px"}
w={"24px"}
minW={"24px"}
icon={<FiEye size={16} />}
disabled={isBaseline}
icon={<FiEyeOff size={16} />}
color={isBaseline ? "gray.300" : "gray.600"}
cursor={isBaseline ? "not-allowed" : "pointer"}
onClick={() => {
// setHighlightedFlowId(flowId);
setFlowVisibility(flowId, true);
}}
/>
)}
</Td>
**/}
</Tr>
);
})}
Expand Down
10 changes: 6 additions & 4 deletions kontrol-frontend/src/components/CytoscapeGraph/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,12 @@ const CytoscapeGraph = ({
if (tooltip.current != null) {
tooltip.current.destroy();
}
const tooltipInstance = createTooltip(e.target);
if (tooltipInstance == null) return;
tooltip.current = tooltipInstance.instance;
setTooltipPortalElem(tooltipInstance.element);
const newTooltip = createTooltip(e.target);
if (newTooltip == null) return;
const { instance, element } = newTooltip;
if (instance == null || element == null) return;
tooltip.current = instance;
setTooltipPortalElem(element);
setHoveredNode(e.target);
});
cy.current.on("mouseout", function () {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ const stylesheet = [
{
selector: ".external",
css: {
ghost: "no",
"line-style": "dashed",
"border-style": "dotted",
"background-color": "#f2ebf8",
Expand Down
26 changes: 13 additions & 13 deletions kontrol-frontend/src/contexts/ApiContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,35 +25,37 @@ export type RequestBody<
: never;

export interface ApiContextType {
deleteFlow: (flowId: string) => Promise<void>;
deleteFlow: (flowId: string) => Promise<Flow[]>;
deleteTemplate: (templateName: string) => Promise<void>;
error: string | null;
flows: Flow[];
getFlows: () => Promise<void>;
getFlows: () => Promise<Flow[]>;
getTemplates: () => Promise<void>;
getTopology: () => Promise<components["schemas"]["ClusterTopology"]>;
loading: boolean;
postFlowCreate: (
b: RequestBody<"/tenant/{uuid}/flow/create", "post">,
) => Promise<void>;
) => Promise<Flow>;
postTemplateCreate: (
b: RequestBody<"/tenant/{uuid}/templates/create", "post">,
) => Promise<void>;
templates: Template[];
}

const defaultContextValue: ApiContextType = {
deleteFlow: async () => {},
deleteFlow: async () => [],
deleteTemplate: async () => {},
error: null,
flows: [],
getFlows: async () => {},
getFlows: async () => [],
getTemplates: async () => {},
getTopology: async () => {
return { nodes: [], edges: [] };
},
loading: false,
postFlowCreate: async () => {},
postFlowCreate: async () => ({
"flow-id": "",
"flow-urls": [],
isBaseline: false,
}),
postTemplateCreate: async () => {},
templates: [],
};
Expand All @@ -72,7 +74,6 @@ export const ApiContextProvider = ({ children }: PropsWithChildren) => {
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [templates, setTemplates] = useState<Template[]>([]);
const [flows, setFlows] = useState<Flow[]>([]);

// boilerplate loading state, error handling for any API call
const handleApiCall = useCallback(async function <T>(
Expand Down Expand Up @@ -104,7 +105,7 @@ export const ApiContextProvider = ({ children }: PropsWithChildren) => {
body,
}),
);
setFlows((state) => [...state, flow]);
return flow;
},
[uuid, handleApiCall],
);
Expand All @@ -117,7 +118,7 @@ export const ApiContextProvider = ({ children }: PropsWithChildren) => {
params: { path: { uuid } },
}),
);
setFlows(flows);
return flows;
}, [uuid, handleApiCall]);

// DELETE "/tenant/{uuid}/flow/{flow-id}"
Expand All @@ -129,7 +130,7 @@ export const ApiContextProvider = ({ children }: PropsWithChildren) => {
params: { path: { uuid, "flow-id": flowId } },
}),
);
setFlows(flows);
return flows;
},
[uuid, handleApiCall],
);
Expand Down Expand Up @@ -193,7 +194,6 @@ export const ApiContextProvider = ({ children }: PropsWithChildren) => {
deleteFlow,
deleteTemplate,
error,
flows,
getFlows,
getTemplates,
getTopology,
Expand Down
90 changes: 90 additions & 0 deletions kontrol-frontend/src/contexts/FlowsContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import {
createContext,
useContext,
useState,
ReactNode,
useEffect,
} from "react";
import { useApi } from "@/contexts/ApiContext";
import { Flow } from "@/types";

interface FlowsContextType {
flows: Flow[];
refetchFlows: () => Promise<Flow[]>;
flowVisibility: Record<string, boolean>;
setFlowVisibility: (flowId: string, visible: boolean) => void;
}

// Create the context with a default value
const FlowsContext = createContext<FlowsContextType>({
flows: [],
refetchFlows: async () => [],
flowVisibility: {},
setFlowVisibility: () => {},
});

// Create a provider component
interface FlowsContextProviderProps {
children: ReactNode;
}

export const FlowsContextProvider = ({
children,
}: FlowsContextProviderProps) => {
const { getFlows } = useApi();
const [flows, setFlows] = useState<Flow[]>([]);

const [flowVisibility, setFlowVisibility] = useState<Record<string, boolean>>(
{},
);

useEffect(() => {
setFlowVisibility((prevState) =>
flows.reduce(
(acc, flow) => ({
...acc,
[flow["flow-id"]]: prevState[flow["flow-id"]] ?? true,
}),
{},
),
);
}, [flows]);

useEffect(() => {
getFlows().then(setFlows);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

const refetchFlows = async () => {
const newFlows = await getFlows();
setFlows(newFlows);
return newFlows;
};

return (
<FlowsContext.Provider
value={{
flows,
refetchFlows,
flowVisibility,
setFlowVisibility: (flowId: string, visible: boolean) => {
setFlowVisibility({ ...flowVisibility, [flowId]: visible });
},
}}
>
{children}
</FlowsContext.Provider>
);
};

// Create a custom hook to use the context
// eslint-disable-next-line react-refresh/only-export-components
export const useFlowsContext = (): FlowsContextType => {
const context = useContext(FlowsContext);
if (context === undefined) {
throw new Error(
"useFlowsContext must be used within a FlowsContextProvider",
);
}
return context;
};
5 changes: 4 additions & 1 deletion kontrol-frontend/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import NotFound from "@/pages/NotFound";
import { ErrorBoundary } from "react-error-boundary";
import { NavigationContextProvider } from "@/contexts/NavigationContext";
import { ApiContextProvider } from "@/contexts/ApiContext";
import { FlowsContextProvider } from "@/contexts/FlowsContext";

const router = createBrowserRouter([
{
Expand Down Expand Up @@ -72,7 +73,9 @@ ReactDOM.createRoot(document.getElementById("root")!).render(
<ChakraProvider theme={theme} resetCSS>
<ApiContextProvider>
<NavigationContextProvider>
<RouterProvider router={router} />
<FlowsContextProvider>
<RouterProvider router={router} />
</FlowsContextProvider>
</NavigationContextProvider>
</ApiContextProvider>
</ChakraProvider>
Expand Down
20 changes: 18 additions & 2 deletions kontrol-frontend/src/pages/TrafficConfiguration.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,31 @@ import { Grid } from "@chakra-ui/react";
import CytoscapeGraph, { utils } from "@/components/CytoscapeGraph";
import { ElementDefinition } from "cytoscape";
import { useApi } from "@/contexts/ApiContext";
import { useFlowsContext } from "@/contexts/FlowsContext";

const pollingIntervalSeconds = 1;

const Page = () => {
const [elems, setElems] = useState<ElementDefinition[]>([]);
const prevResponse = useRef<string>();
const { getTopology } = useApi();
const { refetchFlows, flowVisibility } = useFlowsContext();

useEffect(() => {
const fetchElems = async () => {
const response = await getTopology();
const newElems = utils.normalizeData(response);
const filtered = {
...response,
nodes: response.nodes.map((node) => {
return {
...node,
versions: node.versions?.filter((version) => {
return flowVisibility[version.flowId] === true;
}),
};
}),
};
const newElems = utils.normalizeData(filtered);

// dont update react state if the API response is identical to the previous one
// This avoids unnecessary re-renders
Expand All @@ -23,13 +36,16 @@ const Page = () => {
}
prevResponse.current = JSON.stringify(newElems);
setElems(newElems);
// re-fetch flows if topology changes
refetchFlows();
};

// Continuously fetch elements
const intervalId = setInterval(fetchElems, pollingIntervalSeconds * 1000);
fetchElems();
return () => clearInterval(intervalId);
}, [getTopology]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [flowVisibility]);

return (
<Grid
Expand Down

0 comments on commit 9e05c17

Please sign in to comment.