Skip to content

Commit 2efaa6e

Browse files
committed
feat(views): flatten view component hierarchy
Fix duplicate POST requests to /api/view/... caused by two independent fetch paths for each section: 1. ViewSection had its own useQuery call to fetch section data for rendering. 2. useAggregatedViewVariables was already fetching the same sections via useQueries for variable aggregation. Because the two query keys differed ("view-result" vs "view-variables"), React Query could not deduplicate them, resulting in 2×N requests for a view with N sections. The primary (non-aggregator) view also went through ViewSection and triggered a redundant fetch even though useViewData had already loaded that data via getViewDataById. The fix centralises all section fetching inside useAggregatedViewVariables, which now returns a sectionData Map (data/isLoading/error per section). ViewSection no longer fetches anything; it receives viewData as a prop. ViewContent renders the primary view directly from viewResult via <View />, skipping the extra fetch entirely. Also replace the unstable object-based React Query key with a deterministically sorted, URL-encoded effectiveVariablesKey string to prevent spurious cache misses on re-renders.
1 parent 7ce9d44 commit 2efaa6e

File tree

9 files changed

+174
-135
lines changed

9 files changed

+174
-135
lines changed

src/components/HomepageRedirect.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ import {
1717
UUID_REGEX
1818
} from "./dashboardViewConstants";
1919

