diff --git a/src/components/metrics/MetricsTable.tsx b/src/components/metrics/MetricsTable.tsx index 63e3a7cd..26355018 100644 --- a/src/components/metrics/MetricsTable.tsx +++ b/src/components/metrics/MetricsTable.tsx @@ -68,6 +68,8 @@ export function MetricsTable({ country, region, city, + limit, + search, }, { retryDelay: delay || DEFAULT_ANIMATION_DURATION, onDataLoad }, ); @@ -86,20 +88,8 @@ export function MetricsTable({ } } - if (search) { - items = items.filter(({ x, ...data }) => { - const value = formatValue(x, type, data); - - return value?.toLowerCase().includes(search.toLowerCase()); - }); - } - items = percentFilter(items); - if (limit) { - items = items.slice(0, limit - 1); - } - return items; } return []; @@ -114,6 +104,7 @@ export function MetricsTable({ className={styles.search} value={search} onSearch={setSearch} + delay={300} autoFocus={true} /> )} diff --git a/src/lib/clickhouse.ts b/src/lib/clickhouse.ts index 75328534..6206f2f0 100644 --- a/src/lib/clickhouse.ts +++ b/src/lib/clickhouse.ts @@ -61,12 +61,14 @@ function getDateFormat(date: Date) { return `'${dateFormat(date, 'UTC:yyyy-mm-dd HH:MM:ss')}'`; } -function mapFilter(column: string, operator: string, name: string, type = 'String') { - switch (operator) { +function mapFilter(column: string, filter: string, name: string, type: string = 'String') { + switch (filter) { case OPERATORS.equals: return `${column} = {${name}:${type}}`; case OPERATORS.notEquals: return `${column} != {${name}:${type}}`; + case OPERATORS.contains: + return `positionCaseInsensitive(${column}, {${name}:${type}}) > 0`; default: return ''; } @@ -75,11 +77,11 @@ function mapFilter(column: string, operator: string, name: string, type = 'Strin function getFilterQuery(filters: QueryFilters = {}, options: QueryOptions = {}) { const query = Object.keys(filters).reduce((arr, name) => { const value = filters[name]; - const operator = value?.filter ?? OPERATORS.equals; - const column = FILTER_COLUMNS[name] ?? options?.columns?.[name]; + const filter = value?.filter ?? OPERATORS.equals; + const column = value?.column ?? FILTER_COLUMNS[name] ?? options?.columns?.[name]; - if (value !== undefined && column) { - arr.push(`and ${mapFilter(column, operator, name)}`); + if (value !== undefined && column !== undefined) { + arr.push(`and ${mapFilter(column, filter, name)}`); if (name === 'referrer') { arr.push('and referrer_domain != {websiteDomain:String}'); diff --git a/src/lib/prisma.ts b/src/lib/prisma.ts index 3831d836..d7401c70 100644 --- a/src/lib/prisma.ts +++ b/src/lib/prisma.ts @@ -92,12 +92,14 @@ function getTimestampDiffQuery(field1: string, field2: string): string { } } -function mapFilter(column: string, operator: string, name: string, type = 'varchar') { - switch (operator) { +function mapFilter(column: string, filter: string, name: string, type = 'varchar') { + switch (filter) { case OPERATORS.equals: return `${column} = {{${name}::${type}}}`; case OPERATORS.notEquals: return `${column} != {{${name}::${type}}}`; + case OPERATORS.contains: + return `${column} like {{${name}::${type}}}`; default: return ''; } @@ -106,11 +108,11 @@ function mapFilter(column: string, operator: string, name: string, type = 'varch function getFilterQuery(filters: QueryFilters = {}, options: QueryOptions = {}): string { const query = Object.keys(filters).reduce((arr, name) => { const value = filters[name]; - const operator = value?.filter ?? OPERATORS.equals; - const column = FILTER_COLUMNS[name] ?? options?.columns?.[name]; + const filter = value?.filter ?? OPERATORS.equals; + const column = value?.column ?? FILTER_COLUMNS[name] ?? options?.columns?.[name]; - if (value !== undefined && column) { - arr.push(`and ${mapFilter(column, operator, name)}`); + if (value !== undefined && column !== undefined) { + arr.push(`and ${mapFilter(column, filter, name)}`); if (name === 'referrer') { arr.push( diff --git a/src/lib/types.ts b/src/lib/types.ts index ecba0a6f..3470abe8 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -213,6 +213,7 @@ export interface QueryFilters { city?: string; language?: string; event?: string; + search?: string; } export interface QueryOptions { diff --git a/src/pages/api/websites/[websiteId]/metrics.ts b/src/pages/api/websites/[websiteId]/metrics.ts index 1062960c..eb636170 100644 --- a/src/pages/api/websites/[websiteId]/metrics.ts +++ b/src/pages/api/websites/[websiteId]/metrics.ts @@ -26,6 +26,8 @@ export interface WebsiteMetricsRequestQuery { language?: string; event?: string; limit?: number; + offset?: number; + search?: string; } const schema = { @@ -47,6 +49,8 @@ const schema = { language: yup.string(), event: yup.string(), limit: yup.number(), + offset: yup.number(), + search: yup.string(), }), }; @@ -74,6 +78,8 @@ export default async ( language, event, limit, + offset, + search, } = req.query; if (req.method === 'GET') { @@ -98,12 +104,19 @@ export default async ( city, language, event, + search, }; const column = FILTER_COLUMNS[type] || type; if (SESSION_COLUMNS.includes(type)) { - const data = await getSessionMetrics(websiteId, column, filters, limit); + const data = await getSessionMetrics( + websiteId, + column, + { ...filters, search }, + limit, + offset, + ); if (type === 'language') { const combined = {}; @@ -125,7 +138,13 @@ export default async ( } if (EVENT_COLUMNS.includes(type)) { - const data = await getPageviewMetrics(websiteId, column, filters, limit); + const data = await getPageviewMetrics( + websiteId, + column, + { ...filters, search }, + limit, + offset, + ); return ok(res, data); } diff --git a/src/queries/analytics/pageviews/getPageviewMetrics.ts b/src/queries/analytics/pageviews/getPageviewMetrics.ts index 0bf931fd..8673dbe6 100644 --- a/src/queries/analytics/pageviews/getPageviewMetrics.ts +++ b/src/queries/analytics/pageviews/getPageviewMetrics.ts @@ -1,11 +1,17 @@ import prisma from 'lib/prisma'; import clickhouse from 'lib/clickhouse'; import { runQuery, CLICKHOUSE, PRISMA } from 'lib/db'; -import { EVENT_TYPE, SESSION_COLUMNS } from 'lib/constants'; +import { EVENT_TYPE, SESSION_COLUMNS, OPERATORS } from 'lib/constants'; import { QueryFilters } from 'lib/types'; export async function getPageviewMetrics( - ...args: [websiteId: string, columns: string, filters: QueryFilters, limit?: number] + ...args: [ + websiteId: string, + column: string, + filters: QueryFilters, + limit?: number, + offset?: number, + ] ) { return runQuery({ [PRISMA]: () => relationalQuery(...args), @@ -18,6 +24,7 @@ async function relationalQuery( column: string, filters: QueryFilters, limit: number = 500, + offset: number = 0, ) { const { rawQuery, parseFilters } = prisma; const { filterQuery, joinSession, params } = await parseFilters( @@ -48,6 +55,7 @@ async function relationalQuery( group by 1 order by 2 desc limit ${limit} + offset ${offset} `, params, ); @@ -58,10 +66,19 @@ async function clickhouseQuery( column: string, filters: QueryFilters, limit: number = 500, + offset: number = 0, ): Promise<{ x: string; y: number }[]> { const { rawQuery, parseFilters } = clickhouse; const { filterQuery, params } = await parseFilters(websiteId, { ...filters, + ...(filters.search && { + [column]: { + value: filters.search, + filter: OPERATORS.contains, + column, + name: column, + }, + }), eventType: column === 'event_name' ? EVENT_TYPE.customEvent : EVENT_TYPE.pageView, }); @@ -82,6 +99,7 @@ async function clickhouseQuery( group by x order by y desc limit ${limit} + offset ${offset} `, params, ).then(a => { diff --git a/src/queries/analytics/sessions/getSessionMetrics.ts b/src/queries/analytics/sessions/getSessionMetrics.ts index be414b1c..77ccb410 100644 --- a/src/queries/analytics/sessions/getSessionMetrics.ts +++ b/src/queries/analytics/sessions/getSessionMetrics.ts @@ -5,7 +5,13 @@ import { EVENT_TYPE, SESSION_COLUMNS } from 'lib/constants'; import { QueryFilters } from 'lib/types'; export async function getSessionMetrics( - ...args: [websiteId: string, column: string, filters: QueryFilters, limit?: number] + ...args: [ + websiteId: string, + column: string, + filters: QueryFilters, + limit?: number, + offset?: number, + ] ) { return runQuery({ [PRISMA]: () => relationalQuery(...args), @@ -18,6 +24,7 @@ async function relationalQuery( column: string, filters: QueryFilters, limit: number = 500, + offset: number = 0, ) { const { parseFilters, rawQuery } = prisma; const { filterQuery, joinSession, params } = await parseFilters( @@ -47,7 +54,9 @@ async function relationalQuery( group by 1 ${includeCountry ? ', 3' : ''} order by 2 desc - limit ${limit}`, + limit ${limit} + offset ${offset} + `, params, ); } @@ -57,6 +66,7 @@ async function clickhouseQuery( column: string, filters: QueryFilters, limit: number = 500, + offset: number = 0, ): Promise<{ x: string; y: number }[]> { const { parseFilters, rawQuery } = clickhouse; const { filterQuery, params } = await parseFilters(websiteId, { @@ -80,6 +90,7 @@ async function clickhouseQuery( ${includeCountry ? ', country' : ''} order by y desc limit ${limit} + offset ${offset} `, params, ).then(a => {