Skip to content

Commit 1f1d7e0

Browse files
authored
feat: view sections (#2739)
* feat: View sections * fix: view variables between sections * feat: Display templating variables once at top of page for multi-section views * Fix refresh so sections re-fetch actual data * Eliminate circular dependency between view.tsx and ViewSection.tsx & Make section React keys unique across namespaces to avoid collisions * Remove or wire up unused viewHash helper to avoid dead code * fix: Stop global filter bar flicker/re-fetch on every change (adjust useAggregatedViewVariables caching/query keys) * fix: updating the dependent templating variable * Fix view metadata refresh and add missing type import * Refactor view API to use shared fetch helper and add URL encoding
1 parent a9d266e commit 1f1d7e0

File tree

10 files changed

+482
-183
lines changed

10 files changed

+482
-183
lines changed

src/api/services/views.ts

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -70,16 +70,16 @@ export const getAllViews = (
7070
/**
7171
* Get the data for a view by its id.
7272
*/
73-
export const getViewDataById = async (
74-
viewId: string,
73+
const fetchViewData = async (
74+
path: string,
7575
variables?: Record<string, string>,
7676
headers?: Record<string, string>
7777
): Promise<ViewResult> => {
7878
const body: { variables?: Record<string, string> } = {
79-
variables: variables
79+
variables
8080
};
8181

82-
const response = await fetch(`/api/view/${viewId}`, {
82+
const response = await fetch(path, {
8383
method: "POST",
8484
credentials: "include",
8585
headers: {
@@ -99,6 +99,31 @@ export const getViewDataById = async (
9999
return response.json();
100100
};
101101

102+
export const getViewDataById = async (
103+
viewId: string,
104+
variables?: Record<string, string>,
105+
headers?: Record<string, string>
106+
): Promise<ViewResult> => {
107+
return fetchViewData(
108+
`/api/view/${encodeURIComponent(viewId)}`,
109+
variables,
110+
headers
111+
);
112+
};
113+
114+
export const getViewDataByNamespace = async (
115+
namespace: string,
116+
name: string,
117+
variables?: Record<string, string>,
118+
headers?: Record<string, string>
119+
): Promise<ViewResult> => {
120+
return fetchViewData(
121+
`/api/view/${encodeURIComponent(namespace)}/${encodeURIComponent(name)}`,
122+
variables,
123+
headers
124+
);
125+
};
126+
102127
/**
103128
* Get display plugin variables for a view based on a config
104129
*/

src/pages/audit-report/components/View/View.tsx

Lines changed: 99 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import GlobalFilters from "./GlobalFilters";
2828
import GlobalFiltersForm from "./GlobalFiltersForm";
2929
import { usePrefixedSearchParams } from "../../../../hooks/usePrefixedSearchParams";
3030
import ViewCardsDisplay from "./ViewCardsDisplay";
31+
import { VIEW_VAR_PREFIX } from "@flanksource-ui/pages/views/constants";
3132

3233
interface ViewProps {
3334
title?: string;
@@ -43,6 +44,7 @@ interface ViewProps {
4344
};
4445
requestFingerprint: string;
4546
currentVariables?: Record<string, string>;
47+
hideVariables?: boolean;
4648
}
4749

4850
const View: React.FC<ViewProps> = ({
@@ -55,7 +57,8 @@ const View: React.FC<ViewProps> = ({
5557
variables,
5658
card,
5759
requestFingerprint,
58-
currentVariables
60+
currentVariables,
61+
hideVariables
5962
}) => {
6063
const { pageSize } = useReactTablePaginationState();
6164

@@ -66,8 +69,8 @@ const View: React.FC<ViewProps> = ({
6669
// Separate display mode state (frontend only, not sent to backend)
6770
const [searchParams, setSearchParams] = useSearchParams();
6871

69-
// Create unique prefix for global filters
70-
const globalVarPrefix = "viewvar";
72+
// Create unique prefix for global filters (same as ViewSection uses)
73+
const globalVarPrefix = VIEW_VAR_PREFIX;
7174
const hasDataTable = columns && columns.length > 0;
7275

7376
// Detect if card mode is available (supports both new and old cardPosition field)
@@ -161,94 +164,96 @@ const View: React.FC<ViewProps> = ({
161164

162165
return (
163166
<>
164-
{title !== "" && (
165-
<h3 className="mb-4 flex items-center text-xl font-semibold">
166-
<Box className="mr-2 text-teal-600" size={20} />
167-
{title}
168-
</h3>
169-
)}
170-
171-
{variables && variables.length > 0 && (
172-
<GlobalFiltersForm
173-
variables={variables}
174-
globalVarPrefix={globalVarPrefix}
175-
currentVariables={currentVariables}
176-
>
177-
<GlobalFilters variables={variables} />
178-
</GlobalFiltersForm>
179-
)}
180-
181-
{variables && variables.length > 0 && (
182-
<hr className="my-4 border-gray-200" />
183-
)}
167+
<div className="flex-none">
168+
{title !== "" && (
169+
<h3 className="mb-4 flex items-center text-xl font-semibold">
170+
<Box className="mr-2 text-teal-600" size={20} />
171+
{title}
172+
</h3>
173+
)}
184174

185-
<div className="mb-4 space-y-6">
186-
{panels && panels.length > 0 && (
187-
<div
188-
className="grid gap-4"
189-
style={{
190-
gridTemplateColumns: "repeat(auto-fit, minmax(280px, 1fr))",
191-
gridAutoRows: "minmax(auto, 250px)"
192-
}}
175+
{!hideVariables && variables && variables.length > 0 && (
176+
<GlobalFiltersForm
177+
variables={variables}
178+
globalVarPrefix={globalVarPrefix}
179+
currentVariables={currentVariables}
193180
>
194-
{groupAndRenderPanels(panels)}
195-
</div>
181+
<GlobalFilters variables={variables} />
182+
</GlobalFiltersForm>
196183
)}
197-
</div>
198184

199-
<ViewTableFilterForm
200-
filterFields={filterFields}
201-
defaultFieldValues={{}}
202-
tablePrefix={tablePrefix}
203-
>
204-
{hasDataTable && (
205-
<div className="mb-2">
206-
<div className="flex flex-wrap items-center justify-between gap-2">
207-
<div className="flex flex-wrap items-center gap-2">
208-
{filterableColumns.map(({ column, options }) => (
209-
<ViewColumnDropdown
210-
key={column.name}
211-
label={formatDisplayLabel(column.name)}
212-
paramsKey={column.name}
213-
options={options}
214-
isLabelsColumn={column.type === "labels"}
215-
/>
216-
))}
217-
</div>
185+
{!hideVariables && variables && variables.length > 0 && (
186+
<hr className="my-4 border-gray-200" />
187+
)}
188+
189+
<div className="mb-4 space-y-6">
190+
{panels && panels.length > 0 && (
191+
<div
192+
className="grid gap-4"
193+
style={{
194+
gridTemplateColumns: "repeat(auto-fit, minmax(280px, 1fr))",
195+
gridAutoRows: "minmax(auto, 250px)"
196+
}}
197+
>
198+
{groupAndRenderPanels(panels)}
199+
</div>
200+
)}
201+
</div>
218202

219-
{/* Display Mode Toggle */}
220-
{hasCardMode && (
221-
<div className="flex items-center gap-1 rounded-lg border border-gray-300 bg-white p-1">
222-
<button
223-
onClick={() => setDisplayMode("table")}
224-
className={`flex items-center gap-1.5 rounded px-3 py-1.5 text-sm font-medium transition-colors ${
225-
displayMode === "table"
226-
? "bg-blue-100 text-blue-700"
227-
: "text-gray-600 hover:bg-gray-100"
228-
}`}
229-
title="Table view"
230-
>
231-
<Table2 size={16} />
232-
Table
233-
</button>
234-
<button
235-
onClick={() => setDisplayMode("cards")}
236-
className={`flex items-center gap-1.5 rounded px-3 py-1.5 text-sm font-medium transition-colors ${
237-
displayMode === "cards"
238-
? "bg-blue-100 text-blue-700"
239-
: "text-gray-600 hover:bg-gray-100"
240-
}`}
241-
title="Card view"
242-
>
243-
<LayoutGrid size={16} />
244-
Cards
245-
</button>
203+
<ViewTableFilterForm
204+
filterFields={filterFields}
205+
defaultFieldValues={{}}
206+
tablePrefix={tablePrefix}
207+
>
208+
{hasDataTable && (
209+
<div className="mb-2">
210+
<div className="flex flex-wrap items-center justify-between gap-2">
211+
<div className="flex flex-wrap items-center gap-2">
212+
{filterableColumns.map(({ column, options }) => (
213+
<ViewColumnDropdown
214+
key={column.name}
215+
label={formatDisplayLabel(column.name)}
216+
paramsKey={column.name}
217+
options={options}
218+
isLabelsColumn={column.type === "labels"}
219+
/>
220+
))}
246221
</div>
247-
)}
222+
223+
{/* Display Mode Toggle */}
224+
{hasCardMode && (
225+
<div className="flex items-center gap-1 rounded-lg border border-gray-300 bg-white p-1">
226+
<button
227+
onClick={() => setDisplayMode("table")}
228+
className={`flex items-center gap-1.5 rounded px-3 py-1.5 text-sm font-medium transition-colors ${
229+
displayMode === "table"
230+
? "bg-blue-100 text-blue-700"
231+
: "text-gray-600 hover:bg-gray-100"
232+
}`}
233+
title="Table view"
234+
>
235+
<Table2 size={16} />
236+
Table
237+
</button>
238+
<button
239+
onClick={() => setDisplayMode("cards")}
240+
className={`flex items-center gap-1.5 rounded px-3 py-1.5 text-sm font-medium transition-colors ${
241+
displayMode === "cards"
242+
? "bg-blue-100 text-blue-700"
243+
: "text-gray-600 hover:bg-gray-100"
244+
}`}
245+
title="Card view"
246+
>
247+
<LayoutGrid size={16} />
248+
Cards
249+
</button>
250+
</div>
251+
)}
252+
</div>
248253
</div>
249-
</div>
250-
)}
251-
</ViewTableFilterForm>
254+
)}
255+
</ViewTableFilterForm>
256+
</div>
252257

253258
{tableError && (
254259
<div className="text-center text-red-500">
@@ -270,14 +275,16 @@ const View: React.FC<ViewProps> = ({
270275
totalRowCount={totalEntries}
271276
/>
272277
) : (
273-
<DynamicDataTable
274-
columns={columns}
275-
isLoading={isLoading}
276-
rows={rows || []}
277-
pageCount={totalEntries ? Math.ceil(totalEntries / pageSize) : 1}
278-
totalRowCount={totalEntries}
279-
tablePrefix={tablePrefix}
280-
/>
278+
<div className="min-h-0 flex-1">
279+
<DynamicDataTable
280+
columns={columns}
281+
isLoading={isLoading}
282+
rows={rows || []}
283+
pageCount={totalEntries ? Math.ceil(totalEntries / pageSize) : 1}
284+
totalRowCount={totalEntries}
285+
tablePrefix={tablePrefix}
286+
/>
287+
</div>
281288
))}
282289
</>
283290
);

src/pages/audit-report/types/index.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -376,6 +376,17 @@ export interface ColumnFilterOptions {
376376
labels?: Record<string, string[]>;
377377
}
378378

379+
export interface ViewRef {
380+
namespace: string;
381+
name: string;
382+
}
383+
384+
export interface ViewSection {
385+
title: string;
386+
icon?: string;
387+
viewRef: ViewRef;
388+
}
389+
379390
export interface ViewResult {
380391
title?: string;
381392
icon?: string;
@@ -390,6 +401,7 @@ export interface ViewResult {
390401
variables?: ViewVariable[];
391402
card?: DisplayCard;
392403
requestFingerprint: string;
404+
sections?: ViewSection[];
393405
}
394406

395407
export interface GaugeThreshold {

0 commit comments

Comments
 (0)