diff --git a/src/App.tsx b/src/App.tsx index 2c65a2dd3..01befb4f9 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -35,6 +35,7 @@ import { SchemaResourceType, schemaResourceTypes } from "./components/SchemaResourcePage/resourceTypes"; +import { TopologyPageWrapper } from "./components/Topology/TopologyPageWrapper"; import { ConfigPageContextProvider } from "./context/ConfigPageContext"; import { useFeatureFlagsContext } from "./context/FeatureFlagsContext"; import { HealthPageContextProvider } from "./context/HealthPageContext"; @@ -266,15 +267,63 @@ export function IncidentManagerRoutes({ sidebar }: { sidebar: ReactNode }) { /> - , - tables.database, - "read", - true - )} - /> + + , + tables.database, + "read", + true + )} + /> + + , + tables.database, + "read", + true + )} + /> + , + tables.database, + "read", + true + )} + /> + , + tables.database, + "read", + true + )} + /> + , + tables.database, + "read", + true + )} + /> + , + tables.database, + "read" + )} + /> + + (null); + + return useQuery( + [ + "topologies", + id, + healthStatus, + team, + selectedLabel, + topologyType, + showHiddenComponents, + sortBy, + sortOrder, + agentId + ], + () => { + loadingBarRef.current?.continuousStart(); + const apiParams = { + id, + status: healthStatus, + type: topologyType, + team: team, + labels: selectedLabel, + sortBy, + sortOrder, + // only flatten, if topology type is set + ...(topologyType && + topologyType.toString().toLowerCase() !== "all" && { + flatten: true + }), + hidden: showHiddenComponents === "no" ? false : undefined, + agent_id: agentId + }; + return getTopology(apiParams); + }, + { + onSettled: () => { + loadingBarRef.current?.complete(); + } + } + ); +} diff --git a/src/api/services/configs.ts b/src/api/services/configs.ts index 38f0825e8..d05c499a5 100644 --- a/src/api/services/configs.ts +++ b/src/api/services/configs.ts @@ -11,6 +11,7 @@ import { ConfigChange, ConfigHealthCheckView, ConfigItem, + ConfigItemDetails, ConfigSummary, ConfigTypeRelationships } from "../types/configs"; @@ -138,7 +139,7 @@ export const getAllChanges = ( }; export const getConfig = (id: string) => - resolvePostGrestRequestWithPagination( + resolvePostGrestRequestWithPagination( ConfigDB.get(`/config_detail?id=eq.${id}&select=*,config_scrapers(id,name)`) ); diff --git a/src/api/types/configs.ts b/src/api/types/configs.ts index 9ecfda145..571420874 100644 --- a/src/api/types/configs.ts +++ b/src/api/types/configs.ts @@ -1,5 +1,6 @@ import { Agent, Avatar, CreatedAt, Timestamped } from "../traits"; import { HealthCheckSummary } from "./health"; +import { Topology } from "./topology"; export interface ConfigChange extends CreatedAt { id: string; @@ -69,13 +70,6 @@ export interface ConfigItem extends Timestamped, Avatar, Agent, Costs { id: string; name: string; }; - summary?: { - relationships?: number; - analysis?: number; - changes?: number; - playbook_runs?: number; - checks?: number; - }; properties?: { icon: string; name: string; @@ -87,6 +81,17 @@ export interface ConfigItem extends Timestamped, Avatar, Agent, Costs { last_scraped_time?: string; } +export interface ConfigItemDetails extends ConfigItem { + summary?: { + relationships?: number; + analysis?: number; + changes?: number; + playbook_runs?: number; + checks?: number; + }; + components?: Topology[]; +} + export interface ConfigItemGraphData extends ConfigItem { expanded?: boolean; expandable?: boolean; diff --git a/src/components/Configs/ConfigDetailsTabs.tsx b/src/components/Configs/ConfigDetailsTabs.tsx index a91d715be..1a2e47496 100644 --- a/src/components/Configs/ConfigDetailsTabs.tsx +++ b/src/components/Configs/ConfigDetailsTabs.tsx @@ -1,4 +1,5 @@ import { SearchLayout } from "@flanksource-ui/ui/Layout/SearchLayout"; +import IncidentDetailsPageSkeletonLoader from "@flanksource-ui/ui/SkeletonLoader/IncidentDetailsPageSkeletonLoader"; import clsx from "clsx"; import { useAtom } from "jotai"; import { ReactNode } from "react"; @@ -9,21 +10,24 @@ import { Head } from "../../ui/Head"; import { refreshButtonClickedTrigger } from "../../ui/SlidingSideBar/SlidingSideBar"; import TabbedLinks from "../../ui/Tabs/TabbedLinks"; import { ErrorBoundary } from "../ErrorBoundary"; +import { TopologyCard } from "../Topology/TopologyCard"; import { useConfigDetailsTabs } from "./ConfigTabsLinks"; import ConfigSidebar from "./Sidebar/ConfigSidebar"; -type ConfigDetailsTabsProps = { +export type ConfigTab = + | "Catalog" + | "Changes" + | "Insights" + | "Relationships" + | "Playbooks" + | "Checks"; + +export type ConfigDetailsTabsProps = { refetch?: () => void; children: ReactNode; isLoading?: boolean; pageTitlePrefix: string; - activeTabName: - | "Catalog" - | "Changes" - | "Insights" - | "Relationships" - | "Playbooks" - | "Checks"; + activeTabName: ConfigTab; className?: string; }; @@ -69,21 +73,31 @@ export function ConfigDetailsTabs({ loading={isLoading} contentClass="p-0 h-full overflow-y-hidden" > -
-
- + ) : ( +
+
+ {configItem?.components && configItem.components.length === 1 && ( +
+ +
)} - > - {children} - + + + {children} + +
+
- -
+ )} ); diff --git a/src/components/Configs/ConfigTabsLinks.tsx b/src/components/Configs/ConfigTabsLinks.tsx index 293b40798..84d965657 100644 --- a/src/components/Configs/ConfigTabsLinks.tsx +++ b/src/components/Configs/ConfigTabsLinks.tsx @@ -1,12 +1,15 @@ import { Badge } from "@flanksource-ui/ui/Badge/Badge"; import { useParams } from "react-router-dom"; -import { ConfigItem } from "../../api/types/configs"; +import { ConfigItemDetails } from "../../api/types/configs"; -export function useConfigDetailsTabs(countSummary?: ConfigItem["summary"]) { +export function useConfigDetailsTabs( + countSummary?: ConfigItemDetails["summary"], + basePath: `/${string}` = "/catalog" +) { const { id } = useParams<{ id: string }>(); return [ - { label: "Config", key: "Catalog", path: `/catalog/${id}` }, + { label: "Config", key: "Catalog", path: `${basePath}/${id}` }, { label: ( <> @@ -15,7 +18,7 @@ export function useConfigDetailsTabs(countSummary?: ConfigItem["summary"]) { ), key: "Changes", - path: `/catalog/${id}/changes` + path: `${basePath}/${id}/changes` }, { label: ( @@ -25,7 +28,7 @@ export function useConfigDetailsTabs(countSummary?: ConfigItem["summary"]) { ), key: "Insights", - path: `/catalog/${id}/insights` + path: `${basePath}/${id}/insights` }, { label: ( @@ -35,7 +38,7 @@ export function useConfigDetailsTabs(countSummary?: ConfigItem["summary"]) { ), key: "Relationships", - path: `/catalog/${id}/relationships` + path: `${basePath}/${id}/relationships` }, { label: ( @@ -45,7 +48,7 @@ export function useConfigDetailsTabs(countSummary?: ConfigItem["summary"]) { ), key: "Playbooks", - path: `/catalog/${id}/playbooks` + path: `${basePath}/${id}/playbooks` }, { label: ( @@ -55,7 +58,7 @@ export function useConfigDetailsTabs(countSummary?: ConfigItem["summary"]) { ), key: "Checks", - path: `/catalog/${id}/checks` + path: `${basePath}/${id}/checks` } ]; } diff --git a/src/components/Topology/MergedTopologyConfigPage.tsx b/src/components/Topology/MergedTopologyConfigPage.tsx new file mode 100644 index 000000000..51ce94ebd --- /dev/null +++ b/src/components/Topology/MergedTopologyConfigPage.tsx @@ -0,0 +1,70 @@ +import { Topology } from "@flanksource-ui/api/types/topology"; +import IncidentDetailsPageSkeletonLoader from "@flanksource-ui/ui/SkeletonLoader/IncidentDetailsPageSkeletonLoader"; +import clsx from "clsx"; +import { useGetConfigByIdQuery } from "../../api/query-hooks"; +import TabbedLinks from "../../ui/Tabs/TabbedLinks"; +import { ConfigTab } from "../Configs/ConfigDetailsTabs"; +import { useConfigDetailsTabs } from "../Configs/ConfigTabsLinks"; +import { ErrorBoundary } from "../ErrorBoundary"; +import { TopologyCard } from "./TopologyCard"; +import { useTopologyCardWidth } from "./TopologyPopover/topologyPreference"; + +type ConfigDetailsTabsForTopologyPageProps = { + configId: string; + topologies: Topology[]; + activeTabName?: ConfigTab; + className?: string; + children: React.ReactNode; +}; + +export function MergedTopologyConfigPage({ + children, + activeTabName = "Catalog", + className = "p-2", + configId: id, + topologies +}: ConfigDetailsTabsForTopologyPageProps) { + const { data: configItem, isLoading: isLoadingConfig } = + useGetConfigByIdQuery(id!); + + const configTabList = useConfigDetailsTabs(configItem?.summary, "/topology"); + + const [topologyCardSize] = useTopologyCardWidth(); + + return ( + // eslint-disable-next-line react/jsx-no-useless-fragment + <> + {isLoadingConfig ? ( + + ) : ( +
+
+ {topologies.length > 0 && ( +
+ {topologies.map((topology) => ( + + ))} +
+ )} + + + {children} + +
+
+ )} + + ); +} diff --git a/src/components/Topology/TopologyCard/index.tsx b/src/components/Topology/TopologyCard/index.tsx index aa7a654fd..b85e2228b 100644 --- a/src/components/Topology/TopologyCard/index.tsx +++ b/src/components/Topology/TopologyCard/index.tsx @@ -37,7 +37,20 @@ export const StatusStyles: Record = { interface IProps { size?: Size | string; topologyId?: string; - topology?: Topology; + topology?: Pick< + Topology, + | "summary" + | "is_leaf" + | "id" + | "properties" + | "components" + | "agent_id" + | "status" + | "status_reason" + | "text" + | "name" + | "icon" + >; selectionMode?: boolean; hideMenu?: boolean; // where to open new links diff --git a/src/components/Topology/TopologyPage/TopologyFilterBar.tsx b/src/components/Topology/TopologyPage/TopologyFilterBar.tsx index 9430d7969..2b573ebeb 100644 --- a/src/components/Topology/TopologyPage/TopologyFilterBar.tsx +++ b/src/components/Topology/TopologyPage/TopologyFilterBar.tsx @@ -5,8 +5,8 @@ import FormikFilterForm from "@flanksource-ui/components/Forms/FormikFilterForm" import { StateOption } from "@flanksource-ui/components/ReactSelectDropdown"; import { ComponentLabelsDropdown } from "@flanksource-ui/components/Topology/Dropdowns/ComponentLabelsDropdown"; import { ComponentTypesDropdown } from "@flanksource-ui/components/Topology/Dropdowns/ComponentTypesDropdown"; -import { allOption } from "@flanksource-ui/pages/TopologyPage"; import { useMemo } from "react"; +import { allOption } from "../TopologyPageWrapper"; import TopologyPopOver from "../TopologyPopover"; import { TopologySort } from "../TopologyPopover/topologySort"; diff --git a/src/components/Topology/TopologyPageWrapper.tsx b/src/components/Topology/TopologyPageWrapper.tsx new file mode 100644 index 000000000..b2007b92d --- /dev/null +++ b/src/components/Topology/TopologyPageWrapper.tsx @@ -0,0 +1,247 @@ +import { useComponentConfigRelationshipQuery } from "@flanksource-ui/api/query-hooks/useComponentConfigRelationshipQuery"; +import useTopologyPageQuery from "@flanksource-ui/api/query-hooks/useTopologyPageQuery"; +import { Topology } from "@flanksource-ui/api/types/topology"; +import { InfoMessage } from "@flanksource-ui/components/InfoMessage"; +import { SearchLayout } from "@flanksource-ui/components/Layout/SearchLayout"; +import { toastError } from "@flanksource-ui/components/Toast/toast"; +import { MergedTopologyConfigPage } from "@flanksource-ui/components/Topology/MergedTopologyConfigPage"; +import TopologySidebar from "@flanksource-ui/components/Topology/Sidebar/TopologySidebar"; +import { TopologyBreadcrumbs } from "@flanksource-ui/components/Topology/TopologyBreadcrumbs"; +import { TopologyCard } from "@flanksource-ui/components/Topology/TopologyCard"; +import TopologyFilterBar from "@flanksource-ui/components/Topology/TopologyPage/TopologyFilterBar"; +import { useTopologyCardWidth } from "@flanksource-ui/components/Topology/TopologyPopover/topologyPreference"; +import { Head } from "@flanksource-ui/ui/Head"; +import CardsSkeletonLoader from "@flanksource-ui/ui/SkeletonLoader/CardsSkeletonLoader"; +import { refreshButtonClickedTrigger } from "@flanksource-ui/ui/SlidingSideBar/SlidingSideBar"; +import { useAtom } from "jotai"; +import { useCallback, useEffect, useMemo, useRef } from "react"; +import { useParams, useSearchParams } from "react-router-dom"; +import LoadingBar, { LoadingBarRef } from "react-top-loading-bar"; + +import { ConfigTab } from "../Configs/ConfigDetailsTabs"; +import { + getSortedTopology, + getSortLabels +} from "./TopologyPopover/topologySort"; + +export const allOption = { + All: { + id: "All", + name: "All", + description: "All", + value: "All" + } +}; + +export const saveSortBy = (val: string, sortLabels: any[]) => { + const sortItem = sortLabels.find((s) => s.value === val); + if (sortItem?.standard) { + localStorage.setItem(`topologyCardsSortByStandard`, val); + localStorage.removeItem(`topologyCardsSortByCustom`); + } else { + localStorage.setItem(`topologyCardsSortByCustom`, val); + } +}; + +export const saveSortOrder = (val: string) => { + localStorage.setItem(`topologyCardsSortOrder`, val); +}; + +export const getSortBy = (sortLabels: any[]) => { + const val = localStorage.getItem("topologyCardsSortByCustom"); + const sortItem = sortLabels.find((s) => s.value === val); + if (!sortItem) { + localStorage.removeItem(`topologyCardsSortByCustom`); + return localStorage.getItem(`topologyCardsSortByStandard`) || "status"; + } + return ( + localStorage.getItem("topologyCardsSortByCustom") || + localStorage.getItem("topologyCardsSortByStandard") || + "status" + ); +}; + +export const getSortOrder = () => { + return localStorage.getItem(`topologyCardsSortOrder`) || "asc"; +}; + +type TopologyPageProps = { + catalogTab: ConfigTab; +}; + +export function TopologyPageWrapper({ + catalogTab = "Catalog" +}: TopologyPageProps) { + const { id } = useParams(); + + const [, setTriggerRefresh] = useAtom(refreshButtonClickedTrigger); + + const [searchParams, setSearchParams] = useSearchParams({ + sortBy: "status", + sortOrder: "desc" + }); + + const [topologyCardSize, setTopologyCardSize] = useTopologyCardWidth(); + + const refererId = searchParams.get("refererId") ?? undefined; + + const loadingBarRef = useRef(null); + + const { + data, + isLoading: isLoadingTopology, + refetch + } = useTopologyPageQuery(); + + // We probably need to fetch related configs at this point + const { data: topologyConfigs, isLoading: isLoadingConfigs } = + useComponentConfigRelationshipQuery({ + topologyId: id, + hideDeleted: true + }); + + const isLoading = isLoadingTopology || isLoadingConfigs; + + const currentTopology = useMemo(() => data?.components?.[0], [data]); + + const showMergedTopologyConfigPage = useMemo(() => { + return topologyConfigs?.data?.length === 1; + }, [topologyConfigs?.data?.length]); + + const topology = useMemo(() => { + let topologyData: Topology[] | undefined; + + if (id) { + const x = Array.isArray(data?.components) ? data?.components : []; + + if (x!.length > 1) { + console.warn("Multiple nodes for same id?"); + toastError("Response has multiple components for the id."); + } + + topologyData = x![0]?.components; + + if (!topologyData) { + console.warn("Component doesn't have any child components."); + topologyData = data?.components; + } + } else { + topologyData = data?.components ?? []; + } + + let components = topologyData?.filter( + (item) => (item.name || item.title) && item.id !== id + ); + + if (!components?.length && topologyData?.length) { + let filtered = topologyData?.find( + (x: Record) => x.id === id + ); + if (filtered) { + components = [filtered]; + } else { + components = []; + } + } + return components; + }, [data?.components, id]); + + const sortLabels = useMemo(() => { + if (!topology) { + return null; + } + return getSortLabels(topology); + }, [topology]); + + const onRefresh = useCallback(() => { + refetch(); + setTriggerRefresh((prev) => prev + 1); + }, [refetch, setTriggerRefresh]); + + useEffect(() => { + if (!sortLabels) { + return; + } + + const sortByFromURL = searchParams.get("sortBy"); + const sortOrderFromURL = searchParams.get("sortOrder"); + + const sortByFromLocalStorage = getSortBy(sortLabels) || "status"; + const sortOrderFromLocalStorage = + localStorage.getItem("topologyCardsSortOrder") || "desc"; + + if (!sortByFromURL && !sortOrderFromURL) { + searchParams.set("sortBy", sortByFromLocalStorage); + searchParams.set("sortOrder", sortOrderFromLocalStorage); + } + + // this will replace the history, so that the back button will work as expected + setSearchParams(searchParams, { replace: true }); + }, [searchParams, setSearchParams, sortLabels]); + + const sortedTopologies = useMemo( + () => + getSortedTopology(topology, getSortBy(sortLabels || []), getSortOrder()), + [sortLabels, topology] + ); + + return ( + <> + + + } + onRefresh={onRefresh} + contentClass="p-0 h-full" + loading={isLoading} + > +
+
+ + {isLoading && !topology?.length ? ( + + ) : showMergedTopologyConfigPage ? ( + + ) : ( +
+
+ {sortedTopologies.map((item) => ( + + ))} + {!topology?.length && !isLoading && ( + + )} +
+
+ )} +
+ {id && ( + + )} +
+
+ + ); +} diff --git a/src/components/Topology/TopologyPopover/topologyPreference.tsx b/src/components/Topology/TopologyPopover/topologyPreference.tsx index cbf451071..4207157e6 100644 --- a/src/components/Topology/TopologyPopover/topologyPreference.tsx +++ b/src/components/Topology/TopologyPopover/topologyPreference.tsx @@ -1,28 +1,39 @@ import clsx from "clsx"; +import { useAtom } from "jotai"; +import { atomWithStorage } from "jotai/utils"; +import { LegacyRef, useMemo } from "react"; import { FaCog } from "react-icons/fa"; - -import { CardWidth } from "../TopologyCard"; - -import { LegacyRef } from "react"; import { useSearchParams } from "react-router-dom"; import { useOnMouseActivity } from "../../../hooks/useMouseActivity"; import { Size } from "../../../types"; import { ClickableSvg } from "../../../ui/ClickableSvg/ClickableSvg"; import { Toggle } from "../../../ui/FormControls/Toggle"; +import { CardWidth } from "../TopologyCard"; -export function getCardWidth() { - let value: any = localStorage.getItem("topology_card_width"); - - if (!value?.trim()) { - return CardWidth[Size.extra_large]; +const topologyCardWidthAtom = atomWithStorage( + "topology_card_width", + CardWidth[Size.extra_large], + undefined, + { + getOnInit: true } +); - value = parseInt(value, 10); - if (isNaN(value)) { - return CardWidth[Size.extra_large]; - } else { - return `${value}px`; - } +export function useTopologyCardWidth(): [string, (width: string) => void] { + const [topologyCardSize, setTopologyCardSize] = useAtom( + topologyCardWidthAtom + ); + + const value = useMemo(() => { + const v = parseInt(topologyCardSize, 10); + if (isNaN(v)) { + return CardWidth[Size.extra_large]; + } else { + return `${v}px`; + } + }, [topologyCardSize]); + + return [value, setTopologyCardSize]; } export const TopologyPreference = ({ diff --git a/src/pages/Topology/TopologyCatalogChanges.tsx b/src/pages/Topology/TopologyCatalogChanges.tsx new file mode 100644 index 000000000..fdb85764b --- /dev/null +++ b/src/pages/Topology/TopologyCatalogChanges.tsx @@ -0,0 +1,5 @@ +import { TopologyPageWrapper } from "@flanksource-ui/components/Topology/TopologyPageWrapper"; + +export function TopologyCatalogChanges() { + return ; +} diff --git a/src/pages/Topology/TopologyCatalogChecks.tsx b/src/pages/Topology/TopologyCatalogChecks.tsx new file mode 100644 index 000000000..712cdc2c4 --- /dev/null +++ b/src/pages/Topology/TopologyCatalogChecks.tsx @@ -0,0 +1,5 @@ +import { TopologyPageWrapper } from "@flanksource-ui/components/Topology/TopologyPageWrapper"; + +export function TopologyCatalogChecks() { + return ; +} diff --git a/src/pages/Topology/TopologyCatalogInsights.tsx b/src/pages/Topology/TopologyCatalogInsights.tsx new file mode 100644 index 000000000..c4f67d788 --- /dev/null +++ b/src/pages/Topology/TopologyCatalogInsights.tsx @@ -0,0 +1,5 @@ +import { TopologyPageWrapper } from "@flanksource-ui/components/Topology/TopologyPageWrapper"; + +export function TopologyCatalogInsights() { + return ; +} diff --git a/src/pages/Topology/TopologyCatalogPlaybooks.tsx b/src/pages/Topology/TopologyCatalogPlaybooks.tsx new file mode 100644 index 000000000..2470f133a --- /dev/null +++ b/src/pages/Topology/TopologyCatalogPlaybooks.tsx @@ -0,0 +1,5 @@ +import { TopologyPageWrapper } from "@flanksource-ui/components/Topology/TopologyPageWrapper"; + +export function TopologyCatalogPlaybooks() { + return ; +} diff --git a/src/pages/Topology/TopologyCatalogRelationships.tsx b/src/pages/Topology/TopologyCatalogRelationships.tsx new file mode 100644 index 000000000..28150a961 --- /dev/null +++ b/src/pages/Topology/TopologyCatalogRelationships.tsx @@ -0,0 +1,5 @@ +import { TopologyPageWrapper } from "@flanksource-ui/components/Topology/TopologyPageWrapper"; + +export function TopologyCatalogRelationships() { + return ; +} diff --git a/src/pages/TopologyPage.tsx b/src/pages/TopologyPage.tsx index 58cc62120..e28ae26b9 100644 --- a/src/pages/TopologyPage.tsx +++ b/src/pages/TopologyPage.tsx @@ -1,263 +1,5 @@ -import { getTopology } from "@flanksource-ui/api/services/topology"; -import { Topology } from "@flanksource-ui/api/types/topology"; -import { InfoMessage } from "@flanksource-ui/components/InfoMessage"; -import { SearchLayout } from "@flanksource-ui/components/Layout/SearchLayout"; -import { toastError } from "@flanksource-ui/components/Toast/toast"; -import TopologySidebar from "@flanksource-ui/components/Topology/Sidebar/TopologySidebar"; -import { TopologyBreadcrumbs } from "@flanksource-ui/components/Topology/TopologyBreadcrumbs"; -import { TopologyCard } from "@flanksource-ui/components/Topology/TopologyCard"; -import TopologyFilterBar from "@flanksource-ui/components/Topology/TopologyPage/TopologyFilterBar"; -import { getCardWidth } from "@flanksource-ui/components/Topology/TopologyPopover/topologyPreference"; -import { Head } from "@flanksource-ui/ui/Head"; -import CardsSkeletonLoader from "@flanksource-ui/ui/SkeletonLoader/CardsSkeletonLoader"; -import { refreshButtonClickedTrigger } from "@flanksource-ui/ui/SlidingSideBar/SlidingSideBar"; -import { useQuery } from "@tanstack/react-query"; -import { useAtom } from "jotai"; -import { useCallback, useEffect, useMemo, useRef, useState } from "react"; -import { useParams, useSearchParams } from "react-router-dom"; -import LoadingBar, { LoadingBarRef } from "react-top-loading-bar"; -import { - getSortLabels, - getSortedTopology -} from "../components/Topology/TopologyPopover/topologySort"; - -export const allOption = { - All: { - id: "All", - name: "All", - description: "All", - value: "All" - } -}; - -export const saveSortBy = (val: string, sortLabels: any[]) => { - const sortItem = sortLabels.find((s) => s.value === val); - if (sortItem?.standard) { - localStorage.setItem(`topologyCardsSortByStandard`, val); - localStorage.removeItem(`topologyCardsSortByCustom`); - } else { - localStorage.setItem(`topologyCardsSortByCustom`, val); - } -}; - -export const saveSortOrder = (val: string) => { - localStorage.setItem(`topologyCardsSortOrder`, val); -}; - -export const getSortBy = (sortLabels: any[]) => { - const val = localStorage.getItem("topologyCardsSortByCustom"); - const sortItem = sortLabels.find((s) => s.value === val); - if (!sortItem) { - localStorage.removeItem(`topologyCardsSortByCustom`); - return localStorage.getItem(`topologyCardsSortByStandard`) || "status"; - } - return ( - localStorage.getItem("topologyCardsSortByCustom") || - localStorage.getItem("topologyCardsSortByStandard") || - "status" - ); -}; - -export const getSortOrder = () => { - return localStorage.getItem(`topologyCardsSortOrder`) || "asc"; -}; +import { TopologyPageWrapper } from "@flanksource-ui/components/Topology/TopologyPageWrapper"; export function TopologyPage() { - const { id } = useParams(); - - const [, setTriggerRefresh] = useAtom(refreshButtonClickedTrigger); - - const [searchParams, setSearchParams] = useSearchParams({ - sortBy: "status", - sortOrder: "desc" - }); - - const [topologyCardSize, setTopologyCardSize] = useState(() => - getCardWidth() - ); - - const selectedLabel = searchParams.get("labels") ?? "All"; - const team = searchParams.get("team") ?? "All"; - const topologyType = searchParams.get("type") ?? "All"; - const healthStatus = searchParams.get("status") ?? "All"; - const refererId = searchParams.get("refererId") ?? undefined; - const sortBy = searchParams.get("sortBy") ?? "status"; - const sortOrder = searchParams.get("sortOrder") ?? "desc"; - const agentId = searchParams.get("agent_id") ?? undefined; - const showHiddenComponents = - searchParams.get("showHiddenComponents") ?? undefined; - - const loadingBarRef = useRef(null); - - const { data, isLoading, refetch } = useQuery( - [ - "topologies", - id, - healthStatus, - team, - selectedLabel, - topologyType, - showHiddenComponents, - sortBy, - sortOrder, - agentId - ], - () => { - loadingBarRef.current?.continuousStart(); - const apiParams = { - id, - status: healthStatus, - type: topologyType, - team: team, - labels: selectedLabel, - sortBy, - sortOrder, - // only flatten, if topology type is set - ...(topologyType && - topologyType.toString().toLowerCase() !== "all" && { - flatten: true - }), - hidden: showHiddenComponents === "no" ? false : undefined, - agent_id: agentId - }; - return getTopology(apiParams); - }, - { - onSettled: () => { - loadingBarRef.current?.complete(); - } - } - ); - - const currentTopology = useMemo(() => data?.components?.[0], [data]); - - const topology = useMemo(() => { - let topologyData: Topology[] | undefined; - - if (id) { - const x = Array.isArray(data?.components) ? data?.components : []; - - if (x!.length > 1) { - console.warn("Multiple nodes for same id?"); - toastError("Response has multiple components for the id."); - } - - topologyData = x![0]?.components; - - if (!topologyData) { - console.warn("Component doesn't have any child components."); - topologyData = data?.components; - } - } else { - topologyData = data?.components ?? []; - } - - let components = topologyData?.filter( - (item) => (item.name || item.title) && item.id !== id - ); - - if (!components?.length && topologyData?.length) { - let filtered = topologyData?.find( - (x: Record) => x.id === id - ); - if (filtered) { - components = [filtered]; - } else { - components = []; - } - } - return components; - }, [data?.components, id]); - - const sortLabels = useMemo(() => { - if (!topology) { - return null; - } - return getSortLabels(topology); - }, [topology]); - - const onRefresh = useCallback(() => { - refetch(); - setTriggerRefresh((prev) => prev + 1); - }, [refetch, setTriggerRefresh]); - - useEffect(() => { - if (!sortLabels) { - return; - } - - const sortByFromURL = searchParams.get("sortBy"); - const sortOrderFromURL = searchParams.get("sortOrder"); - - const sortByFromLocalStorage = getSortBy(sortLabels) || "status"; - const sortOrderFromLocalStorage = - localStorage.getItem("topologyCardsSortOrder") || "desc"; - - if (!sortByFromURL && !sortOrderFromURL) { - searchParams.set("sortBy", sortByFromLocalStorage); - searchParams.set("sortOrder", sortOrderFromLocalStorage); - } - - // this will replace the history, so that the back button will work as expected - setSearchParams(searchParams, { replace: true }); - }, [searchParams, setSearchParams, sortLabels]); - - const sortedTopologies = useMemo( - () => - getSortedTopology(topology, getSortBy(sortLabels || []), getSortOrder()), - [sortLabels, topology] - ); - - return ( - <> - - - } - onRefresh={onRefresh} - contentClass="p-0 h-full" - loading={isLoading} - > -
-
- - {isLoading && !topology?.length ? ( - - ) : ( -
-
- {sortedTopologies.map((item) => ( - - ))} - {!topology?.length && !isLoading && ( - - )} -
-
- )} -
- {id && ( - - )} -
-
- - ); + return ; }