Skip to content

Commit 51d54cd

Browse files
authored
fix(views): duplicate POST requests to /api/view/...
* 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. * fix: aggregating view detector improvement
1 parent 7ce9d44 commit 51d54cd

File tree

10 files changed

+194
-139
lines changed

10 files changed

+194
-139
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/audit-report/types/index.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -486,8 +486,23 @@ export interface ViewResult {
486486
panels?: PanelResult[];
487487
columnOptions?: Record<string, ColumnFilterOptions>;
488488
variables?: ViewVariable[];
489+
490+
/**
491+
* Card display configuration for tabular data.
492+
*
493+
* This controls presentation (e.g. card layout/default mode) and does not
494+
* contain primary result data by itself.
495+
*/
489496
card?: DisplayCard;
497+
498+
/**
499+
* Table display configuration (e.g. default sort and page size).
500+
*
501+
* This controls presentation/query defaults and does not contain primary
502+
* result data by itself.
503+
*/
490504
table?: DisplayTable;
505+
491506
requestFingerprint: string;
492507
sections?: ViewSection[];
493508
}

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: 34 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -2,40 +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

26-
const isAggregatorView =
27-
viewResult.sections &&
28-
viewResult.sections.length > 0 &&
29-
!panels &&
30-
!columns;
30+
const hasPrimaryContent =
31+
(Array.isArray(panels) && panels.length > 0) ||
32+
(Array.isArray(columns) && columns.length > 0);
3133

32-
const primaryViewSection = {
33-
title: viewResult.title || name,
34-
viewRef: {
35-
namespace: namespace || "",
36-
name: name
37-
}
38-
};
34+
const isAggregatorView =
35+
Boolean(viewResult.sections?.length) && !hasPrimaryContent;
3936

4037
const showVariables =
4138
!hideVariables && aggregatedVariables && aggregatedVariables.length > 0;
@@ -56,20 +53,26 @@ const ViewWithSections: React.FC<ViewWithSectionsProps> = React.memo(
5653

5754
{!isAggregatorView && (
5855
<div className="mt-2">
59-
<ViewSection
60-
key={`${namespace || "default"}:${name}`}
61-
section={primaryViewSection}
56+
<View
57+
title=""
58+
namespace={viewResult.namespace}
59+
name={viewResult.name}
60+
columns={viewResult.columns}
61+
columnOptions={viewResult.columnOptions}
62+
panels={viewResult.panels}
63+
table={viewResult.table}
64+
variables={viewResult.variables}
65+
card={viewResult.card}
66+
requestFingerprint={viewResult.requestFingerprint}
67+
currentVariables={currentVariables}
6268
hideVariables
63-
variables={currentVariables}
64-
sectionKeySuffix={`primary-${namespace || "default"}:${name}`}
6569
/>
6670
</div>
6771
)}
6872

6973
{viewResult?.sections && viewResult.sections.length > 0 && (
7074
<>
7175
{viewResult.sections.map((section, index) => {
72-
// Generate a unique key based on section type
7376
const baseKey = section.viewRef
7477
? `${section.viewRef.namespace || "default"}:${section.viewRef.name}`
7578
: section.uiRef?.changes
@@ -78,14 +81,21 @@ const ViewWithSections: React.FC<ViewWithSectionsProps> = React.memo(
7881
? `configs:${section.title}`
7982
: `section:${section.title}`;
8083
const sectionKey = `${baseKey}:${index}`;
84+
const viewRefKey = section.viewRef
85+
? `${section.viewRef.namespace || ""}:${section.viewRef.name}`
86+
: undefined;
87+
const sectionEntry = viewRefKey
88+
? sectionData?.get(viewRefKey)
89+
: undefined;
8190

8291
return (
8392
<div key={sectionKey} className="mt-4">
8493
<ViewSection
8594
section={section}
86-
hideVariables
95+
viewData={sectionEntry?.data}
96+
isLoading={sectionEntry?.isLoading}
97+
error={sectionEntry?.error}
8798
variables={currentVariables}
88-
sectionKeySuffix={sectionKey}
8999
/>
90100
</div>
91101
);
@@ -97,4 +107,4 @@ const ViewWithSections: React.FC<ViewWithSectionsProps> = React.memo(
97107
}
98108
);
99109

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

0 commit comments

Comments
 (0)