Segment editing.

This commit is contained in:
Mike Cao 2025-07-31 21:32:22 -07:00
parent fba7e12c36
commit 2dbcf63eeb
22 changed files with 306 additions and 42 deletions

View file

@ -3,6 +3,7 @@ import { WebsiteFilterButton } from '@/app/(main)/websites/[websiteId]/WebsiteFi
import { WebsiteDateFilter } from '@/components/input/WebsiteDateFilter'; import { WebsiteDateFilter } from '@/components/input/WebsiteDateFilter';
import { FilterBar } from '@/components/input/FilterBar'; import { FilterBar } from '@/components/input/FilterBar';
import { WebsiteMonthSelect } from '@/components/input/WebsiteMonthSelect'; import { WebsiteMonthSelect } from '@/components/input/WebsiteMonthSelect';
import { ExportButton } from '@/components/input/ExportButton';
export function WebsiteControls({ export function WebsiteControls({
websiteId, websiteId,
@ -10,21 +11,24 @@ export function WebsiteControls({
allowDateFilter = true, allowDateFilter = true,
allowMonthFilter, allowMonthFilter,
allowCompare, allowCompare,
allowDownload = false,
}: { }: {
websiteId: string; websiteId: string;
allowFilter?: boolean; allowFilter?: boolean;
allowCompare?: boolean; allowCompare?: boolean;
allowDateFilter?: boolean; allowDateFilter?: boolean;
allowMonthFilter?: boolean; allowMonthFilter?: boolean;
allowDownload?: boolean;
}) { }) {
return ( return (
<Column gap> <Column gap>
<Row alignItems="center" justifyContent="space-between" gap="3"> <Row alignItems="center" justifyContent="space-between" gap="3">
{allowFilter ? <WebsiteFilterButton websiteId={websiteId} /> : <div />} {allowFilter ? <WebsiteFilterButton websiteId={websiteId} /> : <div />}
{allowDateFilter && <WebsiteDateFilter websiteId={websiteId} allowCompare={allowCompare} />} {allowDateFilter && <WebsiteDateFilter websiteId={websiteId} allowCompare={allowCompare} />}
{allowDownload && <ExportButton websiteId={websiteId} />}
{allowMonthFilter && <WebsiteMonthSelect websiteId={websiteId} />} {allowMonthFilter && <WebsiteMonthSelect websiteId={websiteId} />}
</Row> </Row>
{allowFilter && <FilterBar />} {allowFilter && <FilterBar websiteId={websiteId} />}
</Column> </Column>
); );
} }

View file

@ -2,6 +2,7 @@ import { Button, Icon, DialogTrigger, Dialog, Modal, Text } from '@umami/react-z
import { ListFilter } from '@/components/icons'; import { ListFilter } from '@/components/icons';
import { FilterEditForm } from '@/components/common/FilterEditForm'; import { FilterEditForm } from '@/components/common/FilterEditForm';
import { useMessages, useNavigation, useFilters } from '@/components/hooks'; import { useMessages, useNavigation, useFilters } from '@/components/hooks';
import { filtersArrayToObject } from '@/lib/params';
export function WebsiteFilterButton({ export function WebsiteFilterButton({
websiteId, websiteId,
@ -21,16 +22,7 @@ export function WebsiteFilterButton({
const { filters } = useFilters(); const { filters } = useFilters();
const handleChange = ({ filters, segment }) => { const handleChange = ({ filters, segment }) => {
const params = filters.reduce( const params = filtersArrayToObject(filters);
(obj: { [x: string]: string }, filter: { name: any; operator: any; value: any }) => {
const { name, operator, value } = filter;
obj[name] = `${operator}.${value}`;
return obj;
},
{},
);
const url = replaceParams({ ...params, segment }); const url = replaceParams({ ...params, segment });

View file

@ -0,0 +1,26 @@
import { Button, DialogTrigger, Modal, Text, Icon, Dialog } from '@umami/react-zen';
import { useMessages } from '@/components/hooks';
import { Plus } from '@/components/icons';
import { SegmentEditForm } from '@/app/(main)/websites/[websiteId]/segments/SegmentEditForm';
export function SegmentAddButton({ websiteId }: { websiteId: string }) {
const { formatMessage, labels } = useMessages();
return (
<DialogTrigger>
<Button variant="primary">
<Icon>
<Plus />
</Icon>
<Text>{formatMessage(labels.segment)}</Text>
</Button>
<Modal>
<Dialog title={formatMessage(labels.segment)} style={{ width: 800 }}>
{({ close }) => {
return <SegmentEditForm websiteId={websiteId} onClose={close} />;
}}
</Dialog>
</Modal>
</DialogTrigger>
);
}

View file

@ -0,0 +1,79 @@
import {
Button,
Form,
FormButtons,
FormField,
FormSubmitButton,
TextField,
Label,
} from '@umami/react-zen';
import { subMonths, endOfDay } from 'date-fns';
import { FieldFilters } from '@/components/input/FieldFilters';
import { useState } from 'react';
import { useApi, useMessages, useModified } from '@/components/hooks';
import { filtersArrayToObject } from '@/lib/params';
export function SegmentEditForm({
websiteId,
filters = [],
onSave,
onClose,
}: {
websiteId: string;
filters?: any[];
onSave?: () => void;
onClose?: () => void;
}) {
const { formatMessage, labels } = useMessages();
const [currentFilters, setCurrentFilters] = useState(filters);
const { touch } = useModified();
const startDate = subMonths(endOfDay(new Date()), 6);
const endDate = endOfDay(new Date());
const { post, useMutation } = useApi();
const { mutate, error, isPending } = useMutation({
mutationFn: (data: any) =>
post(`/websites/${websiteId}/segments`, { ...data, type: 'segment' }),
});
const handleSubmit = async (data: any) => {
mutate(
{ ...data, parameters: filtersArrayToObject(currentFilters) },
{
onSuccess: async () => {
touch('segments');
onSave?.();
onClose?.();
},
},
);
};
return (
<Form error={error} onSubmit={handleSubmit}>
<FormField
name="name"
label={formatMessage(labels.name)}
rules={{ required: formatMessage(labels.required) }}
>
<TextField />
</FormField>
<Label>{formatMessage(labels.filters)}</Label>
<FieldFilters
websiteId={websiteId}
filters={currentFilters}
startDate={startDate}
endDate={endDate}
onSave={setCurrentFilters}
/>
<FormButtons>
<Button isDisabled={isPending} onPress={onClose}>
{formatMessage(labels.cancel)}
</Button>
<FormSubmitButton variant="primary" data-test="button-submit" isDisabled={isPending}>
{formatMessage(labels.save)}
</FormSubmitButton>
</FormButtons>
</Form>
);
}

View file

@ -0,0 +1,24 @@
import { SegmentAddButton } from './SegmentAddButton';
import { useWebsiteSegmentsQuery } from '@/components/hooks';
import { SegmentsTable } from './SegmentsTable';
import { DataGrid } from '@/components/common/DataGrid';
export function SegmentsDataTable({ websiteId }: { websiteId?: string }) {
const query = useWebsiteSegmentsQuery(websiteId, { type: 'segment' });
const renderActions = () => {
return <SegmentAddButton websiteId={websiteId} />;
};
return (
<DataGrid
query={query}
allowSearch={true}
autoFocus={false}
allowPaging={true}
renderActions={renderActions}
>
{({ data }) => <SegmentsTable data={data} />}
</DataGrid>
);
}

View file

@ -0,0 +1,16 @@
'use client';
import { Column } from '@umami/react-zen';
import { SegmentsDataTable } from './SegmentsDataTable';
import { WebsiteControls } from '@/app/(main)/websites/[websiteId]/WebsiteControls';
import { Panel } from '@/components/common/Panel';
export function SegmentsPage({ websiteId }) {
return (
<Column gap="3">
<WebsiteControls websiteId={websiteId} allowFilter={false} allowDateFilter={false} />
<Panel>
<SegmentsDataTable websiteId={websiteId} />
</Panel>
</Column>
);
}

View file

@ -0,0 +1,49 @@
import { DataTable, DataColumn, Icon, Row, Text, MenuItem } from '@umami/react-zen';
import { useMessages } from '@/components/hooks';
import { Empty } from '@/components/common/Empty';
import { Edit, Trash } from '@/components/icons';
import { DateDistance } from '@/components/common/DateDistance';
import { MenuButton } from '@/components/input/MenuButton';
export function SegmentsTable({ data = [] }) {
const { formatMessage, labels } = useMessages();
if (data.length === 0) {
return <Empty />;
}
return (
<DataTable data={data}>
<DataColumn id="name" label={formatMessage(labels.name)} />
<DataColumn id="created" label={formatMessage(labels.created)}>
{(row: any) => <DateDistance date={new Date(row.createdAt)} />}
</DataColumn>
<DataColumn id="action" align="end" width="100px">
{(row: any) => {
const { id } = row;
return (
<MenuButton>
<MenuItem href={`/admin/users/${id}`} data-test="link-button-edit">
<Row alignItems="center" gap>
<Icon>
<Edit />
</Icon>
<Text>{formatMessage(labels.edit)}</Text>
</Row>
</MenuItem>
<MenuItem id="delete" onAction={null} data-test="link-button-delete">
<Row alignItems="center" gap>
<Icon>
<Trash />
</Icon>
<Text>{formatMessage(labels.delete)}</Text>
</Row>
</MenuItem>
</MenuButton>
);
}}
</DataColumn>
</DataTable>
);
}

View file

@ -0,0 +1,12 @@
import { Metadata } from 'next';
import { SegmentsPage } from './SegmentsPage';
export default async function ({ params }: { params: Promise<{ websiteId: string }> }) {
const { websiteId } = await params;
return <SegmentsPage websiteId={websiteId} />;
}
export const metadata: Metadata = {
title: 'Segments',
};

View file

@ -2,7 +2,7 @@ import { canUpdateWebsite, canViewWebsite } from '@/lib/auth';
import { uuid } from '@/lib/crypto'; import { uuid } from '@/lib/crypto';
import { parseRequest } from '@/lib/request'; import { parseRequest } from '@/lib/request';
import { json, unauthorized } from '@/lib/response'; import { json, unauthorized } from '@/lib/response';
import { segmentTypeParam } from '@/lib/schema'; import { segmentTypeParam, searchParams } from '@/lib/schema';
import { createSegment, getWebsiteSegments } from '@/queries'; import { createSegment, getWebsiteSegments } from '@/queries';
import { z } from 'zod'; import { z } from 'zod';
@ -12,6 +12,7 @@ export async function GET(
) { ) {
const schema = z.object({ const schema = z.object({
type: segmentTypeParam, type: segmentTypeParam,
...searchParams,
}); });
const { auth, query, error } = await parseRequest(request, schema); const { auth, query, error } = await parseRequest(request, schema);

View file

@ -1,6 +1,6 @@
import { useState } from 'react'; import { useState } from 'react';
import { Column, Tabs, TabList, Tab, TabPanel, Row, Button } from '@umami/react-zen'; import { Column, Tabs, TabList, Tab, TabPanel, Row, Button } from '@umami/react-zen';
import { useMessages } from '@/components/hooks'; import { useDateRange, useMessages } from '@/components/hooks';
import { FieldFilters } from '@/components/input/FieldFilters'; import { FieldFilters } from '@/components/input/FieldFilters';
import { SegmentFilters } from '@/components/input/SegmentFilters'; import { SegmentFilters } from '@/components/input/SegmentFilters';
@ -23,6 +23,10 @@ export function FilterEditForm({
const [currentFilters, setCurrentFilters] = useState(filters); const [currentFilters, setCurrentFilters] = useState(filters);
const [currentSegment, setCurrentSegment] = useState(segmentId); const [currentSegment, setCurrentSegment] = useState(segmentId);
const {
dateRange: { startDate, endDate },
} = useDateRange(websiteId);
const handleReset = () => { const handleReset = () => {
setCurrentFilters([]); setCurrentFilters([]);
setCurrentSegment(null); setCurrentSegment(null);
@ -45,7 +49,13 @@ export function FilterEditForm({
<Tab id="segments">{formatMessage(labels.segments)}</Tab> <Tab id="segments">{formatMessage(labels.segments)}</Tab>
</TabList> </TabList>
<TabPanel id="fields"> <TabPanel id="fields">
<FieldFilters websiteId={websiteId} filters={currentFilters} onSave={setCurrentFilters} /> <FieldFilters
websiteId={websiteId}
filters={currentFilters}
startDate={startDate}
endDate={endDate}
onSave={setCurrentFilters}
/>
</TabPanel> </TabPanel>
<TabPanel id="segments"> <TabPanel id="segments">
<SegmentFilters <SegmentFilters

View file

@ -26,7 +26,8 @@ export * from './queries/useTeamMembersQuery';
export * from './queries/useUserQuery'; export * from './queries/useUserQuery';
export * from './queries/useUsersQuery'; export * from './queries/useUsersQuery';
export * from './queries/useWebsiteQuery'; export * from './queries/useWebsiteQuery';
export * from './queries/useWebsiteSegementsQuery'; export * from './queries/useWebsiteSegmentQuery';
export * from './queries/useWebsiteSegmentsQuery';
export * from './queries/useWebsitesQuery'; export * from './queries/useWebsitesQuery';
export * from './queries/useWebsiteEventsQuery'; export * from './queries/useWebsiteEventsQuery';
export * from './queries/useWebsiteEventsSeriesQuery'; export * from './queries/useWebsiteEventsSeriesQuery';

View file

@ -0,0 +1,21 @@
import { useApi } from '../useApi';
import { useModified } from '@/components/hooks';
import { keepPreviousData } from '@tanstack/react-query';
import { ReactQueryOptions } from '@/lib/types';
export function useWebsiteSegmentQuery(
websiteId: string,
segmentId: string,
options?: ReactQueryOptions<any>,
) {
const { get, useQuery } = useApi();
const { modified } = useModified(`segments`);
return useQuery({
queryKey: ['website:segments', { websiteId, segmentId, modified }],
queryFn: () => get(`/websites/${websiteId}/segments/${segmentId}`),
enabled: !!(websiteId && segmentId),
placeholderData: keepPreviousData,
...options,
});
}

View file

@ -2,6 +2,7 @@ import { useApi } from '../useApi';
import { useModified } from '@/components/hooks'; import { useModified } from '@/components/hooks';
import { keepPreviousData } from '@tanstack/react-query'; import { keepPreviousData } from '@tanstack/react-query';
import { ReactQueryOptions } from '@/lib/types'; import { ReactQueryOptions } from '@/lib/types';
import { useFilterParameters } from '@/components/hooks/useFilterParameters';
export function useWebsiteSegmentsQuery( export function useWebsiteSegmentsQuery(
websiteId: string, websiteId: string,
@ -9,11 +10,12 @@ export function useWebsiteSegmentsQuery(
options?: ReactQueryOptions<any>, options?: ReactQueryOptions<any>,
) { ) {
const { get, useQuery } = useApi(); const { get, useQuery } = useApi();
const { modified } = useModified(`website:${websiteId}`); const { modified } = useModified(`segments`);
const filters = useFilterParameters();
return useQuery({ return useQuery({
queryKey: ['website:segments', { websiteId, modified, ...params }], queryKey: ['website:segments', { websiteId, modified, ...filters, ...params }],
queryFn: () => get(`/websites/${websiteId}/segments`, { ...params }), queryFn: () => get(`/websites/${websiteId}/segments`, { ...filters, ...params }),
enabled: !!websiteId, enabled: !!websiteId,
placeholderData: keepPreviousData, placeholderData: keepPreviousData,
...options, ...options,

View file

@ -1,21 +1,26 @@
import { Key } from 'react'; import { Key } from 'react';
import { Grid, Column, List, ListItem } from '@umami/react-zen'; import { Grid, Column, List, ListItem } from '@umami/react-zen';
import { useDateRange, useFields, useMessages } from '@/components/hooks'; import { useFields, useMessages } from '@/components/hooks';
import { FilterRecord } from '@/components/common/FilterRecord'; import { FilterRecord } from '@/components/common/FilterRecord';
import { Empty } from '@/components/common/Empty'; import { Empty } from '@/components/common/Empty';
export interface FieldFiltersProps { export interface FieldFiltersProps {
websiteId: string; websiteId: string;
filters: { name: string; operator: string; value: string }[]; filters: { name: string; operator: string; value: string }[];
startDate: Date;
endDate: Date;
onSave?: (data: any) => void; onSave?: (data: any) => void;
} }
export function FieldFilters({ websiteId, filters, onSave }: FieldFiltersProps) { export function FieldFilters({
websiteId,
filters,
startDate,
endDate,
onSave,
}: FieldFiltersProps) {
const { formatMessage, messages } = useMessages(); const { formatMessage, messages } = useMessages();
const { fields } = useFields(); const { fields } = useFields();
const {
dateRange: { startDate, endDate },
} = useDateRange(websiteId);
const updateFilter = (name: string, props: Record<string, any>) => { const updateFilter = (name: string, props: Record<string, any>) => {
onSave(filters.map(filter => (filter.name === name ? { ...filter, ...props } : filter))); onSave(filters.map(filter => (filter.name === name ? { ...filter, ...props } : filter)));

View file

@ -1,9 +1,15 @@
import { Button, Icon, Text, Row, TooltipTrigger, Tooltip } from '@umami/react-zen'; import { Button, Icon, Text, Row, TooltipTrigger, Tooltip } from '@umami/react-zen';
import { useNavigation, useMessages, useFormat, useFilters } from '@/components/hooks'; import {
useNavigation,
useMessages,
useFormat,
useFilters,
useWebsiteSegmentQuery,
} from '@/components/hooks';
import { Close } from '@/components/icons'; import { Close } from '@/components/icons';
import { isSearchOperator } from '@/lib/params'; import { isSearchOperator } from '@/lib/params';
export function FilterBar() { export function FilterBar({ websiteId }: { websiteId: string }) {
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
const { formatValue } = useFormat(); const { formatValue } = useFormat();
const { const {
@ -13,6 +19,7 @@ export function FilterBar() {
query: { segment }, query: { segment },
} = useNavigation(); } = useNavigation();
const { filters, operatorLabels } = useFilters(); const { filters, operatorLabels } = useFilters();
const { data, isLoading } = useWebsiteSegmentQuery(websiteId, segment);
const handleCloseFilter = (param: string) => { const handleCloseFilter = (param: string) => {
router.push(updateParams({ [param]: undefined })); router.push(updateParams({ [param]: undefined }));
@ -33,11 +40,11 @@ export function FilterBar() {
return ( return (
<Row gap alignItems="center" justifyContent="space-between" padding="2" backgroundColor="3"> <Row gap alignItems="center" justifyContent="space-between" padding="2" backgroundColor="3">
<Row alignItems="center" gap="2" wrap="wrap"> <Row alignItems="center" gap="2" wrap="wrap">
{segment && ( {segment && !isLoading && (
<FilterItem <FilterItem
name="segment" name="segment"
label={formatMessage(labels.segment)} label={formatMessage(labels.segment)}
value={segment} value={data?.name || segment}
operator={operatorLabels.eq} operator={operatorLabels.eq}
onRemove={handleSegmentRemove} onRemove={handleSegmentRemove}
/> />

View file

@ -19,7 +19,7 @@ export function SegmentFilters({ websiteId, segmentId, onSave }: SegmentFiltersP
<Column height="400px" gap> <Column height="400px" gap>
<LoadingPanel data={data} isLoading={isLoading} overflowY="auto"> <LoadingPanel data={data} isLoading={isLoading} overflowY="auto">
<List selectionMode="single" value={[segmentId]} onChange={id => handleChange(id[0])}> <List selectionMode="single" value={[segmentId]} onChange={id => handleChange(id[0])}>
{data?.map(item => { {data?.data?.map(item => {
return ( return (
<ListItem key={item.id} id={item.id}> <ListItem key={item.id} id={item.id}>
{item.name} {item.name}

View file

@ -12,7 +12,6 @@ import { isAfter } from 'date-fns';
import { Chevron, Close, Compare } from '@/components/icons'; import { Chevron, Close, Compare } from '@/components/icons';
import { useDateRange, useMessages, useNavigation } from '@/components/hooks'; import { useDateRange, useMessages, useNavigation } from '@/components/hooks';
import { DateFilter } from './DateFilter'; import { DateFilter } from './DateFilter';
import { ExportButton } from '@/components/input/ExportButton';
export function WebsiteDateFilter({ export function WebsiteDateFilter({
websiteId, websiteId,
@ -102,7 +101,6 @@ export function WebsiteDateFilter({
<Tooltip>{formatMessage(compare ? labels.cancel : labels.compareDates)}</Tooltip> <Tooltip>{formatMessage(compare ? labels.cancel : labels.compareDates)}</Tooltip>
</TooltipTrigger> </TooltipTrigger>
)} )}
<ExportButton websiteId={websiteId} />
</Row> </Row>
); );
} }

View file

@ -3,7 +3,7 @@ import { formatInTimeZone } from 'date-fns-tz';
import debug from 'debug'; import debug from 'debug';
import { CLICKHOUSE } from '@/lib/db'; import { CLICKHOUSE } from '@/lib/db';
import { DEFAULT_PAGE_SIZE, OPERATORS } from './constants'; import { DEFAULT_PAGE_SIZE, OPERATORS } from './constants';
import { filtersToArray } from './params'; import { filtersObjectToArray } from './params';
import { QueryFilters, QueryOptions } from './types'; import { QueryFilters, QueryOptions } from './types';
export const CLICKHOUSE_DATE_FORMATS = { export const CLICKHOUSE_DATE_FORMATS = {
@ -88,7 +88,7 @@ function mapFilter(column: string, operator: string, name: string, type: string
} }
function getFilterQuery(filters: Record<string, any>, options: QueryOptions = {}) { function getFilterQuery(filters: Record<string, any>, options: QueryOptions = {}) {
const query = filtersToArray(filters, options).reduce((arr, { name, column, operator }) => { const query = filtersObjectToArray(filters, options).reduce((arr, { name, column, operator }) => {
if (column) { if (column) {
arr.push(`and ${mapFilter(column, operator, name)}`); arr.push(`and ${mapFilter(column, operator, name)}`);
@ -144,7 +144,7 @@ function getDateQuery(filters: Record<string, any>) {
function getQueryParams(filters: Record<string, any>) { function getQueryParams(filters: Record<string, any>) {
return { return {
...filters, ...filters,
...filtersToArray(filters).reduce((obj, { name, value }) => { ...filtersObjectToArray(filters).reduce((obj, { name, value }) => {
if (name && value !== undefined) { if (name && value !== undefined) {
obj[name] = value; obj[name] = value;
} }

View file

@ -1,7 +1,7 @@
import { FILTER_COLUMNS, OPERATORS } from '@/lib/constants'; import { FILTER_COLUMNS, OPERATORS } from '@/lib/constants';
import { QueryFilters, QueryOptions } from '@/lib/types'; import { Filter, QueryFilters, QueryOptions } from '@/lib/types';
export function parseParameterValue(param: any) { export function parseFilterValue(param: any) {
if (typeof param === 'string') { if (typeof param === 'string') {
const [, operator, value] = param.match(/^([a-z]+)\.(.*)/) || []; const [, operator, value] = param.match(/^([a-z]+)\.(.*)/) || [];
@ -18,7 +18,7 @@ export function isSearchOperator(operator: any) {
return [OPERATORS.contains, OPERATORS.doesNotContain].includes(operator); return [OPERATORS.contains, OPERATORS.doesNotContain].includes(operator);
} }
export function filtersToArray(filters: QueryFilters, options: QueryOptions = {}) { export function filtersObjectToArray(filters: QueryFilters, options: QueryOptions = {}) {
return Object.keys(filters).reduce((arr, key) => { return Object.keys(filters).reduce((arr, key) => {
const filter = filters[key]; const filter = filters[key];
@ -30,7 +30,7 @@ export function filtersToArray(filters: QueryFilters, options: QueryOptions = {}
return arr.concat({ ...filter, column: options?.columns?.[key] ?? FILTER_COLUMNS[key] }); return arr.concat({ ...filter, column: options?.columns?.[key] ?? FILTER_COLUMNS[key] });
} }
const { operator, value } = parseParameterValue(filter); const { operator, value } = parseFilterValue(filter);
return arr.concat({ return arr.concat({
name: key, name: key,
@ -41,3 +41,13 @@ export function filtersToArray(filters: QueryFilters, options: QueryOptions = {}
}); });
}, []); }, []);
} }
export function filtersArrayToObject(filters: Filter[]) {
return filters.reduce((obj, filter: Filter) => {
const { name, operator, value } = filter;
obj[name] = `${operator}.${value}`;
return obj;
}, {});
}

View file

@ -4,7 +4,7 @@ import { PrismaPg } from '@prisma/adapter-pg';
import { readReplicas } from '@prisma/extension-read-replicas'; import { readReplicas } from '@prisma/extension-read-replicas';
import { SESSION_COLUMNS, OPERATORS, DEFAULT_PAGE_SIZE } from './constants'; import { SESSION_COLUMNS, OPERATORS, DEFAULT_PAGE_SIZE } from './constants';
import { QueryOptions, QueryFilters } from './types'; import { QueryOptions, QueryFilters } from './types';
import { filtersToArray } from './params'; import { filtersObjectToArray } from './params';
const log = debug('umami:prisma'); const log = debug('umami:prisma');
@ -95,7 +95,7 @@ function mapCohortFilter(column: string, operator: string, value: string) {
} }
function getFilterQuery(filters: Record<string, any>, options: QueryOptions = {}): string { function getFilterQuery(filters: Record<string, any>, options: QueryOptions = {}): string {
const query = filtersToArray(filters, options).reduce( const query = filtersObjectToArray(filters, options).reduce(
(arr, { name, column, operator, prefix = '' }) => { (arr, { name, column, operator, prefix = '' }) => {
if (column) { if (column) {
arr.push(`and ${mapFilter(`${prefix}${column}`, operator, name)}`); arr.push(`and ${mapFilter(`${prefix}${column}`, operator, name)}`);
@ -116,7 +116,7 @@ function getFilterQuery(filters: Record<string, any>, options: QueryOptions = {}
} }
function getCohortQuery(websiteId: string, filters: QueryFilters = {}, options: QueryOptions = {}) { function getCohortQuery(websiteId: string, filters: QueryFilters = {}, options: QueryOptions = {}) {
const query = filtersToArray(filters, options).reduce( const query = filtersObjectToArray(filters, options).reduce(
(arr, { name, column, operator, value }) => { (arr, { name, column, operator, value }) => {
if (column) { if (column) {
arr.push( arr.push(
@ -169,7 +169,7 @@ function getDateQuery(filters: Record<string, any>) {
function getQueryParams(filters: Record<string, any>) { function getQueryParams(filters: Record<string, any>) {
return { return {
...filters, ...filters,
...filtersToArray(filters).reduce((obj, { name, operator, value }) => { ...filtersObjectToArray(filters).reduce((obj, { name, operator, value }) => {
obj[name] = [OPERATORS.contains, OPERATORS.doesNotContain].includes(operator) obj[name] = [OPERATORS.contains, OPERATORS.doesNotContain].includes(operator)
? `%${value}%` ? `%${value}%`
: value; : value;

View file

@ -24,6 +24,13 @@ export interface Auth {
}; };
} }
export interface Filter {
name: string;
operator: string;
value: string;
type?: string;
}
export interface DateRange { export interface DateRange {
startDate: Date; startDate: Date;
endDate: Date; endDate: Date;

View file

@ -20,7 +20,7 @@ export async function getWebsiteSegment(websiteId: string, segmentId: string): P
} }
export async function getWebsiteSegments(websiteId: string, type: string): Promise<Segment[]> { export async function getWebsiteSegments(websiteId: string, type: string): Promise<Segment[]> {
return prisma.client.Segment.findMany({ return prisma.pagedQuery('segment', {
where: { websiteId, type }, where: { websiteId, type },
}); });
} }