Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/pages/audit-report/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -417,7 +417,7 @@ export interface ColumnFilterOptions {
}

export interface ViewRef {
namespace: string;
namespace?: string;
name: string;
}

Expand Down
90 changes: 18 additions & 72 deletions src/pages/config/details/ConfigDetailsViewPage.tsx
Original file line number Diff line number Diff line change
@@ -1,106 +1,52 @@
import { ConfigDetailsTabs } from "@flanksource-ui/components/Configs/ConfigDetailsTabs";
import {
getViewDataById,
getViewDisplayPluginVariables
} from "@flanksource-ui/api/services/views";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { useParams } from "react-router-dom";
import { Loading } from "@flanksource-ui/ui/Loading";
import View from "@flanksource-ui/pages/audit-report/components/View/View";
import { useRef } from "react";
import { toastError } from "@flanksource-ui/components/Toast/toast";
import { useViewData } from "@flanksource-ui/pages/views/hooks/useViewData";
import ViewWithSections from "@flanksource-ui/pages/views/components/ViewWithSections";
import { ErrorViewer } from "@flanksource-ui/components/ErrorViewer";

// Displays the view as a tab in the config details page
export function ConfigDetailsViewPage() {
const queryClient = useQueryClient();
const forceRefreshRef = useRef(false);

const { id: configId, viewId } = useParams<{
id: string;
viewId: string;
}>();

// Fetch display plugin variables from the API
const { data: variables } = useQuery({
queryKey: ["viewDisplayPluginVariables", viewId, configId],
queryFn: () => getViewDisplayPluginVariables(viewId!, configId!),
enabled: !!viewId && !!configId
});

const {
data: viewResult,
viewResult,
isLoading,
error,
refetch
} = useQuery({
queryKey: ["viewDataById", viewId, configId, variables],
queryFn: () => {
if (!viewId) {
throw new Error("View ID is required");
}
const headers = forceRefreshRef.current
? { "cache-control": "max-age=1" }
: undefined;

return getViewDataById(viewId, variables, headers);
},
enabled: !!viewId && !!configId && !!variables
aggregatedVariables,
currentVariables,
handleForceRefresh
} = useViewData({
viewId: viewId!,
configId
});

const handleRefresh = async () => {
forceRefreshRef.current = true;
const result = await refetch();
forceRefreshRef.current = false;

if (result.isError) {
toastError(
result.error instanceof Error
? result.error.message
: "Failed to refresh view"
);
} else if (result.data?.namespace && result.data?.name) {
await queryClient.invalidateQueries({
queryKey: ["view-table", result.data.namespace, result.data.name]
});
}
};

const displayName = viewResult?.title || viewResult?.name || "";

return (
<ConfigDetailsTabs
pageTitlePrefix={`Config View - ${displayName}`}
isLoading={isLoading}
activeTabName={displayName}
refetch={handleRefresh}
refetch={handleForceRefresh}
>
<div className="flex h-full flex-1 flex-col overflow-auto p-4">
<div className="">
{isLoading ? (
<div className="flex flex-1 flex-col items-center justify-center">
<Loading />
</div>
) : error ? (
<div className="flex flex-1 flex-col items-center justify-center">
<div className="text-center">
<div className="mb-4 text-xl text-red-500">Error</div>
<p className="text-gray-600">
{error instanceof Error
? error.message
: "Failed to load view data"}
</p>
</div>
<ErrorViewer error={error} className="max-w-3xl" />
</div>
) : viewResult ? (
<View
title=""
namespace={viewResult.namespace}
name={viewResult.name}
panels={viewResult.panels}
columns={viewResult.columns}
card={viewResult.card}
table={viewResult.table}
requestFingerprint={viewResult.requestFingerprint}
columnOptions={viewResult.columnOptions}
<ViewWithSections
viewResult={viewResult}
aggregatedVariables={aggregatedVariables}
currentVariables={currentVariables}
hideVariables
/>
) : (
<div className="flex flex-1 flex-col items-center justify-center">
Expand Down
169 changes: 15 additions & 154 deletions src/pages/views/components/SingleView.tsx
Original file line number Diff line number Diff line change
@@ -1,112 +1,24 @@
import React, { useMemo, useRef } from "react";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { getViewDataById } from "../../../api/services/views";
import ViewSection from "./ViewSection";
import React from "react";
import Age from "../../../ui/Age/Age";
import { toastError } from "../../../components/Toast/toast";
import ViewLayout from "./ViewLayout";
import { useAggregatedViewVariables } from "../hooks/useAggregatedViewVariables";
import GlobalFiltersForm from "../../audit-report/components/View/GlobalFiltersForm";
import GlobalFilters from "../../audit-report/components/View/GlobalFilters";
import { VIEW_VAR_PREFIX } from "../constants";
import type { ViewRef } from "../../audit-report/types";
import ViewWithSections from "./ViewWithSections";
import { useViewData } from "../hooks/useViewData";
import { ErrorViewer } from "@flanksource-ui/components/ErrorViewer";

interface SingleViewProps {
id: string;
}

const SingleView: React.FC<SingleViewProps> = ({ id }) => {
const queryClient = useQueryClient();
const forceRefreshRef = useRef(false);

// Fetch view metadata only. Each section (including the main view) will fetch
// its own data with its own variables using its unique prefix.
const {
data: viewResult,
viewResult,
isLoading,
isFetching,
error,
refetch
} = useQuery({
queryKey: ["view-result", id],
queryFn: () => {
const headers = forceRefreshRef.current
? { "cache-control": "max-age=1" }
: undefined;
return getViewDataById(id, undefined, headers);
},
enabled: !!id,
staleTime: 5 * 60 * 1000,
keepPreviousData: true
});

// Collect all section refs (main view + additional sections)
// Must be called before early returns to satisfy React hooks rules
const allSectionRefs = useMemo<ViewRef[]>(() => {
if (!viewResult?.namespace || !viewResult?.name) {
return [];
}
const refs = [{ namespace: viewResult.namespace, name: viewResult.name }];
if (viewResult?.sections) {
viewResult.sections.forEach((section) => {
refs.push({
namespace: section.viewRef.namespace,
name: section.viewRef.name
});
});
}
return refs;
}, [viewResult?.namespace, viewResult?.name, viewResult?.sections]);

// Fetch and aggregate variables from all sections
const { variables: aggregatedVariables, currentVariables } =
useAggregatedViewVariables(allSectionRefs);

const handleForceRefresh = async () => {
forceRefreshRef.current = true;
const result = await refetch();
forceRefreshRef.current = false;

if (result.isError) {
const err = result.error as any;
toastError(
err?.message ||
err?.error ||
err?.detail ||
err?.msg ||
"Failed to refresh view"
);
return;
}

// Invalidate all section data (view results and tables) so they refetch
const sectionsToRefresh =
allSectionRefs.length > 0 &&
allSectionRefs[0].namespace &&
allSectionRefs[0].name
? allSectionRefs
: result.data?.namespace && result.data.name
? [{ namespace: result.data.namespace, name: result.data.name }]
: [];

// Also clear the main view query by id so the metadata refetches
await queryClient.invalidateQueries({ queryKey: ["view-result", id] });

await Promise.all(
sectionsToRefresh.flatMap((section) => [
queryClient.invalidateQueries({
queryKey: ["view-result", section.namespace, section.name]
}),
queryClient.invalidateQueries({
queryKey: ["view-table", section.namespace, section.name]
}),
queryClient.invalidateQueries({
queryKey: ["view-variables", section.namespace, section.name]
})
])
);
};
aggregatedVariables,
currentVariables,
handleForceRefresh
} = useViewData({ viewId: id });

if (isLoading && !viewResult) {
return (
Expand Down Expand Up @@ -153,24 +65,7 @@ const SingleView: React.FC<SingleViewProps> = ({ id }) => {
);
}

const { icon, title, namespace, name } = viewResult;

// Render the main view through ViewSection to reuse its spacing/scroll styling;
// rendering the raw View here caused padding/overflow glitches alongside sections.
const primaryViewSection = {
title: title || name,
viewRef: {
namespace: namespace || "",
name: name
}
};

// Check if this view only aggregates other views (has sections but no content of its own)
const isAggregatorView =
viewResult.sections &&
viewResult.sections.length > 0 &&
!viewResult.panels &&
!viewResult.columns;
const { icon, title, name } = viewResult;

return (
<ViewLayout
Expand All @@ -187,46 +82,12 @@ const SingleView: React.FC<SingleViewProps> = ({ id }) => {
)
}
>
<div className="flex h-full w-full flex-1 flex-col overflow-y-auto px-6">
{/* Render aggregated variables once at the top */}
{aggregatedVariables && aggregatedVariables.length > 0 && (
<GlobalFiltersForm
variables={aggregatedVariables}
globalVarPrefix={VIEW_VAR_PREFIX}
currentVariables={currentVariables}
>
<GlobalFilters variables={aggregatedVariables} />
</GlobalFiltersForm>
)}

{aggregatedVariables && aggregatedVariables.length > 0 && (
<hr className="my-4 border-gray-200" />
)}

{/* Only show the primary ViewSection if this view has its own content */}
{!isAggregatorView && (
<div className="mt-2">
<ViewSection
key={`${namespace || "default"}:${name}`}
section={primaryViewSection}
hideVariables
/>
</div>
)}

{viewResult?.sections && viewResult.sections.length > 0 && (
<>
{viewResult.sections.map((section) => (
<div
key={`${section.viewRef.namespace}:${section.viewRef.name}`}
className="mt-4"
>
<ViewSection section={section} hideVariables />
</div>
))}
</>
)}
</div>
<ViewWithSections
className="flex h-full w-full flex-1 flex-col overflow-y-auto px-6"
viewResult={viewResult}
aggregatedVariables={aggregatedVariables}
currentVariables={currentVariables}
/>
</ViewLayout>
);
};
Expand Down
2 changes: 1 addition & 1 deletion src/pages/views/components/ViewSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ const ViewSection: React.FC<ViewSectionProps> = ({
} = useQuery({
queryKey: ["view-result", namespace, name, currentViewVariables],
queryFn: () =>
getViewDataByNamespace(namespace, name, currentViewVariables),
getViewDataByNamespace(namespace || "", name, currentViewVariables),
enabled: !!namespace && !!name,
staleTime: 5 * 60 * 1000
});
Expand Down
Loading
Loading