Skip to content

Commit d73dbe2

Browse files
authored
Merge pull request #112 from projectsveltos/search
filtering addon tables by query parameters
2 parents dcfb6d7 + 5b10e51 commit d73dbe2

File tree

25 files changed

+773
-210
lines changed

25 files changed

+773
-210
lines changed

components.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
"cssVariables": true
1111
},
1212
"aliases": {
13-
"components": "@/components",
13+
"components": "@/lib/components",
1414
"utils": "@/lib/utils"
1515
}
1616
}

package-lock.json

Lines changed: 128 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
"@radix-ui/react-popover": "^1.0.7",
2626
"@radix-ui/react-scroll-area": "^1.2.1",
2727
"@radix-ui/react-separator": "^1.0.3",
28-
"@radix-ui/react-slot": "^1.0.2",
28+
"@radix-ui/react-slot": "^1.2.3",
2929
"@radix-ui/react-tabs": "^1.0.4",
3030
"@radix-ui/react-toggle": "^1.0.3",
3131
"@radix-ui/react-toggle-group": "^1.0.4",

src/config/app.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import { ClusterType } from "@/types/cluster.types";
22
import { sveltosClusterValue } from "@/types/cluster.consts";
33

4+
interface QueryParams {
5+
failure: string;
6+
}
47
interface AppConfig {
58
name: string;
69
github: {
@@ -11,6 +14,8 @@ interface AppConfig {
1114
defaultPage: number;
1215
defaultSize: number;
1316
maxBadges: number;
17+
queryParams: QueryParams;
18+
debounceDelay: number;
1419
defaultTableSize: number;
1520
}
1621

@@ -25,4 +30,8 @@ export const appConfig: AppConfig = {
2530
defaultSize: 8,
2631
defaultTableSize: 5,
2732
maxBadges: 2,
33+
queryParams: {
34+
failure: "failure",
35+
},
36+
debounceDelay: 600,
2837
};
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import { useSearchParams } from "react-router-dom";
2+
import { FC, useState, useMemo, useCallback, useRef, memo } from "react";
3+
import {
4+
InputGroup,
5+
InputGroupAddon,
6+
InputGroupInput,
7+
} from "@/lib/components/ui/inputs/input-group";
8+
import { Search, Trash2 } from "lucide-react";
9+
import useDebounce from "@/hooks/useDebounce";
10+
import { appConfig } from "@/config/app";
11+
12+
interface SearchConfig {
13+
key: string;
14+
placeholder: string;
15+
}
16+
17+
interface SearchInputProps {
18+
searchConfig: SearchConfig[];
19+
onSearch?: (key: string, value: string) => void;
20+
}
21+
22+
export const SearchQueryParamInput: FC<SearchInputProps> = memo(
23+
({ searchConfig, onSearch }) => {
24+
const [searchParams, setSearchParams] = useSearchParams();
25+
26+
// Memoize initial values to avoid recalculating on every render
27+
const initialValues = useMemo(
28+
() =>
29+
searchConfig.reduce(
30+
(acc, { key }) => {
31+
acc[key] = searchParams.get(key) || "";
32+
return acc;
33+
},
34+
{} as Record<string, string>,
35+
),
36+
[searchConfig, searchParams],
37+
);
38+
39+
const [values, setValues] = useState<Record<string, string>>(initialValues);
40+
const prevValuesRef = useRef(values);
41+
42+
// Debounce the effect to update search params
43+
useDebounce(
44+
() => {
45+
if (JSON.stringify(prevValuesRef.current) === JSON.stringify(values)) {
46+
return;
47+
}
48+
49+
const updatedSearchParams = new URLSearchParams(searchParams);
50+
51+
Object.entries(values).forEach(([key, value]) => {
52+
if (value) {
53+
updatedSearchParams.set(key, value);
54+
} else {
55+
updatedSearchParams.delete(key);
56+
}
57+
});
58+
59+
setSearchParams(updatedSearchParams);
60+
prevValuesRef.current = values; // Update the ref
61+
62+
if (onSearch) {
63+
Object.entries(values).forEach(([key, value]) => {
64+
onSearch(key, value);
65+
});
66+
}
67+
},
68+
[values],
69+
appConfig.debounceDelay,
70+
);
71+
72+
const handleClear = useCallback((key: string) => {
73+
setValues((prev) => {
74+
if (prev[key] === "") return prev;
75+
return { ...prev, [key]: "" };
76+
});
77+
}, []);
78+
79+
const handleChange = useCallback((key: string, value: string) => {
80+
setValues((prev) => {
81+
if (prev[key] === value) return prev;
82+
return { ...prev, [key]: value };
83+
});
84+
}, []);
85+
86+
return (
87+
<div className="flex flex-wrap gap-4">
88+
{searchConfig.map(({ key, placeholder }) => (
89+
<InputGroup key={key} className="max-w-sm my-2">
90+
<InputGroupInput
91+
placeholder={placeholder}
92+
value={values[key]}
93+
onChange={(e) => handleChange(key, e.target.value)}
94+
/>
95+
<InputGroupAddon>
96+
<Search className="cursor-pointer" />
97+
</InputGroupAddon>
98+
<InputGroupAddon align="inline-end" className="capitalize">
99+
{key
100+
.replace(/_/g, " ")
101+
.replace(/\b\w/g, (char) => char.toUpperCase())}
102+
</InputGroupAddon>
103+
{values[key] && (
104+
<InputGroupAddon
105+
align="inline-end"
106+
onClick={() => handleClear(key)}
107+
className={
108+
"cursor-pointer bg-slate-300 rounded text-xs mx-1 px-1"
109+
}
110+
>
111+
<Trash2 className={"h-4 w-4"} />
112+
</InputGroupAddon>
113+
)}
114+
</InputGroup>
115+
))}
116+
</div>
117+
);
118+
},
119+
);
120+
121+
SearchQueryParamInput.displayName = "SearchQueryParamInput";

0 commit comments

Comments
 (0)