From 45e84408d0b1bb0a87a88f8ccbec1804578622e5 Mon Sep 17 00:00:00 2001 From: Maina Wycliffe Date: Fri, 23 Aug 2024 12:14:42 +0300 Subject: [PATCH] feat: when a catalog has one topology, show only topology card Fixes #2174 fix: show loading animation, until card is shown or not chore: wip fix: fix incorrect import fix: change how merged pages work --- src/App.tsx | 2 +- src/api/query-hooks/useTopologyByIDQuery.tsx | 64 +++++ src/api/services/configs.ts | 3 +- src/api/types/configs.ts | 19 +- src/api/types/topology.ts | 3 + src/components/Configs/ConfigComponents.tsx | 35 +++ src/components/Configs/ConfigDetailsTabs.tsx | 55 ++-- src/components/Configs/ConfigTabsLinks.tsx | 19 +- .../Configs/Sidebar/ConfigDetails.tsx | 17 +- .../Configs/Sidebar/ConfigSidebar.tsx | 11 +- .../Dropdowns/ComponentLabelsDropdown.tsx | 2 +- .../Topology/MergedTopologyConfigPage.tsx | 70 +++++ .../Topology/Sidebar/TopologyDetails.tsx | 2 +- .../Sidebar/Utils/formatProperties.tsx | 3 +- .../Topology/TopologyCard/index.tsx | 23 +- .../TopologyPage/TopologyFilterBar.tsx | 2 +- .../Topology/TopologyPageWrapper.tsx | 247 ++++++++++++++++++ .../Topology/TopologyPopover/topologySort.tsx | 2 +- src/pages/TopologyPage.tsx | 212 +-------------- 19 files changed, 533 insertions(+), 258 deletions(-) create mode 100644 src/api/query-hooks/useTopologyByIDQuery.tsx create mode 100644 src/components/Configs/ConfigComponents.tsx create mode 100644 src/components/Topology/MergedTopologyConfigPage.tsx create mode 100644 src/components/Topology/TopologyPageWrapper.tsx diff --git a/src/App.tsx b/src/App.tsx index 86b6b4b23..cf100a75f 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -265,7 +265,7 @@ export function IncidentManagerRoutes({ sidebar }: { sidebar: ReactNode }) { , + , tables.database, "read", true diff --git a/src/api/query-hooks/useTopologyByIDQuery.tsx b/src/api/query-hooks/useTopologyByIDQuery.tsx new file mode 100644 index 000000000..683850d78 --- /dev/null +++ b/src/api/query-hooks/useTopologyByIDQuery.tsx @@ -0,0 +1,64 @@ +import { useQuery } from "@tanstack/react-query"; +import { useRef } from "react"; +import { useSearchParams } from "react-router-dom"; +import { LoadingBarRef } from "react-top-loading-bar"; +import { getTopology } from "../services/topology"; + +export default function useTopologyByIDQuery(id: string) { + const [searchParams] = useSearchParams({ + sortBy: "status", + sortOrder: "desc" + }); + + 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 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); + + 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 bf239d9dd..b3291b443 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"; @@ -144,7 +145,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 08058a8e6..cb4b67cf3 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/api/types/topology.ts b/src/api/types/topology.ts index 867ba1f4d..a5a743337 100644 --- a/src/api/types/topology.ts +++ b/src/api/types/topology.ts @@ -1,5 +1,6 @@ import { Agent, Namespaced, Timestamped } from "../traits"; import { CostsData, Severity, ValueType } from "./common"; +import { ConfigItem } from "./configs"; import { HealthCheckSummary } from "./health"; import { IncidentType } from "./incident"; import { User } from "./users"; @@ -68,6 +69,8 @@ export interface Topology extends Component, CostsData, Agent { children?: string[]; is_leaf?: boolean; description?: string; + config_id?: string; + configs?: Pick[]; } export type ComponentTeamItem = { diff --git a/src/components/Configs/ConfigComponents.tsx b/src/components/Configs/ConfigComponents.tsx new file mode 100644 index 000000000..2ee202d00 --- /dev/null +++ b/src/components/Configs/ConfigComponents.tsx @@ -0,0 +1,35 @@ +import useTopologyByIDQuery from "@flanksource-ui/api/query-hooks/useTopologyByIDQuery"; +import IncidentDetailsPageSkeletonLoader from "@flanksource-ui/ui/SkeletonLoader/IncidentDetailsPageSkeletonLoader"; +import { TopologyCard } from "../Topology/TopologyCard"; +import { useTopologyCardWidth } from "../Topology/TopologyPopover/topologyPreference"; + +type ConfigComponentsProps = { + topologyId: string; +}; + +export default function ConfigComponents({ + topologyId +}: ConfigComponentsProps) { + const { data, isLoading } = useTopologyByIDQuery(topologyId); + + const [topologyCardSize] = useTopologyCardWidth(); + + return ( +
+
+ {isLoading && data ? ( + + ) : ( + data?.components?.[0].components?.map((component) => ( + + )) + )} +
+
+ ); +} diff --git a/src/components/Configs/ConfigDetailsTabs.tsx b/src/components/Configs/ConfigDetailsTabs.tsx index d1e3c98ab..d87fbe71c 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 ConfigComponents from "./ConfigComponents"; 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,30 @@ 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/Configs/Sidebar/ConfigDetails.tsx b/src/components/Configs/Sidebar/ConfigDetails.tsx index a0c83adcc..0d854981a 100644 --- a/src/components/Configs/Sidebar/ConfigDetails.tsx +++ b/src/components/Configs/Sidebar/ConfigDetails.tsx @@ -1,5 +1,6 @@ import { useGetConfigByIdQuery } from "@flanksource-ui/api/query-hooks"; import { isCostsEmpty } from "@flanksource-ui/api/types/configs"; +import { Topology } from "@flanksource-ui/api/types/topology"; import { formatProperties } from "@flanksource-ui/components/Topology/Sidebar/Utils/formatProperties"; import { Age } from "@flanksource-ui/ui/Age"; import TextSkeletonLoader from "@flanksource-ui/ui/SkeletonLoader/TextSkeletonLoader"; @@ -17,9 +18,10 @@ import { formatConfigLabels } from "./Utils/formatConfigLabels"; type Props = { configId: string; + topologyProperties?: Topology["properties"]; }; -export function ConfigDetails({ configId }: Props) { +export function ConfigDetails({ configId, topologyProperties }: Props) { const { data: configDetails, isLoading, @@ -67,6 +69,15 @@ export function ConfigDetails({ configId }: Props) { [configDetails] ); + const formattedTopologyProperties = useMemo(() => { + if (topologyProperties) { + return formatProperties({ + properties: topologyProperties + }); + } + return undefined; + }, [topologyProperties]); + const isLastScrappedMoreThan1Hour = useMemo(() => { if (!configDetails?.last_scraped_time) { return false; @@ -179,6 +190,10 @@ export function ConfigDetails({ configId }: Props) { ]} /> + {formattedTopologyProperties && ( + + )} + - + ); diff --git a/src/components/Topology/Dropdowns/ComponentLabelsDropdown.tsx b/src/components/Topology/Dropdowns/ComponentLabelsDropdown.tsx index 8c0a4b105..6ac637c5b 100644 --- a/src/components/Topology/Dropdowns/ComponentLabelsDropdown.tsx +++ b/src/components/Topology/Dropdowns/ComponentLabelsDropdown.tsx @@ -2,7 +2,7 @@ import FormikFilterSelectDropdown from "@flanksource-ui/components/Forms/Formik/ import clsx from "clsx"; import { useEffect, useState } from "react"; import { useComponentLabelsQuery } from "../../../api/query-hooks"; -import { allOption } from "../../../pages/TopologyPage"; +import { allOption } from "../TopologyPageWrapper"; type ComponentLabelsDropdownProps = { name: string; 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/Sidebar/TopologyDetails.tsx b/src/components/Topology/Sidebar/TopologyDetails.tsx index bade9a0e1..817d081a8 100644 --- a/src/components/Topology/Sidebar/TopologyDetails.tsx +++ b/src/components/Topology/Sidebar/TopologyDetails.tsx @@ -17,7 +17,7 @@ import { TopologyLink } from "../TopologyLink"; import { formatProperties } from "./Utils/formatProperties"; type Props = { - topology?: Topology; + topology?: Omit; refererId?: string; isCollapsed?: boolean; onCollapsedStateChange?: (isClosed: boolean) => void; diff --git a/src/components/Topology/Sidebar/Utils/formatProperties.tsx b/src/components/Topology/Sidebar/Utils/formatProperties.tsx index b77d7908d..144224c25 100644 --- a/src/components/Topology/Sidebar/Utils/formatProperties.tsx +++ b/src/components/Topology/Sidebar/Utils/formatProperties.tsx @@ -36,8 +36,7 @@ export function formatProperties(topology?: Pick) { const items = new Map(); // remove headline properties from the list of properties - const topologyProperties = - topology?.properties?.filter((property) => !property.headline) ?? []; + const topologyProperties = topology?.properties ?? []; const rowKeysMaps = new Map([ ["region", "region/zone"], diff --git a/src/components/Topology/TopologyCard/index.tsx b/src/components/Topology/TopologyCard/index.tsx index b2ad1af2c..73c55010c 100644 --- a/src/components/Topology/TopologyCard/index.tsx +++ b/src/components/Topology/TopologyCard/index.tsx @@ -37,7 +37,22 @@ 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" + | "health" + | "config_id" + >; selectionMode?: boolean; hideMenu?: boolean; // where to open new links @@ -108,6 +123,12 @@ export function TopologyCard({ return ""; } + // we want to link to the config page if it exists, where we will show a + // merged view of the topology and the config + if (topologyItem?.config_id) { + return `/catalog/${topologyItem.config_id}`; + } + const params = Object.fromEntries(searchParams.entries()); delete params.refererId; delete params.status; 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..a1298c6c2 --- /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 { 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 { SearchLayout } from "@flanksource-ui/ui/Layout/SearchLayout"; +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/topologySort.tsx b/src/components/Topology/TopologyPopover/topologySort.tsx index 12a356a5c..34321ff13 100644 --- a/src/components/Topology/TopologyPopover/topologySort.tsx +++ b/src/components/Topology/TopologyPopover/topologySort.tsx @@ -5,8 +5,8 @@ import { BsSortDown, BsSortUp } from "react-icons/bs"; import { ValueType } from "../../../api/types/common"; import { Topology } from "../../../api/types/topology"; import { useOnMouseActivity } from "../../../hooks/useMouseActivity"; -import { saveSortBy, saveSortOrder } from "../../../pages/TopologyPage"; import { isDate } from "../../../utils/date"; +import { saveSortBy, saveSortOrder } from "../TopologyPageWrapper"; const STATUS = { info: 4, diff --git a/src/pages/TopologyPage.tsx b/src/pages/TopologyPage.tsx index bbba81a7d..e28ae26b9 100644 --- a/src/pages/TopologyPage.tsx +++ b/src/pages/TopologyPage.tsx @@ -1,213 +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(); - - 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 ; }