Skip to content

Commit 7e88d02

Browse files
feat: finished filters
Signed-off-by: Henry Gressmann <[email protected]>
1 parent 0d39c1b commit 7e88d02

File tree

11 files changed

+198
-65
lines changed

11 files changed

+198
-65
lines changed

Cargo.lock

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

data/licenses-cargo.json

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

data/licenses-npm.json

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

src/app/core/reports.rs

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ pub enum Metric {
4545
AvgViewsPerSession,
4646
}
4747

48-
#[derive(Debug, Enum, Clone, Copy)]
48+
#[derive(Debug, Enum, Clone, Copy, PartialEq)]
4949
#[oai(rename_all = "snake_case")]
5050
pub enum Dimension {
5151
Url,
@@ -59,14 +59,19 @@ pub enum Dimension {
5959
City,
6060
}
6161

62-
#[derive(Enum, Debug, Clone, Copy)]
62+
#[derive(Enum, Debug, Clone, Copy, PartialEq)]
6363
#[oai(rename_all = "snake_case")]
6464
pub enum FilterType {
65+
// Generic filters
66+
IsNull,
67+
68+
// String filters
6569
Equal,
6670
Contains,
6771
StartsWith,
6872
EndsWith,
69-
IsNull,
73+
74+
// Boolean filters
7075
IsTrue,
7176
IsFalse,
7277
}
@@ -148,6 +153,18 @@ fn filter_sql(filters: &[DimensionFilter]) -> Result<(String, ParamVec)> {
148153
_ => bail!("Invalid filter type for value"),
149154
};
150155

156+
if filter.dimension == Dimension::Mobile
157+
&& !(filter.filter_type == FilterType::IsFalse || filter.filter_type == FilterType::IsTrue)
158+
{
159+
bail!("Invalid filter type for boolean dimension");
160+
}
161+
162+
if filter.dimension != Dimension::Mobile
163+
&& (filter.filter_type == FilterType::IsFalse || filter.filter_type == FilterType::IsTrue)
164+
{
165+
bail!("Invalid filter type for string dimension");
166+
}
167+
151168
Ok(match filter.dimension {
152169
Dimension::Url => format!("concat(fqdn, path) {}", filter_value),
153170
Dimension::Path => format!("path {}", filter_value),

web/bun.lockb

0 Bytes
Binary file not shown.

web/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
"react-tooltip": "^5.28.0"
3434
},
3535
"devDependencies": {
36-
"@biomejs/biome": "1.9.0",
36+
"@biomejs/biome": "1.9.1",
3737
"@types/react": "^18.3.5",
3838
"@types/react-dom": "^18.3.0",
3939
"@types/react-simple-maps": "^3.0.6",

web/src/api/constants.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ export const dimensionNames: Record<Dimension, string> = {
2121

2222
export const filterNames: Record<DimensionFilter["filterType"], string> = {
2323
contains: "contains",
24-
equal: "is",
24+
equal: "equals",
2525
is_null: "is null",
2626
ends_with: "ends with",
2727
is_false: "is false",
@@ -38,5 +38,3 @@ export const filterNamesInverted: Record<DimensionFilter["filterType"], string>
3838
is_true: "is not true",
3939
starts_with: "does not start with",
4040
};
41-
42-
export const capitalizeAll = (str: string) => str.replace(/(?:^|\s)\S/g, (a) => a.toUpperCase());

web/src/api/types.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,19 @@ export type Dimension = OASModel<DashboardSpec, "Dimension">;
88
export type DimensionFilter = OASModel<DashboardSpec, "DimensionFilter">;
99
export type DimensionTableRow = OASModel<DashboardSpec, "DimensionTableRow">;
1010
export type FilterType = OASModel<DashboardSpec, "FilterType">;
11+
12+
export const dimensions = [
13+
"platform",
14+
"browser",
15+
"url",
16+
"path",
17+
"mobile",
18+
"referrer",
19+
"city",
20+
"country",
21+
"fqdn",
22+
] as const satisfies Dimension[];
23+
1124
export const filterTypes = [
1225
"contains",
1326
"equal",

web/src/components/project/filter.module.css

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,26 @@
55
flex-wrap: wrap;
66
}
77

8+
.formInvertable {
9+
display: flex;
10+
gap: 0.8rem;
11+
align-items: flex-end;
12+
margin-bottom: 0.5rem;
13+
14+
select,
15+
fieldset {
16+
margin: 0;
17+
margin-top: 0.2rem;
18+
}
19+
20+
> label,
21+
> div {
22+
flex: 1;
23+
display: flex;
24+
flex-direction: column;
25+
}
26+
}
27+
828
.filterField {
929
display: flex;
1030
gap: 0.2rem;

web/src/components/project/filter.tsx

Lines changed: 137 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,10 @@
11
import styles from "./filter.module.css";
22
import { SearchIcon, XIcon } from "lucide-react";
3-
import {
4-
capitalizeAll,
5-
dimensionNames,
6-
filterNames,
7-
filterNamesInverted,
8-
filterTypes,
9-
type DimensionFilter,
10-
type FilterType,
11-
} from "../../api";
3+
4+
import { dimensionNames, filterNames, filterNamesInverted, type DimensionFilter, type FilterType } from "../../api";
5+
126
import { Dialog } from "../dialog";
13-
import { cls } from "../../utils";
7+
import { capitalizeAll, cls } from "../../utils";
148
import { useRef, useState } from "react";
159

1610
export const SelectFilters = ({
@@ -27,9 +21,14 @@ export const SelectFilters = ({
2721
<div className={styles.filterField}>
2822
<span>{dimensionNames[filter.dimension]}</span>
2923
<span className={styles.filterType}>
30-
{filter.inversed ? filterNamesInverted[filter.filterType] : filterNames[filter.filterType]}
24+
{filters?.[filter.dimension].displayType?.(filter) ??
25+
(filter.inversed ? filterNamesInverted[filter.filterType] : filterNames[filter.filterType])}
3126
</span>
32-
{filter.filterType === "is_null" ? null : <span className={styles.filterValue}>{filter.value}</span>}
27+
{filter.filterType === "is_null" ? null : (
28+
<span className={styles.filterValue}>
29+
{filters?.[filter.dimension].displayValue?.(filter) ?? filter.value}
30+
</span>
31+
)}
3332
</div>
3433
<button type="button" onClick={() => onChange(value.filter((_, j) => i !== j))} className={styles.remove}>
3534
<XIcon size={20} />
@@ -43,35 +42,109 @@ export const SelectFilters = ({
4342
);
4443
};
4544

45+
const filters = {
46+
platform: {
47+
invertable: true,
48+
filterTypes: ["equal", "contains"],
49+
},
50+
browser: {
51+
invertable: true,
52+
filterTypes: ["equal", "contains", "starts_with", "ends_with"],
53+
},
54+
url: {
55+
invertable: true,
56+
filterTypes: ["equal", "contains", "starts_with", "ends_with"],
57+
},
58+
fqdn: {
59+
invertable: true,
60+
filterTypes: ["equal", "contains", "starts_with", "ends_with"],
61+
},
62+
path: {
63+
invertable: true,
64+
filterTypes: ["equal", "contains", "starts_with", "ends_with"],
65+
},
66+
referrer: {
67+
invertable: true,
68+
filterTypes: ["equal", "contains"],
69+
},
70+
city: {
71+
invertable: true,
72+
filterTypes: ["equal", "contains"],
73+
},
74+
country: {
75+
invertable: true,
76+
filterTypes: ["equal", "contains"],
77+
},
78+
mobile: {
79+
custom: true,
80+
displayValue: (filter: DimensionFilter) => (filter.filterType === "is_true" ? "Mobile" : "Desktop"),
81+
displayType: (filter: DimensionFilter) => (filter.inversed ? "is not" : "is"),
82+
render: () => (
83+
<label>
84+
Device Type
85+
<select name="mobile">
86+
<option value="true">Mobile</option>
87+
<option value="false">Desktop</option>
88+
</select>
89+
</label>
90+
),
91+
getFilter: (data: FormData) => {
92+
return {
93+
dimension: "mobile",
94+
filterType: data.get("mobile") === "true" ? "is_true" : "is_false",
95+
value: undefined,
96+
};
97+
},
98+
},
99+
} as Record<
100+
keyof typeof dimensionNames,
101+
{
102+
filterTypes: FilterType[];
103+
invertable?: boolean;
104+
custom?: boolean;
105+
render?: () => JSX.Element;
106+
getFilter?: (data: FormData) => DimensionFilter;
107+
displayValue?: (filter: DimensionFilter) => string;
108+
displayType?: (filter: DimensionFilter) => string;
109+
}
110+
>;
111+
112+
type filterDimension = keyof typeof filters;
113+
46114
const FilterDialog = ({
47115
onAdd,
48116
}: {
49117
onAdd: (filter: DimensionFilter) => void;
50118
}) => {
51119
const closeRef = useRef<HTMLButtonElement>(null);
52-
const [dimension, setDimension] = useState<keyof typeof dimensionNames>("url");
53-
const [filterType, setFilterType] = useState<FilterType | `${FilterType}-inverted`>("equal");
54-
const [value, setValue] = useState("");
120+
const [dimension, setDimension] = useState<filterDimension>("url");
121+
const filter = filters[dimension];
55122

56123
const handleSubmit = (e: React.FormEvent) => {
57124
e.preventDefault();
125+
const data = new FormData(e.currentTarget as HTMLFormElement);
126+
127+
if (filter.getFilter) {
128+
onAdd(filter.getFilter(data));
129+
closeRef.current?.click();
130+
return;
131+
}
132+
58133
onAdd({
59134
dimension,
60-
inversed: filterType.endsWith("-inverted"),
61-
filterType: filterType.replace("-inverted", "") as FilterType,
62-
value: value.length ? value : undefined,
135+
inversed: filter.invertable && data.get("inverted") === "on",
136+
filterType: data.get("filterType") as FilterType,
137+
value: data.get("value") as string,
63138
});
64139
setDimension("url");
65-
setFilterType("equal");
66-
setValue("");
67140
closeRef.current?.click();
68141
};
69142

70-
const dimensions = Object.entries(dimensionNames) as [keyof typeof dimensionNames, string][];
71-
72143
return (
73144
<Dialog
74145
title="Add Filter"
146+
description="Filter the data by a specific dimension"
147+
hideDescription
75148
trigger={
76149
<button type="button">
77150
<h2>Add Filter</h2>
@@ -82,41 +155,53 @@ const FilterDialog = ({
82155
<form onSubmit={handleSubmit}>
83156
<label>
84157
Dimension
85-
<select
86-
name="dimension"
87-
value={dimension}
88-
onChange={(e) => setDimension(e.target.value as keyof typeof dimensionNames)}
89-
>
90-
{dimensions.map(([dimension, name]) => (
158+
<select name="dimension" value={dimension} onChange={(e) => setDimension(e.target.value as filterDimension)}>
159+
{Object.keys(filters).map((dimension) => (
91160
<option key={dimension} value={dimension}>
92-
{dimensionNames[dimension]}
161+
{dimensionNames[dimension as filterDimension]}
93162
</option>
94163
))}
95164
</select>
96165
</label>
97-
<label>
98-
Filter Type
99-
<select
100-
name="filterType"
101-
value={filterType}
102-
onChange={(e) => setFilterType(e.target.value as FilterType | `${FilterType}-inverted`)}
103-
>
104-
{filterTypes.map((filterType) => (
105-
<>
106-
<option key={filterType} value={filterType}>
107-
{capitalizeAll(filterNames[filterType])}
108-
</option>
109-
<option key={`${filterType}-inverted`} value={`${filterType}-inverted`}>
110-
{capitalizeAll(filterNamesInverted[filterType])}
111-
</option>
112-
</>
113-
))}
114-
</select>
115-
</label>
116-
<label>
117-
Value
118-
<input type="text" name="value" value={value} onChange={(e) => setValue(e.target.value)} />
119-
</label>
166+
167+
{filter.custom && filter.render?.()}
168+
169+
{!filter.custom && (
170+
<div className={styles.formInvertable}>
171+
<label>
172+
Filter Type
173+
<select name="filterType">
174+
{filter.filterTypes.map((filterType) => (
175+
<option key={filterType} value={filterType}>
176+
{capitalizeAll(filterNames[filterType])}
177+
</option>
178+
))}
179+
</select>
180+
</label>
181+
{filter.invertable && (
182+
<div className={styles.inverted}>
183+
<fieldset name="test">
184+
<label>
185+
<input defaultChecked type="radio" aria-invalid="false" />
186+
Show Matches
187+
</label>
188+
<label>
189+
<input type="radio" name="inverted" aria-invalid="true" />
190+
Exclude Matches
191+
</label>
192+
</fieldset>
193+
</div>
194+
)}
195+
</div>
196+
)}
197+
198+
{!filter.custom && (
199+
<label>
200+
Value
201+
<input type="text" name="value" />
202+
</label>
203+
)}
204+
120205
<div className="grid">
121206
<Dialog.Close asChild ref={closeRef}>
122207
<button className="secondary outline" type="button">

0 commit comments

Comments
 (0)