mirror of
https://github.com/umami-software/umami.git
synced 2026-02-25 06:55:35 +01:00
enable using same filters
This commit is contained in:
parent
b4b3ba5552
commit
79c06787cd
9 changed files with 125 additions and 145 deletions
|
|
@ -1,91 +1,28 @@
|
|||
import { useMemo } from 'react';
|
||||
import { FILTER_COLUMNS } from '@/lib/constants';
|
||||
import { useNavigation } from './useNavigation';
|
||||
|
||||
export function useFilterParameters() {
|
||||
const {
|
||||
query: {
|
||||
path,
|
||||
referrer,
|
||||
title,
|
||||
query,
|
||||
host,
|
||||
os,
|
||||
browser,
|
||||
device,
|
||||
country,
|
||||
region,
|
||||
city,
|
||||
event,
|
||||
tag,
|
||||
hostname,
|
||||
distinctId,
|
||||
utmSource,
|
||||
utmMedium,
|
||||
utmCampaign,
|
||||
utmContent,
|
||||
utmTerm,
|
||||
page,
|
||||
pageSize,
|
||||
search,
|
||||
segment,
|
||||
cohort,
|
||||
excludeBounce,
|
||||
},
|
||||
} = useNavigation();
|
||||
const { query } = useNavigation();
|
||||
|
||||
return useMemo(() => {
|
||||
const filterParams: Record<string, any> = {};
|
||||
|
||||
for (const key of Object.keys(query)) {
|
||||
const baseName = key.replace(/\d+$/, '');
|
||||
if (FILTER_COLUMNS[baseName]) {
|
||||
filterParams[key] = query[key];
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
path,
|
||||
referrer,
|
||||
title,
|
||||
query,
|
||||
host,
|
||||
os,
|
||||
browser,
|
||||
device,
|
||||
country,
|
||||
region,
|
||||
city,
|
||||
event,
|
||||
tag,
|
||||
hostname,
|
||||
distinctId,
|
||||
utmSource,
|
||||
utmMedium,
|
||||
utmCampaign,
|
||||
utmContent,
|
||||
utmTerm,
|
||||
search,
|
||||
segment,
|
||||
cohort,
|
||||
excludeBounce,
|
||||
...filterParams,
|
||||
search: query.search,
|
||||
segment: query.segment,
|
||||
cohort: query.cohort,
|
||||
excludeBounce: query.excludeBounce,
|
||||
page: query.page,
|
||||
pageSize: query.pageSize,
|
||||
};
|
||||
}, [
|
||||
path,
|
||||
referrer,
|
||||
title,
|
||||
query,
|
||||
host,
|
||||
os,
|
||||
browser,
|
||||
device,
|
||||
country,
|
||||
region,
|
||||
city,
|
||||
event,
|
||||
tag,
|
||||
hostname,
|
||||
distinctId,
|
||||
utmSource,
|
||||
utmMedium,
|
||||
utmCampaign,
|
||||
utmContent,
|
||||
utmTerm,
|
||||
page,
|
||||
pageSize,
|
||||
search,
|
||||
segment,
|
||||
cohort,
|
||||
excludeBounce,
|
||||
]);
|
||||
}, [query]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -74,10 +74,11 @@ export function useFilters() {
|
|||
};
|
||||
|
||||
const filters = Object.keys(query).reduce((arr, key) => {
|
||||
if (FILTER_COLUMNS[key]) {
|
||||
const baseName = key.replace(/\d+$/, '');
|
||||
if (FILTER_COLUMNS[baseName]) {
|
||||
let operator = 'eq';
|
||||
let value = safeDecodeURIComponent(query[key]);
|
||||
const label = fields.find(({ name }) => name === key)?.label;
|
||||
const label = fields.find(({ name }) => name === baseName)?.label;
|
||||
|
||||
const match = value.match(/^([a-z]+)\.(.*)/);
|
||||
|
||||
|
|
@ -88,6 +89,7 @@ export function useFilters() {
|
|||
|
||||
return arr.concat({
|
||||
name: key,
|
||||
type: baseName,
|
||||
operator,
|
||||
value,
|
||||
label,
|
||||
|
|
|
|||
|
|
@ -48,24 +48,24 @@ export function FieldFilters({ websiteId, value, exclude = [], onChange }: Field
|
|||
{} as Record<FieldGroup, typeof fields>,
|
||||
);
|
||||
|
||||
const updateFilter = (name: string, props: Record<string, any>) => {
|
||||
onChange(value.map(filter => (filter.name === name ? { ...filter, ...props } : filter)));
|
||||
const updateFilter = (index: number, props: Record<string, any>) => {
|
||||
onChange(value.map((filter, i) => (i === index ? { ...filter, ...props } : filter)));
|
||||
};
|
||||
|
||||
const handleAdd = (name: Key) => {
|
||||
onChange(value.concat({ name: name.toString(), operator: 'eq', value: '' }));
|
||||
};
|
||||
|
||||
const handleChange = (name: string, value: Key) => {
|
||||
updateFilter(name, { value });
|
||||
const handleChange = (index: number, val: Key) => {
|
||||
updateFilter(index, { value: val });
|
||||
};
|
||||
|
||||
const handleSelect = (name: string, operator: Key) => {
|
||||
updateFilter(name, { operator });
|
||||
const handleSelect = (index: number, operator: Key) => {
|
||||
updateFilter(index, { operator });
|
||||
};
|
||||
|
||||
const handleRemove = (name: string) => {
|
||||
onChange(value.filter(filter => filter.name !== name));
|
||||
const handleRemove = (index: number) => {
|
||||
onChange(value.filter((_, i) => i !== index));
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
@ -87,9 +87,8 @@ export function FieldFilters({ websiteId, value, exclude = [], onChange }: Field
|
|||
return (
|
||||
<MenuSection key={groupKey} title={label}>
|
||||
{groupFields.map(field => {
|
||||
const isDisabled = !!value.find(({ name }) => name === field.name);
|
||||
return (
|
||||
<MenuItem key={field.name} id={field.name} isDisabled={isDisabled}>
|
||||
<MenuItem key={field.name} id={field.name}>
|
||||
{field.filterLabel}
|
||||
</MenuItem>
|
||||
);
|
||||
|
|
@ -115,9 +114,8 @@ export function FieldFilters({ websiteId, value, exclude = [], onChange }: Field
|
|||
return (
|
||||
<ListSection key={groupKey} title={label}>
|
||||
{groupFields.map(field => {
|
||||
const isDisabled = !!value.find(({ name }) => name === field.name);
|
||||
return (
|
||||
<ListItem key={field.name} id={field.name} isDisabled={isDisabled}>
|
||||
<ListItem key={field.name} id={field.name}>
|
||||
{field.filterLabel}
|
||||
</ListItem>
|
||||
);
|
||||
|
|
@ -128,18 +126,18 @@ export function FieldFilters({ websiteId, value, exclude = [], onChange }: Field
|
|||
</List>
|
||||
</Column>
|
||||
<Column overflow="auto" gapY="4" style={{ contain: 'layout' }}>
|
||||
{value.map(filter => {
|
||||
{value.map((filter, index) => {
|
||||
return (
|
||||
<FilterRecord
|
||||
key={filter.name}
|
||||
key={`${filter.name}-${index}`}
|
||||
websiteId={websiteId}
|
||||
type={filter.name}
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
{...filter}
|
||||
onSelect={handleSelect}
|
||||
onRemove={handleRemove}
|
||||
onChange={handleChange}
|
||||
onSelect={(_name, operator) => handleSelect(index, operator)}
|
||||
onRemove={() => handleRemove(index)}
|
||||
onChange={(_name, val) => handleChange(index, val)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
|
|
|||
|
|
@ -78,12 +78,12 @@ export function FilterBar({ websiteId }: { websiteId: string }) {
|
|||
/>
|
||||
)}
|
||||
{filters.map(filter => {
|
||||
const { name, label, operator, value } = filter;
|
||||
const { name, type, label, operator, value } = filter;
|
||||
const paramValue = isSearchOperator(operator)
|
||||
? value
|
||||
: String(value)
|
||||
.split(',')
|
||||
.map(v => formatValue(v, name))
|
||||
.map(v => formatValue(v, type || name))
|
||||
.join(', ');
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -70,14 +70,21 @@ function getSearchSQL(column: string, param: string = 'search'): string {
|
|||
return `and positionCaseInsensitive(${column}, {${param}:String}) > 0`;
|
||||
}
|
||||
|
||||
function mapFilter(column: string, operator: string, name: string, type: string = 'String') {
|
||||
const value = `{${name}:${type}}`;
|
||||
function mapFilter(
|
||||
column: string,
|
||||
operator: string,
|
||||
name: string,
|
||||
type: string = 'String',
|
||||
paramName?: string,
|
||||
) {
|
||||
const param = paramName ?? name;
|
||||
const value = `{${param}:${type}}`;
|
||||
|
||||
switch (operator) {
|
||||
case OPERATORS.equals:
|
||||
return `${column} IN {${name}:Array(${type})}`;
|
||||
return `${column} IN {${param}:Array(${type})}`;
|
||||
case OPERATORS.notEquals:
|
||||
return `${column} NOT IN {${name}:Array(${type})}`;
|
||||
return `${column} NOT IN {${param}:Array(${type})}`;
|
||||
case OPERATORS.contains:
|
||||
return `positionCaseInsensitive(${column}, ${value}) > 0`;
|
||||
case OPERATORS.doesNotContain:
|
||||
|
|
@ -92,27 +99,30 @@ function mapFilter(column: string, operator: string, name: string, type: string
|
|||
}
|
||||
|
||||
function getFilterQuery(filters: Record<string, any>, options: QueryOptions = {}) {
|
||||
const query = filtersObjectToArray(filters, options).reduce((arr, { name, column, operator }) => {
|
||||
const isCohort = options?.isCohort;
|
||||
const query = filtersObjectToArray(filters, options).reduce(
|
||||
(arr, { name, column, operator, paramName }) => {
|
||||
const isCohort = options?.isCohort;
|
||||
|
||||
if (isCohort) {
|
||||
column = FILTER_COLUMNS[name.slice('cohort_'.length)];
|
||||
}
|
||||
|
||||
if (column) {
|
||||
if (name === 'eventType') {
|
||||
arr.push(`and ${mapFilter(column, operator, name, 'UInt32')}`);
|
||||
} else {
|
||||
arr.push(`and ${mapFilter(column, operator, name)}`);
|
||||
if (isCohort) {
|
||||
column = FILTER_COLUMNS[name.slice('cohort_'.length)];
|
||||
}
|
||||
|
||||
if (name === 'referrer') {
|
||||
arr.push(`and referrer_domain != hostname`);
|
||||
}
|
||||
}
|
||||
if (column) {
|
||||
if (name === 'eventType') {
|
||||
arr.push(`and ${mapFilter(column, operator, name, 'UInt32', paramName)}`);
|
||||
} else {
|
||||
arr.push(`and ${mapFilter(column, operator, name, 'String', paramName)}`);
|
||||
}
|
||||
|
||||
return arr;
|
||||
}, []);
|
||||
if (name === 'referrer') {
|
||||
arr.push(`and referrer_domain != hostname`);
|
||||
}
|
||||
}
|
||||
|
||||
return arr;
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
return query.join('\n');
|
||||
}
|
||||
|
|
@ -177,13 +187,15 @@ function getDateQuery(filters: Record<string, any>) {
|
|||
function getQueryParams(filters: Record<string, any>) {
|
||||
return {
|
||||
...filters,
|
||||
...filtersObjectToArray(filters).reduce((obj, { name, column, operator, value }) => {
|
||||
...filtersObjectToArray(filters).reduce((obj, { name, column, operator, value, paramName }) => {
|
||||
const resolvedColumn =
|
||||
column || (name?.startsWith('cohort_') && FILTER_COLUMNS[name.slice('cohort_'.length)]);
|
||||
|
||||
if (!resolvedColumn || !name || value === undefined) return obj;
|
||||
|
||||
obj[name] = ([OPERATORS.equals, OPERATORS.notEquals] as string[]).includes(operator)
|
||||
const key = paramName ?? name;
|
||||
|
||||
obj[key] = ([OPERATORS.equals, OPERATORS.notEquals] as string[]).includes(operator)
|
||||
? Array.isArray(value)
|
||||
? value
|
||||
: [value]
|
||||
|
|
|
|||
|
|
@ -51,15 +51,23 @@ export function filtersObjectToArray(filters: QueryFilters, options: QueryOption
|
|||
return arr;
|
||||
}
|
||||
|
||||
const baseName = key.replace(/\d+$/, '');
|
||||
const paramName = key !== baseName ? key : undefined;
|
||||
|
||||
if (filter?.name && filter?.value !== undefined) {
|
||||
return arr.concat({ ...filter, column: options?.columns?.[key] ?? FILTER_COLUMNS[key] });
|
||||
return arr.concat({
|
||||
...filter,
|
||||
column: options?.columns?.[baseName] ?? FILTER_COLUMNS[baseName],
|
||||
paramName: paramName ?? filter.paramName,
|
||||
});
|
||||
}
|
||||
|
||||
const { operator, value } = parseFilterValue(filter);
|
||||
|
||||
return arr.concat({
|
||||
name: key,
|
||||
column: options?.columns?.[key] ?? FILTER_COLUMNS[key],
|
||||
name: baseName,
|
||||
paramName,
|
||||
column: options?.columns?.[baseName] ?? FILTER_COLUMNS[baseName],
|
||||
operator,
|
||||
value,
|
||||
prefix: options?.prefix,
|
||||
|
|
@ -68,10 +76,14 @@ export function filtersObjectToArray(filters: QueryFilters, options: QueryOption
|
|||
}
|
||||
|
||||
export function filtersArrayToObject(filters: Filter[]) {
|
||||
const nameCounts: Record<string, number> = {};
|
||||
return filters.reduce((obj, filter: Filter) => {
|
||||
const { name, operator, value } = filter;
|
||||
const count = nameCounts[name] ?? 0;
|
||||
const key = count === 0 ? name : `${name}${count}`;
|
||||
nameCounts[name] = count + 1;
|
||||
|
||||
obj[name] = `${operator}.${Array.isArray(value) ? value.join(',') : value}`;
|
||||
obj[key] = `${operator}.${Array.isArray(value) ? value.join(',') : value}`;
|
||||
|
||||
return obj;
|
||||
}, {});
|
||||
|
|
|
|||
|
|
@ -71,8 +71,15 @@ function getSearchSQL(column: string, param: string = 'search'): string {
|
|||
return `and ${column} ilike {{${param}}}`;
|
||||
}
|
||||
|
||||
function mapFilter(column: string, operator: string, name: string, type: string = '') {
|
||||
const value = `{{${name}${type ? `::${type}` : ''}}}`;
|
||||
function mapFilter(
|
||||
column: string,
|
||||
operator: string,
|
||||
name: string,
|
||||
type: string = '',
|
||||
paramName?: string,
|
||||
) {
|
||||
const param = paramName ?? name;
|
||||
const value = `{{${param}${type ? `::${type}` : ''}}}`;
|
||||
|
||||
if (name.startsWith('cohort_')) {
|
||||
name = name.slice('cohort_'.length);
|
||||
|
|
@ -100,7 +107,7 @@ function mapFilter(column: string, operator: string, name: string, type: string
|
|||
|
||||
function getFilterQuery(filters: Record<string, any>, options: QueryOptions = {}): string {
|
||||
const query = filtersObjectToArray(filters, options).reduce(
|
||||
(arr, { name, column, operator, prefix = '' }) => {
|
||||
(arr, { name, column, operator, prefix = '', paramName }) => {
|
||||
const isCohort = options?.isCohort;
|
||||
|
||||
if (isCohort) {
|
||||
|
|
@ -108,7 +115,7 @@ function getFilterQuery(filters: Record<string, any>, options: QueryOptions = {}
|
|||
}
|
||||
|
||||
if (column) {
|
||||
arr.push(`and ${mapFilter(`${prefix}${column}`, operator, name)}`);
|
||||
arr.push(`and ${mapFilter(`${prefix}${column}`, operator, name, '', paramName)}`);
|
||||
|
||||
if (name === 'referrer') {
|
||||
arr.push(
|
||||
|
|
@ -181,18 +188,20 @@ function getDateQuery(filters: Record<string, any>) {
|
|||
function getQueryParams(filters: Record<string, any>) {
|
||||
return {
|
||||
...filters,
|
||||
...filtersObjectToArray(filters).reduce((obj, { name, column, operator, value }) => {
|
||||
...filtersObjectToArray(filters).reduce((obj, { name, column, operator, value, paramName }) => {
|
||||
const resolvedColumn =
|
||||
column || (name?.startsWith('cohort_') && FILTER_COLUMNS[name.slice('cohort_'.length)]);
|
||||
|
||||
if (!resolvedColumn) return obj;
|
||||
|
||||
const key = paramName ?? name;
|
||||
|
||||
if (([OPERATORS.contains, OPERATORS.doesNotContain] as Operator[]).includes(operator)) {
|
||||
obj[name] = `%${value}%`;
|
||||
obj[key] = `%${value}%`;
|
||||
} else if (([OPERATORS.equals, OPERATORS.notEquals] as Operator[]).includes(operator)) {
|
||||
obj[name] = Array.isArray(value) ? value : [value];
|
||||
obj[key] = Array.isArray(value) ? value : [value];
|
||||
} else {
|
||||
obj[name] = value;
|
||||
obj[key] = value;
|
||||
}
|
||||
|
||||
return obj;
|
||||
|
|
@ -201,9 +210,10 @@ function getQueryParams(filters: Record<string, any>) {
|
|||
}
|
||||
|
||||
function parseFilters(filters: Record<string, any>, options?: QueryOptions) {
|
||||
const joinSession = Object.keys(filters).find(key =>
|
||||
['referrer', ...SESSION_COLUMNS].includes(key),
|
||||
);
|
||||
const joinSession = Object.keys(filters).find(key => {
|
||||
const baseName = key.replace(/\d+$/, '');
|
||||
return ['referrer', ...SESSION_COLUMNS].includes(baseName);
|
||||
});
|
||||
|
||||
const cohortFilters = Object.fromEntries(
|
||||
Object.entries(filters).filter(([key]) => key.startsWith('cohort_')),
|
||||
|
|
|
|||
|
|
@ -22,12 +22,20 @@ export async function parseRequest(
|
|||
|
||||
if (schema) {
|
||||
const isGet = request.method === 'GET';
|
||||
const rawQuery = query;
|
||||
const result = schema.safeParse(isGet ? query : body);
|
||||
|
||||
if (!result.success) {
|
||||
error = () => badRequest(z.treeifyError(result.error));
|
||||
} else if (isGet) {
|
||||
query = result.data;
|
||||
|
||||
// Re-add suffixed filter params (e.g., browser1, os2) stripped by Zod schema
|
||||
for (const key of Object.keys(rawQuery)) {
|
||||
if (/\d+$/.test(key) && !(key in query)) {
|
||||
query[key] = rawQuery[key];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
body = result.data;
|
||||
}
|
||||
|
|
@ -71,10 +79,10 @@ export function getRequestDateRange(query: Record<string, string>) {
|
|||
export function getRequestFilters(query: Record<string, any>) {
|
||||
const result: Record<string, any> = {};
|
||||
|
||||
for (const key of Object.keys(FILTER_COLUMNS)) {
|
||||
const value = query[key];
|
||||
if (value !== undefined) {
|
||||
result[key] = value;
|
||||
for (const key of Object.keys(query)) {
|
||||
const baseName = key.replace(/\d+$/, '');
|
||||
if (baseName in FILTER_COLUMNS) {
|
||||
result[key] = query[key];
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ export interface Filter {
|
|||
type?: string;
|
||||
column?: string;
|
||||
prefix?: string;
|
||||
paramName?: string;
|
||||
}
|
||||
|
||||
export interface DateRange {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue