diff --git a/src/App.tsx b/src/App.tsx index a16f8a584..b1f6a4f8a 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -85,6 +85,12 @@ const ConfigDetailsPage = dynamic( ) ); +const ConfigDetailsIndexRedirect = dynamic( + import( + "@flanksource-ui/pages/config/details/ConfigDetailsIndexRedirect" + ).then((mod) => mod.ConfigDetailsIndexRedirect) +); + const ConfigDetailsChangesPage = dynamic( import("@flanksource-ui/pages/config/details/ConfigDetailsChangesPage").then( (mod) => mod.ConfigDetailsChangesPage @@ -897,6 +903,19 @@ export function IncidentManagerRoutes({ sidebar }: { sidebar: ReactNode }) { + {withAuthorizationAccessCheck( + , + tables.database, + "read", + true + )} + + } + /> + {withAuthorizationAccessCheck( diff --git a/src/api/services/views.ts b/src/api/services/views.ts index 7821443c2..f59b8db30 100644 --- a/src/api/services/views.ts +++ b/src/api/services/views.ts @@ -34,6 +34,7 @@ export type ViewListItem = { namespace?: string; title?: string; icon?: string; + ordinal?: number; }; export const getAllViews = ( diff --git a/src/components/Configs/ConfigDetailsTabs.tsx b/src/components/Configs/ConfigDetailsTabs.tsx index 393a5305c..1f951cb37 100644 --- a/src/components/Configs/ConfigDetailsTabs.tsx +++ b/src/components/Configs/ConfigDetailsTabs.tsx @@ -18,7 +18,7 @@ type ConfigDetailsTabsProps = { isLoading?: boolean; pageTitlePrefix: string; activeTabName: - | "Catalog" + | "Spec" | "Changes" | "Insights" | "Relationships" @@ -33,7 +33,7 @@ export function ConfigDetailsTabs({ children, isLoading = false, pageTitlePrefix, - activeTabName = "Catalog", + activeTabName = "Spec", className = "p-2" }: ConfigDetailsTabsProps) { const { id } = useParams(); diff --git a/src/components/Configs/ConfigTabsLinks.tsx b/src/components/Configs/ConfigTabsLinks.tsx index 1c3061ea8..25ab7d134 100644 --- a/src/components/Configs/ConfigTabsLinks.tsx +++ b/src/components/Configs/ConfigTabsLinks.tsx @@ -4,8 +4,19 @@ import { ConfigItem } from "../../api/types/configs"; import { getViewsByConfigId } from "../../api/services/views"; import { useQuery } from "@tanstack/react-query"; import { Icon } from "@flanksource-ui/ui/Icons/Icon"; +import { ReactNode } from "react"; -export function useConfigDetailsTabs(countSummary?: ConfigItem["summary"]) { +type ConfigDetailsTab = { + label: ReactNode; + key: string; + path: string; + icon?: ReactNode; + search?: string; +}; + +export function useConfigDetailsTabs( + countSummary?: ConfigItem["summary"] +): ConfigDetailsTab[] { const { id } = useParams<{ id: string }>(); const { data: views = [] } = useQuery({ @@ -14,8 +25,8 @@ export function useConfigDetailsTabs(countSummary?: ConfigItem["summary"]) { enabled: !!id }); - const staticTabs = [ - { label: "Config", key: "Catalog", path: `/catalog/${id}` }, + const staticTabs: ConfigDetailsTab[] = [ + { label: "Spec", key: "Spec", path: `/catalog/${id}/spec` }, { label: ( <> @@ -68,12 +79,35 @@ export function useConfigDetailsTabs(countSummary?: ConfigItem["summary"]) { } ]; - const viewTabs = views.map((view) => ({ + const hasExplicitOrdering = views.some((view) => view.ordinal != null); + + const orderedViews = hasExplicitOrdering + ? [...views].sort((a, b) => { + const aOrdinal = a.ordinal ?? Number.MAX_SAFE_INTEGER; + const bOrdinal = b.ordinal ?? Number.MAX_SAFE_INTEGER; + + if (aOrdinal !== bOrdinal) { + return aOrdinal - bOrdinal; + } + + const aLabel = a.title || a.name; + const bLabel = b.title || b.name; + + return aLabel.localeCompare(bLabel); + }) + : views; + + const viewTabs: ConfigDetailsTab[] = orderedViews.map((view) => ({ label: view.title || view.name, key: view.id, path: `/catalog/${id}/view/${view.id}`, - icon: + icon: })); - return [...staticTabs, ...viewTabs]; + if (viewTabs.length === 0) { + return staticTabs; + } + + // Views configured for a config should appear ahead of the built-in tabs. + return [...viewTabs, ...staticTabs]; } diff --git a/src/pages/config/details/ConfigDetailsIndexRedirect.tsx b/src/pages/config/details/ConfigDetailsIndexRedirect.tsx new file mode 100644 index 000000000..ae70845a7 --- /dev/null +++ b/src/pages/config/details/ConfigDetailsIndexRedirect.tsx @@ -0,0 +1,59 @@ +import { useGetConfigByIdQuery } from "@flanksource-ui/api/query-hooks"; +import { useConfigDetailsTabs } from "@flanksource-ui/components/Configs/ConfigTabsLinks"; +import { Loading } from "@flanksource-ui/ui/Loading"; +import { useEffect } from "react"; +import { useLocation, useNavigate, useParams } from "react-router-dom"; + +export function ConfigDetailsIndexRedirect() { + const { id } = useParams<{ id: string }>(); + const navigate = useNavigate(); + const location = useLocation(); + + const idParam = id ?? ""; + + const { + data: configDetails, + isLoading: isConfigLoading, + isError: isConfigError + } = useGetConfigByIdQuery(idParam); + + const tabs = useConfigDetailsTabs(configDetails?.summary); + + useEffect(() => { + if (!id || tabs.length === 0) { + return; + } + + const firstTab = tabs[0]; + + if (!firstTab || location.pathname === firstTab.path) { + return; + } + + navigate( + { + pathname: firstTab.path, + search: firstTab.search ?? location.search + }, + { replace: true } + ); + }, [id, tabs, navigate, location.pathname, location.search]); + + if (isConfigLoading) { + return ( +
+ +
+ ); + } + + if (isConfigError) { + return ( +
+ Failed to load config +
+ ); + } + + return null; +} diff --git a/src/pages/config/details/ConfigDetailsPage.tsx b/src/pages/config/details/ConfigDetailsPage.tsx index d08851ca9..7c16401e9 100644 --- a/src/pages/config/details/ConfigDetailsPage.tsx +++ b/src/pages/config/details/ConfigDetailsPage.tsx @@ -76,7 +76,7 @@ export function ConfigDetailsPage() { pageTitlePrefix={"Catalog"} isLoading={isLoading} refetch={refetch} - activeTabName="Catalog" + activeTabName="Spec" className="" >