Skip to content

Commit ff9a0a5

Browse files
committed
fix: view variables
1 parent 2b17909 commit ff9a0a5

File tree

4 files changed

+103
-102
lines changed

4 files changed

+103
-102
lines changed

src/api/services/views.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -64,16 +64,18 @@ export const getAllViews = (
6464
})
6565
);
6666
};
67+
68+
/**
69+
* Get the data for a view by its id.
70+
*/
6771
export const getViewDataById = async (
6872
viewId: string,
69-
filters?: Record<string, string>,
73+
variables?: Record<string, string>,
7074
headers?: Record<string, string>
7175
): Promise<ViewResult> => {
72-
const body: { variables?: Record<string, string> } = {};
73-
74-
if (filters && Object.keys(filters).length > 0) {
75-
body.variables = filters;
76-
}
76+
const body: { variables?: Record<string, string> } = {
77+
variables: variables
78+
};
7779

7880
const response = await fetch(`/api/view/${viewId}`, {
7981
method: "POST",

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

Lines changed: 42 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,15 @@
1-
import { useMemo, useState, useEffect, useCallback } from "react";
2-
import { useQueryClient } from "@tanstack/react-query";
1+
import { useMemo, useCallback } from "react";
32
import {
4-
ReactSelectDropdown,
5-
StateOption
6-
} from "../../../../components/ReactSelectDropdown";
7-
import { ViewFilter } from "../../types";
3+
GroupByOptions,
4+
MultiSelectDropdown
5+
} from "../../../../ui/Dropdowns/MultiSelectDropdown";
6+
import { ViewVariable } from "../../types";
87
import { formatDisplayLabel } from "./panels/utils";
98

109
interface GlobalFiltersProps {
11-
filters?: ViewFilter[];
12-
viewId: string;
13-
namespace?: string;
14-
name?: string;
15-
onFilterStateChange?: (filterState: Record<string, string>) => void;
10+
filters?: ViewVariable[];
11+
current?: Record<string, string>;
12+
onChange?: (filterState: Record<string, string>) => void;
1613
}
1714

1815
interface GlobalFilterDropdownProps {
@@ -31,92 +28,67 @@ const GlobalFilterDropdown: React.FC<GlobalFilterDropdownProps> = ({
3128
onChange
3229
}) => {
3330
const dropdownOptions = useMemo(() => {
34-
return options.map(
31+
const mappedOptions = options.map(
3532
(option) =>
3633
({
37-
id: option,
3834
value: option,
39-
label: option,
40-
description: option
41-
}) satisfies StateOption
35+
label: option
36+
}) satisfies GroupByOptions
4237
);
38+
39+
return mappedOptions;
4340
}, [options]);
4441

42+
const selectedValue = useMemo(() => {
43+
if (!value) {
44+
return { value: "all", label: "All" };
45+
}
46+
return (
47+
dropdownOptions.find((option) => option.value === value) || {
48+
value: "all",
49+
label: "All"
50+
}
51+
);
52+
}, [value, dropdownOptions]);
53+
4554
return (
46-
<ReactSelectDropdown
55+
<MultiSelectDropdown
4756
label={label}
48-
name={paramsKey}
49-
items={dropdownOptions}
50-
value={value}
51-
onChange={(selectedValue) => {
57+
options={dropdownOptions}
58+
value={selectedValue}
59+
onChange={(selectedOption: unknown) => {
60+
const option = selectedOption as GroupByOptions;
5261
onChange(
5362
paramsKey,
54-
selectedValue && selectedValue !== "all" ? selectedValue : undefined
63+
option && option.value !== "all" ? option.value : undefined
5564
);
5665
}}
57-
placeholder="Select..."
58-
dropDownClassNames="w-auto max-w-[400px]"
66+
className="w-auto max-w-[400px]"
5967
isMulti={false}
68+
closeMenuOnSelect={true}
69+
isClearable={false}
6070
/>
6171
);
6272
};
6373

6474
const GlobalFilters: React.FC<GlobalFiltersProps> = ({
6575
filters,
66-
viewId,
67-
namespace,
68-
name,
69-
onFilterStateChange
76+
current = {},
77+
onChange
7078
}) => {
71-
const queryClient = useQueryClient();
72-
73-
// Initialize filter state with defaults
74-
const [filterState, setFilterState] = useState<Record<string, string>>({});
75-
const [hasInitialized, setHasInitialized] = useState(false);
76-
77-
// Update filter state when filters prop changes (e.g., when view data loads)
78-
// But only initialize once, don't reset user selections
79-
useEffect(() => {
80-
if (filters && filters.length > 0 && !hasInitialized) {
81-
const initial: Record<string, string> = {};
82-
filters.forEach((filter) => {
83-
// Use default value if provided, otherwise use first option
84-
const defaultValue =
85-
filter.default ||
86-
(filter.options.length > 0 ? filter.options[0] : "");
87-
if (defaultValue) {
88-
initial[filter.key] = defaultValue;
89-
}
90-
});
91-
92-
setFilterState(initial);
93-
setHasInitialized(true);
94-
console.log("Setting initial filter state:", initial);
95-
onFilterStateChange?.(initial);
96-
}
97-
}, [filters, hasInitialized, onFilterStateChange]);
98-
9979
const handleFilterChange = useCallback(
10080
(key: string, value?: string) => {
101-
const newFilterState = { ...filterState };
81+
const newFilterState = { ...current };
10282
if (value) {
10383
newFilterState[key] = value;
10484
} else {
10585
delete newFilterState[key];
10686
}
10787

108-
setFilterState(newFilterState);
109-
console.log("Filter changed, new state:", newFilterState);
110-
onFilterStateChange?.(newFilterState);
111-
112-
// Invalidate table queries to refresh table data
113-
if (namespace && name) {
114-
queryClient.invalidateQueries({
115-
queryKey: ["view-table", namespace, name]
116-
});
117-
}
88+
console.log("GlobalFilters: Filter changed, new state:", newFilterState);
89+
onChange?.(newFilterState);
11890
},
119-
[filterState, namespace, name, queryClient, onFilterStateChange]
91+
[current, onChange]
12092
);
12193

12294
const filterComponents = useMemo(() => {
@@ -128,11 +100,11 @@ const GlobalFilters: React.FC<GlobalFiltersProps> = ({
128100
label={filter.label || formatDisplayLabel(filter.key)}
129101
paramsKey={filter.key}
130102
options={filter.options}
131-
value={filterState[filter.key]}
103+
value={current[filter.key]}
132104
onChange={handleFilterChange}
133105
/>
134106
));
135-
}, [filters, filterState, handleFilterChange]);
107+
}, [filters, current, handleFilterChange]);
136108

137109
if (!filters || filters.length === 0) {
138110
return null;

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

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,9 @@ interface ViewProps {
3232
columns?: ViewColumnDef[];
3333
columnOptions?: Record<string, string[]>;
3434
variables?: ViewVariable[];
35-
viewId?: string;
36-
onGlobalFilterStateChange?: (filterState: Record<string, string>) => void;
35+
onVariableStateChange?: (filterState: Record<string, string>) => void;
3736
viewResult?: ViewResult;
38-
currentGlobalFilters?: Record<string, string>;
37+
currentVariables?: Record<string, string>;
3938
}
4039

4140
const View: React.FC<ViewProps> = ({
@@ -46,10 +45,9 @@ const View: React.FC<ViewProps> = ({
4645
columnOptions,
4746
panels,
4847
variables,
49-
viewId,
50-
onGlobalFilterStateChange,
48+
onVariableStateChange,
5149
viewResult,
52-
currentGlobalFilters
50+
currentVariables
5351
}) => {
5452
const { pageSize } = useReactTablePaginationState();
5553
const [searchParams] = useSearchParams();
@@ -134,10 +132,8 @@ const View: React.FC<ViewProps> = ({
134132

135133
<GlobalFilters
136134
filters={variables}
137-
viewId={viewId || ""}
138-
namespace={namespace}
139-
name={name}
140-
onFilterStateChange={onGlobalFilterStateChange}
135+
current={currentVariables}
136+
onChange={onVariableStateChange}
141137
/>
142138

143139
{variables && variables.length > 0 && (

src/pages/views/components/SingleView.tsx

Lines changed: 47 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import { useState, useEffect } from "react";
1+
import { useState, useEffect, useCallback } from "react";
22
import { useQuery, useQueryClient } from "@tanstack/react-query";
33
import { getViewDataById } from "../../../api/services/views";
4+
import { ViewVariable } from "../../audit-report/types";
45
import View from "../../audit-report/components/View/View";
56
import { Head } from "../../../ui/Head";
67
import { Icon } from "../../../ui/Icons/Icon";
@@ -14,26 +15,27 @@ interface SingleViewProps {
1415

1516
const SingleView: React.FC<SingleViewProps> = ({ id }) => {
1617
const [error, setError] = useState<string>();
17-
const [currentGlobalFilters, setCurrentGlobalFilters] = useState<
18+
const [currentViewVariables, setCurrentViewVariables] = useState<
1819
Record<string, string>
1920
>({});
21+
const [hasFiltersInitialized, setHasFiltersInitialized] = useState(false);
2022
const queryClient = useQueryClient();
2123

2224
// Fetch all the view metadata, panel results and the column definitions
2325
// NOTE: This doesn't fetch the table rows.
24-
// Use currentGlobalFilters in the query key so it updates when filters change
2526
const {
2627
data: viewResult,
2728
isLoading,
29+
isFetching,
2830
error: viewDataError
2931
} = useQuery({
30-
queryKey: ["view-result", id, currentGlobalFilters],
32+
queryKey: ["view-result", id, currentViewVariables],
3133
queryFn: () => {
32-
console.log("useQuery running with filters:", currentGlobalFilters);
33-
return getViewDataById(id, currentGlobalFilters);
34+
return getViewDataById(id, currentViewVariables);
3435
},
3536
enabled: !!id,
36-
staleTime: 5 * 60 * 1000
37+
staleTime: 5 * 60 * 1000,
38+
placeholderData: (previousData: any) => previousData
3739
});
3840

3941
useEffect(() => {
@@ -48,7 +50,36 @@ const SingleView: React.FC<SingleViewProps> = ({ id }) => {
4850
setError(undefined);
4951
}, [viewDataError]);
5052

51-
if (isLoading) {
53+
// Initialize filters when view data loads, but preserve user selections
54+
useEffect(() => {
55+
if (viewResult?.variables && viewResult.variables.length > 0) {
56+
if (!hasFiltersInitialized) {
57+
// First time - initialize with defaults
58+
const initial: Record<string, string> = {};
59+
viewResult.variables.forEach((filter: ViewVariable) => {
60+
const defaultValue =
61+
filter.default ||
62+
(filter.options.length > 0 ? filter.options[0] : "");
63+
if (defaultValue) {
64+
initial[filter.key] = defaultValue;
65+
}
66+
});
67+
setCurrentViewVariables(initial);
68+
setHasFiltersInitialized(true);
69+
}
70+
}
71+
}, [viewResult?.variables, hasFiltersInitialized]);
72+
73+
// Handle global filter changes with useCallback to stabilize reference
74+
const handleGlobalFilterChange = useCallback(
75+
(newFilters: Record<string, string>) => {
76+
setCurrentViewVariables(newFilters);
77+
},
78+
[]
79+
);
80+
81+
// Only show full loading screen for initial load, not for filter refetches
82+
if (isLoading && !viewResult) {
5283
return (
5384
<div className="flex min-h-screen items-center justify-center">
5485
<div className="text-center">
@@ -60,8 +91,9 @@ const SingleView: React.FC<SingleViewProps> = ({ id }) => {
6091
}
6192

6293
if (!viewResult) {
63-
// FIXME: No view result does not mean the view is not found.
64-
// we need to display the error in here.
94+
// TODO: Better error handling.
95+
// viewResult = undefined does not mean the view is not found.
96+
// There could be errors other than 404.
6597
return (
6698
<div className="flex min-h-screen items-center justify-center">
6799
<div className="text-center">
@@ -89,11 +121,11 @@ const SingleView: React.FC<SingleViewProps> = ({ id }) => {
89121

90122
const handleForceRefresh = async () => {
91123
if (namespace && name) {
92-
const freshData = await getViewDataById(id, currentGlobalFilters, {
124+
const freshData = await getViewDataById(id, currentViewVariables, {
93125
"cache-control": "max-age=1"
94126
});
95127
queryClient.setQueryData(
96-
["view-result", id, currentGlobalFilters],
128+
["view-result", id, currentViewVariables],
97129
freshData
98130
);
99131
// Invalidate the table query that will be handled by the View component
@@ -119,7 +151,7 @@ const SingleView: React.FC<SingleViewProps> = ({ id }) => {
119151
}
120152
onRefresh={handleForceRefresh}
121153
contentClass="p-0 h-full"
122-
loading={isLoading}
154+
loading={isFetching}
123155
extra={
124156
viewResult?.lastRefreshedAt && (
125157
<p className="text-sm text-gray-500">
@@ -138,10 +170,9 @@ const SingleView: React.FC<SingleViewProps> = ({ id }) => {
138170
columnOptions={viewResult?.columnOptions}
139171
panels={viewResult?.panels}
140172
variables={viewResult?.variables}
141-
viewId={id}
142-
onGlobalFilterStateChange={setCurrentGlobalFilters}
173+
onVariableStateChange={handleGlobalFilterChange}
143174
viewResult={viewResult}
144-
currentGlobalFilters={currentGlobalFilters}
175+
currentVariables={currentViewVariables}
145176
/>
146177
</div>
147178
</SearchLayout>

0 commit comments

Comments
 (0)