mirror of
https://github.com/kremalicious/umami.git
synced 2024-11-21 17:37:00 +01:00
Support contains queries in overview page.
This commit is contained in:
commit
d945ed3a23
@ -211,7 +211,7 @@ const ResultsMenu = ({ values, type, isLoading, onSelect }) => {
|
||||
}
|
||||
|
||||
if (!values?.length) {
|
||||
return <h1>poop</h1>;
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -1,5 +1,4 @@
|
||||
.form {
|
||||
position: absolute;
|
||||
background: var(--base50);
|
||||
min-width: 300px;
|
||||
padding: 20px;
|
||||
|
@ -35,7 +35,7 @@ export function ShareUrl({
|
||||
|
||||
const url = `${hostUrl || process.env.hostUrl || window?.location.origin}${
|
||||
process.env.basePath
|
||||
}/share/${id}/${encodeURIComponent(domain)}`;
|
||||
}/share/${id}/${domain}`;
|
||||
|
||||
const handleGenerate = () => {
|
||||
setId(generateId());
|
||||
|
@ -2,6 +2,7 @@ import { Button, Icon, Icons, Popup, PopupTrigger, Text } from 'react-basics';
|
||||
import PopupForm from 'app/(main)/reports/[reportId]/PopupForm';
|
||||
import FilterSelectForm from 'app/(main)/reports/[reportId]/FilterSelectForm';
|
||||
import { useFields, useMessages, useNavigation } from 'components/hooks';
|
||||
import { OPERATORS } from 'lib/constants';
|
||||
|
||||
export function WebsiteFilterButton({
|
||||
websiteId,
|
||||
@ -14,8 +15,18 @@ export function WebsiteFilterButton({
|
||||
const { renderUrl, router } = useNavigation();
|
||||
const { fields } = useFields();
|
||||
|
||||
const handleAddFilter = ({ name, value }) => {
|
||||
router.push(renderUrl({ [name]: value }));
|
||||
const handleAddFilter = ({ name, operator, value }) => {
|
||||
let prefix = '';
|
||||
|
||||
if (operator === OPERATORS.notEquals) {
|
||||
prefix = '!';
|
||||
} else if (operator === OPERATORS.contains) {
|
||||
prefix = '~';
|
||||
} else if (operator === OPERATORS.doesNotContain) {
|
||||
prefix = '!~';
|
||||
}
|
||||
|
||||
router.push(renderUrl({ [name]: prefix + value }));
|
||||
};
|
||||
|
||||
return (
|
||||
@ -26,7 +37,7 @@ export function WebsiteFilterButton({
|
||||
</Icon>
|
||||
<Text>{formatMessage(labels.filter)}</Text>
|
||||
</Button>
|
||||
<Popup position="bottom" alignment="start">
|
||||
<Popup position="bottom" alignment="end">
|
||||
{(close: () => void) => {
|
||||
return (
|
||||
<PopupForm>
|
||||
@ -37,7 +48,6 @@ export function WebsiteFilterButton({
|
||||
handleAddFilter(value);
|
||||
close();
|
||||
}}
|
||||
allowFilterSelect={false}
|
||||
/>
|
||||
</PopupForm>
|
||||
);
|
||||
|
@ -47,24 +47,15 @@ export function WebsiteMetricsBar({
|
||||
value={views.value}
|
||||
change={views.change}
|
||||
/>
|
||||
<MetricCard
|
||||
label={formatMessage(labels.visitors)}
|
||||
value={visitors.value}
|
||||
change={visitors.change}
|
||||
/>
|
||||
<MetricCard
|
||||
label={formatMessage(labels.visits)}
|
||||
value={visits.value}
|
||||
change={visits.change}
|
||||
/>
|
||||
<MetricCard
|
||||
label={formatMessage(labels.viewsPerVisit)}
|
||||
value={visits.value ? views.value / visits.value : 0}
|
||||
change={
|
||||
visits.value && visits.change
|
||||
? views.value / visits.value - diffs.views / diffs.visits
|
||||
: 0
|
||||
}
|
||||
label={formatMessage(labels.visitors)}
|
||||
value={visitors.value}
|
||||
change={visitors.change}
|
||||
/>
|
||||
<MetricCard
|
||||
label={formatMessage(labels.bounceRate)}
|
||||
|
@ -117,7 +117,7 @@ export function DateFilter({
|
||||
);
|
||||
}
|
||||
|
||||
return options.find(e => e.value === value).label;
|
||||
return options.find(e => e.value === value)?.label;
|
||||
};
|
||||
|
||||
return (
|
||||
|
@ -62,30 +62,32 @@ function getDateFormat(date: Date) {
|
||||
}
|
||||
|
||||
function mapFilter(column: string, operator: string, name: string, type: string = 'String') {
|
||||
const value = `{${name}:${type}}`;
|
||||
|
||||
switch (operator) {
|
||||
case OPERATORS.equals:
|
||||
return `${column} = {${name}:${type}}`;
|
||||
return `${column} = ${value}`;
|
||||
case OPERATORS.notEquals:
|
||||
return `${column} != {${name}:${type}}`;
|
||||
return `${column} != ${value}`;
|
||||
case OPERATORS.contains:
|
||||
return `positionCaseInsensitive(${column}, {${name}:${type}}) > 0`;
|
||||
return `positionCaseInsensitive(${column}, ${value}) > 0`;
|
||||
case OPERATORS.doesNotContain:
|
||||
return `positionCaseInsensitive(${column}, {${name}:${type}}) = 0`;
|
||||
return `positionCaseInsensitive(${column}, ${value}) = 0`;
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function getFilterQuery(filters: QueryFilters = {}, options: QueryOptions = {}) {
|
||||
const query = Object.keys(filters).reduce((arr, name) => {
|
||||
const value = filters[name];
|
||||
const filter = value?.filter ?? OPERATORS.equals;
|
||||
const column = value?.column ?? FILTER_COLUMNS[name] ?? options?.columns?.[name];
|
||||
const query = Object.keys(filters).reduce((arr, key) => {
|
||||
const filter = filters[key];
|
||||
const operator = filter?.operator ?? OPERATORS.equals;
|
||||
const column = filter?.column ?? FILTER_COLUMNS[key] ?? options?.columns?.[key];
|
||||
|
||||
if (value !== undefined && column !== undefined) {
|
||||
arr.push(`and ${mapFilter(column, filter, name)}`);
|
||||
if (filter !== undefined && column !== undefined) {
|
||||
arr.push(`and ${mapFilter(column, operator, key)}`);
|
||||
|
||||
if (name === 'referrer') {
|
||||
if (key === 'referrer') {
|
||||
arr.push('and referrer_domain != {websiteDomain:String}');
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,13 @@
|
||||
import { NextApiRequest } from 'next';
|
||||
import { getAllowedUnits, getMinimumUnit } from './date';
|
||||
import { getWebsiteDateRange } from '../queries';
|
||||
import { FILTER_COLUMNS, OPERATORS } from 'lib/constants';
|
||||
|
||||
const OPERATOR_SYMBOLS = {
|
||||
'!': 'neq',
|
||||
'~': 'c',
|
||||
'!~': 'dnc',
|
||||
};
|
||||
|
||||
export async function parseDateRangeQuery(req: NextApiRequest) {
|
||||
const { websiteId, startAt, endAt, unit } = req.query;
|
||||
@ -29,3 +36,28 @@ export async function parseDateRangeQuery(req: NextApiRequest) {
|
||||
unit: (getAllowedUnits(startDate, endDate).includes(unit as string) ? unit : minUnit) as string,
|
||||
};
|
||||
}
|
||||
|
||||
export function getQueryFilters(req: NextApiRequest) {
|
||||
return Object.keys(FILTER_COLUMNS).reduce((obj, key) => {
|
||||
const value = req.query[key];
|
||||
|
||||
if (value) {
|
||||
obj[key] = value;
|
||||
}
|
||||
|
||||
if (typeof value === 'string') {
|
||||
const [, prefix, paramValue] = value.match(/^(!~|!|~)?(.*)$/);
|
||||
|
||||
if (prefix && paramValue) {
|
||||
obj[key] = {
|
||||
name: key,
|
||||
column: FILTER_COLUMNS[key],
|
||||
operator: OPERATOR_SYMBOLS[prefix] || OPERATORS.equals,
|
||||
value: paramValue,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return obj;
|
||||
}, {});
|
||||
}
|
||||
|
@ -5,7 +5,7 @@ import { canViewWebsite } from 'lib/auth';
|
||||
import { useAuth, useCors, useValidate } from 'lib/middleware';
|
||||
import { SESSION_COLUMNS, EVENT_COLUMNS, FILTER_COLUMNS, OPERATORS } from 'lib/constants';
|
||||
import { getPageviewMetrics, getSessionMetrics } from 'queries';
|
||||
import { parseDateRangeQuery } from 'lib/query';
|
||||
import { getQueryFilters, parseDateRangeQuery } from 'lib/query';
|
||||
import * as yup from 'yup';
|
||||
|
||||
export interface WebsiteMetricsRequestQuery {
|
||||
@ -62,25 +62,7 @@ export default async (
|
||||
await useAuth(req, res);
|
||||
await useValidate(schema, req, res);
|
||||
|
||||
const {
|
||||
websiteId,
|
||||
type,
|
||||
url,
|
||||
referrer,
|
||||
title,
|
||||
query,
|
||||
os,
|
||||
browser,
|
||||
device,
|
||||
country,
|
||||
region,
|
||||
city,
|
||||
language,
|
||||
event,
|
||||
limit,
|
||||
offset,
|
||||
search,
|
||||
} = req.query;
|
||||
const { websiteId, type, limit, offset, search } = req.query;
|
||||
|
||||
if (req.method === 'GET') {
|
||||
if (!(await canViewWebsite(req.auth, websiteId))) {
|
||||
@ -90,24 +72,13 @@ export default async (
|
||||
const { startDate, endDate } = await parseDateRangeQuery(req);
|
||||
const column = FILTER_COLUMNS[type] || type;
|
||||
const filters = {
|
||||
...getQueryFilters(req),
|
||||
startDate,
|
||||
endDate,
|
||||
url,
|
||||
referrer,
|
||||
title,
|
||||
query,
|
||||
os,
|
||||
browser,
|
||||
device,
|
||||
country,
|
||||
region,
|
||||
city,
|
||||
language,
|
||||
event,
|
||||
};
|
||||
|
||||
if (search) {
|
||||
filters[column] = {
|
||||
filters[type] = {
|
||||
column,
|
||||
operator: OPERATORS.contains,
|
||||
value: search,
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { canViewWebsite } from 'lib/auth';
|
||||
import { useAuth, useCors, useValidate } from 'lib/middleware';
|
||||
import { parseDateRangeQuery } from 'lib/query';
|
||||
import { getQueryFilters, parseDateRangeQuery } from 'lib/query';
|
||||
import { NextApiRequestQueryBody, WebsitePageviews } from 'lib/types';
|
||||
import { NextApiResponse } from 'next';
|
||||
import { methodNotAllowed, ok, unauthorized } from 'next-basics';
|
||||
@ -52,8 +52,7 @@ export default async (
|
||||
await useAuth(req, res);
|
||||
await useValidate(schema, req, res);
|
||||
|
||||
const { websiteId, timezone, url, referrer, title, os, browser, device, country, region, city } =
|
||||
req.query;
|
||||
const { websiteId, timezone } = req.query;
|
||||
|
||||
if (req.method === 'GET') {
|
||||
if (!(await canViewWebsite(req.auth, websiteId))) {
|
||||
@ -63,19 +62,11 @@ export default async (
|
||||
const { startDate, endDate, unit } = await parseDateRangeQuery(req);
|
||||
|
||||
const filters = {
|
||||
...getQueryFilters(req),
|
||||
startDate,
|
||||
endDate,
|
||||
timezone,
|
||||
unit,
|
||||
url,
|
||||
referrer,
|
||||
title,
|
||||
os,
|
||||
browser,
|
||||
device,
|
||||
country,
|
||||
region,
|
||||
city,
|
||||
};
|
||||
|
||||
const [pageviews, sessions] = await Promise.all([
|
||||
|
@ -4,7 +4,7 @@ import { methodNotAllowed, ok, unauthorized } from 'next-basics';
|
||||
import { canViewWebsite } from 'lib/auth';
|
||||
import { useAuth, useCors, useValidate } from 'lib/middleware';
|
||||
import { NextApiRequestQueryBody, WebsiteStats } from 'lib/types';
|
||||
import { parseDateRangeQuery } from 'lib/query';
|
||||
import { getQueryFilters, parseDateRangeQuery } from 'lib/query';
|
||||
import { getWebsiteStats } from 'queries';
|
||||
|
||||
export interface WebsiteStatsRequestQuery {
|
||||
@ -52,20 +52,7 @@ export default async (
|
||||
await useAuth(req, res);
|
||||
await useValidate(schema, req, res);
|
||||
|
||||
const {
|
||||
websiteId,
|
||||
url,
|
||||
referrer,
|
||||
title,
|
||||
query,
|
||||
event,
|
||||
os,
|
||||
browser,
|
||||
device,
|
||||
country,
|
||||
region,
|
||||
city,
|
||||
}: any & { websiteId: string } = req.query;
|
||||
const { websiteId } = req.query;
|
||||
|
||||
if (req.method === 'GET') {
|
||||
if (!(await canViewWebsite(req.auth, websiteId))) {
|
||||
@ -77,19 +64,7 @@ export default async (
|
||||
const prevStartDate = subMinutes(startDate, diff);
|
||||
const prevEndDate = subMinutes(endDate, diff);
|
||||
|
||||
const filters = {
|
||||
url,
|
||||
referrer,
|
||||
title,
|
||||
query,
|
||||
event,
|
||||
os,
|
||||
browser,
|
||||
device,
|
||||
country,
|
||||
region,
|
||||
city,
|
||||
};
|
||||
const filters = getQueryFilters(req);
|
||||
|
||||
const metrics = await getWebsiteStats(websiteId, { ...filters, startDate, endDate });
|
||||
|
||||
|
@ -70,7 +70,6 @@ async function clickhouseQuery(
|
||||
offset: number = 0,
|
||||
): Promise<{ x: string; y: number }[]> {
|
||||
const { rawQuery, parseFilters } = clickhouse;
|
||||
|
||||
const { filterQuery, params } = await parseFilters(websiteId, {
|
||||
...filters,
|
||||
eventType: column === 'event_name' ? EVENT_TYPE.customEvent : EVENT_TYPE.pageView,
|
||||
|
Loading…
Reference in New Issue
Block a user