20-
const SingleView = React.lazy(
21-
() => import("../pages/views/components/SingleView")
20+
const ViewContainer = React.lazy(
21+
() => import("../pages/views/components/ViewContainer")
2222
);
2323

2424
async function resolveViewId(value: string): Promise<string | undefined> {
@@ -59,7 +59,7 @@ export function HomepageRedirect() {
5959
if (resolvedViewId) {
6060
return (
6161
<Suspense fallback={<FullPageSkeletonLoader />}>
62-
<SingleView id={resolvedViewId} />
62+
<ViewContainer id={resolvedViewId} />
6363
</Suspense>
6464
);
6565
}

src/components/__tests__/HomepageRedirect.unit.test.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ jest.mock("../../api/services/views", () => ({
2020
getViewIdByNamespaceAndName: jest.fn()
2121
}));
2222

23-
jest.mock("../../pages/views/components/SingleView", () => ({
23+
jest.mock("../../pages/views/components/ViewContainer", () => ({
2424
__esModule: true,
2525
default: ({ id }: { id: string }) => (
2626
<div data-testid="single-view" data-view-id={id}>

src/pages/config/details/ConfigDetailsViewPage.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { ConfigDetailsTabs } from "@flanksource-ui/components/Configs/ConfigDeta
22
import { useParams } from "react-router-dom";
33
import { Loading } from "@flanksource-ui/ui/Loading";
44
import { useViewData } from "@flanksource-ui/pages/views/hooks/useViewData";
5-
import ViewWithSections from "@flanksource-ui/pages/views/components/ViewWithSections";
5+
import ViewContent from "@flanksource-ui/pages/views/components/ViewContent";
66
import { ErrorViewer } from "@flanksource-ui/components/ErrorViewer";
77

88
export function ConfigDetailsViewPage() {
@@ -17,6 +17,7 @@ export function ConfigDetailsViewPage() {
1717
error,
1818
aggregatedVariables,
1919
currentVariables,
20+
sectionData,
2021
handleForceRefresh
2122
} = useViewData({
2223
viewId: viewId!,
@@ -42,8 +43,9 @@ export function ConfigDetailsViewPage() {
4243
<ErrorViewer error={error} className="max-w-3xl" />
4344
</div>
4445
) : viewResult ? (
45-
<ViewWithSections
46+
<ViewContent
4647
viewResult={viewResult}
48+
sectionData={sectionData}
4749
aggregatedVariables={aggregatedVariables}
4850
currentVariables={currentVariables}
4951
hideVariables

src/pages/views/ViewPage.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import {
66
getViewIdByName
77
} from "../../api/services/views";
88

9-
const SingleView = React.lazy(() => import("./components/SingleView"));
9+
const ViewContainer = React.lazy(() => import("./components/ViewContainer"));
1010

1111
/**
1212
* ViewPage supports the following routes:
@@ -102,7 +102,7 @@ export function ViewPage() {
102102
</div>
103103
}
104104
>
105-
<SingleView id={viewId} />
105+
<ViewContainer id={viewId} />
106106
</Suspense>
107107
);
108108
}

src/pages/views/components/SingleView.tsx renamed to src/pages/views/components/ViewContainer.tsx

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import React, { useEffect, useState } from "react";
22
import Age from "../../../ui/Age/Age";
33
import ViewLayout from "./ViewLayout";
4-
import ViewWithSections from "./ViewWithSections";
4+
import ViewContent from "./ViewContent";
55
import { useViewData } from "../hooks/useViewData";
66
import { ErrorViewer } from "@flanksource-ui/components/ErrorViewer";
77
import {
@@ -12,18 +12,19 @@ import {
1212
DialogTitle
1313
} from "@flanksource-ui/components/ui/dialog";
1414

15-
interface SingleViewProps {
15+
interface ViewContainerProps {
1616
id: string;
1717
}
1818

19-
const SingleView: React.FC<SingleViewProps> = ({ id }) => {
19+
const ViewContainer: React.FC<ViewContainerProps> = ({ id }) => {
2020
const {
2121
viewResult,
2222
isLoading,
2323
isFetching,
2424
error,
2525
aggregatedVariables,
2626
currentVariables,
27+
sectionData,
2728
handleForceRefresh
2829
} = useViewData({ viewId: id });
2930
const [refreshErrorOpen, setRefreshErrorOpen] = useState(false);
@@ -115,9 +116,10 @@ const SingleView: React.FC<SingleViewProps> = ({ id }) => {
115116
)
116117
}
117118
>
118-
<ViewWithSections
119+
<ViewContent
119120
className="flex h-full w-full flex-1 flex-col overflow-y-auto px-6"
120121
viewResult={viewResult}
122+
sectionData={sectionData}
121123
aggregatedVariables={aggregatedVariables}
122124
currentVariables={currentVariables}
123125
/>
@@ -126,4 +128,4 @@ const SingleView: React.FC<SingleViewProps> = ({ id }) => {
126128
);
127129
};
128130

129-
export default SingleView;
131+
export default ViewContainer;

src/pages/views/components/ViewWithSections.tsx renamed to src/pages/views/components/ViewContent.tsx

Lines changed: 29 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,41 +2,37 @@ import React from "react";
22
import ViewSection from "./ViewSection";
33
import GlobalFiltersForm from "../../audit-report/components/View/GlobalFiltersForm";
44
import GlobalFilters from "../../audit-report/components/View/GlobalFilters";
5+
import View from "../../audit-report/components/View/View";
56
import { VIEW_VAR_PREFIX } from "../constants";
67
import type { ViewResult, ViewVariable } from "../../audit-report/types";
8+
import type { SectionDataEntry } from "../hooks/useAggregatedViewVariables";
79

8-
interface ViewWithSectionsProps {
10+
interface ViewContentProps {
911
className?: string;
1012
viewResult: ViewResult;
13+
sectionData?: Map<string, SectionDataEntry>;
1114
aggregatedVariables?: ViewVariable[];
1215
currentVariables?: Record<string, string>;
1316
hideVariables?: boolean;
1417
}
1518

16-
const ViewWithSections: React.FC<ViewWithSectionsProps> = React.memo(
19+
const ViewContent: React.FC<ViewContentProps> = React.memo(
1720
({
1821
viewResult,
1922
className,
23+
sectionData,
2024
aggregatedVariables,
2125
currentVariables,
2226
hideVariables
2327
}) => {
24-
const { namespace, name, panels, columns } = viewResult;
28+
const { panels, columns } = viewResult;
2529

2630
const isAggregatorView =
2731
viewResult.sections &&
2832
viewResult.sections.length > 0 &&
2933
!panels &&
3034
!columns;
3135

32-
const primaryViewSection = {
33-
title: viewResult.title || name,
34-
viewRef: {
35-
namespace: namespace || "",
36-
name: name
37-
}
38-
};
39-
4036
const showVariables =
4137
!hideVariables && aggregatedVariables && aggregatedVariables.length > 0;
4238

@@ -56,20 +52,26 @@ const ViewWithSections: React.FC<ViewWithSectionsProps> = React.memo(
5652

5753
{!isAggregatorView && (
5854
<div className="mt-2">
59-
<ViewSection
60-
key={`${namespace || "default"}:${name}`}
61-
section={primaryViewSection}
55+
<View
56+
title=""
57+
namespace={viewResult.namespace}
58+
name={viewResult.name}
59+
columns={viewResult.columns}
60+
columnOptions={viewResult.columnOptions}
61+
panels={viewResult.panels}
62+
table={viewResult.table}
63+
variables={viewResult.variables}
64+
card={viewResult.card}
65+
requestFingerprint={viewResult.requestFingerprint}
66+
currentVariables={currentVariables}
6267
hideVariables
63-
variables={currentVariables}
64-
sectionKeySuffix={`primary-${namespace || "default"}:${name}`}
6568
/>
6669
</div>
6770
)}
6871

6972
{viewResult?.sections && viewResult.sections.length > 0 && (
7073
<>
7174
{viewResult.sections.map((section, index) => {
72-
// Generate a unique key based on section type
7375
const baseKey = section.viewRef
7476
? `${section.viewRef.namespace || "default"}:${section.viewRef.name}`
7577
: section.uiRef?.changes
@@ -78,14 +80,21 @@ const ViewWithSections: React.FC<ViewWithSectionsProps> = React.memo(
7880
? `configs:${section.title}`
7981
: `section:${section.title}`;
8082
const sectionKey = `${baseKey}:${index}`;
83+
const viewRefKey = section.viewRef
84+
? `${section.viewRef.namespace || ""}:${section.viewRef.name}`
85+
: undefined;
86+
const sectionEntry = viewRefKey
87+
? sectionData?.get(viewRefKey)
88+
: undefined;
8189

8290
return (
8391
<div key={sectionKey} className="mt-4">
8492
<ViewSection
8593
section={section}
86-
hideVariables
94+
viewData={sectionEntry?.data}
95+
isLoading={sectionEntry?.isLoading}
96+
error={sectionEntry?.error}
8797
variables={currentVariables}
88-
sectionKeySuffix={sectionKey}
8998
/>
9099
</div>
91100
);
@@ -97,4 +106,4 @@ const ViewWithSections: React.FC<ViewWithSectionsProps> = React.memo(
97106
}
98107
);
99108

100-
export default ViewWithSections;
109+
export default ViewContent;

0 commit comments

Comments
 (0)