diff --git a/src/App.tsx b/src/App.tsx index 86b6b4b23..05f3411ad 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -34,6 +34,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"; @@ -262,15 +263,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" + )} + /> + + void; children: ReactNode; isLoading?: boolean; pageTitlePrefix: string; - activeTabName: - | "Catalog" - | "Changes" - | "Insights" - | "Relationships" - | "Playbooks" - | "Checks"; + activeTabName: ConfigTab; className?: string; }; diff --git a/src/components/Configs/ConfigTabsLinks.tsx b/src/components/Configs/ConfigTabsLinks.tsx index b3b9e75ad..84d965657 100644 --- a/src/components/Configs/ConfigTabsLinks.tsx +++ b/src/components/Configs/ConfigTabsLinks.tsx @@ -3,12 +3,13 @@ import { useParams } from "react-router-dom"; import { ConfigItemDetails } from "../../api/types/configs"; export function useConfigDetailsTabs( - countSummary?: ConfigItemDetails["summary"] + 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: ( <> @@ -17,7 +18,7 @@ export function useConfigDetailsTabs( ), key: "Changes", - path: `/catalog/${id}/changes` + path: `${basePath}/${id}/changes` }, { label: ( @@ -27,7 +28,7 @@ export function useConfigDetailsTabs( ), key: "Insights", - path: `/catalog/${id}/insights` + path: `${basePath}/${id}/insights` }, { label: ( @@ -37,7 +38,7 @@ export function useConfigDetailsTabs( ), key: "Relationships", - path: `/catalog/${id}/relationships` + path: `${basePath}/${id}/relationships` }, { label: ( @@ -47,7 +48,7 @@ export function useConfigDetailsTabs( ), key: "Playbooks", - path: `/catalog/${id}/playbooks` + path: `${basePath}/${id}/playbooks` }, { label: ( @@ -57,7 +58,7 @@ export function useConfigDetailsTabs( ), 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/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/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 2bd6048e1..e28ae26b9 100644 --- a/src/pages/TopologyPage.tsx +++ b/src/pages/TopologyPage.tsx @@ -1,215 +1,5 @@ -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 { 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 { useTopologyCardWidth } from "@flanksource-ui/components/Topology/TopologyPopover/topologyPreference"; -import { Head } from "@flanksource-ui/ui/Head"; -import { SearchLayout } from "@flanksource-ui/ui/Layout/SearchLayout"; -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 { - 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] = useTopologyCardWidth(); - - const refererId = searchParams.get("refererId") ?? undefined; - - const loadingBarRef = useRef(null); - - const { data, isLoading, refetch } = useTopologyPageQuery(); - - // We probably need to fetch related configs at this point - - 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 ; }