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