Fixed editing and navigation issues.

This commit is contained in:
Mike Cao 2025-07-13 00:37:43 -07:00
parent bf6c9395c6
commit 8c26e310f7
52 changed files with 118 additions and 122 deletions

View file

@ -199,18 +199,6 @@ export default {
typescript: {
ignoreBuildErrors: true,
},
functions: {
'app/api/**/*.js': {
maxDuration: 30,
},
},
outputFileTracing: {
include: [
'src/generated/prisma/**/*',
'node_modules/@prisma/client/**/*',
'node_modules/.prisma/client/**/*',
],
},
async headers() {
return headers;
},

View file

@ -4,7 +4,7 @@ import Script from 'next/script';
import { usePathname } from 'next/navigation';
import { UpdateNotice } from './UpdateNotice';
import { SideNav } from '@/app/(main)/SideNav';
import { MenuBar } from '@/app/(main)/MenuBar';
import { TopNav } from '@/app/(main)/TopNav';
import { useLoginQuery, useConfig } from '@/components/hooks';
export function App({ children }) {
@ -35,7 +35,7 @@ export function App({ children }) {
<SideNav />
</Column>
<Row gridColumn="2 / 3" gridRow="1 / 2">
<MenuBar />
<TopNav />
</Row>
<Column
gridColumn="2 / 3"

View file

@ -6,11 +6,10 @@ import { WebsiteSelect } from '@/components/input/WebsiteSelect';
import { PanelLeft, Slash } from '@/components/icons';
import { useNavigation, useGlobalState } from '@/components/hooks';
export function MenuBar() {
export function TopNav() {
const [isCollapsed, setCollapsed] = useGlobalState('sidenav-collapsed');
const { teamId, websiteId } = useNavigation();
const handleSelect = () => {};
const { teamId, websiteId, pathname } = useNavigation();
const isSettings = pathname.includes('/settings');
return (
<Row
@ -30,17 +29,12 @@ export function MenuBar() {
</Button>
<Row alignItems="center" gap="1">
<TeamsButton />
{websiteId && (
{websiteId && !isSettings && (
<>
<Icon strokeColor="7" rotate={-25}>
<Slash />
</Icon>
<WebsiteSelect
variant="quiet"
websiteId={websiteId}
teamId={teamId}
onSelect={handleSelect}
/>
<WebsiteSelect variant="quiet" websiteId={websiteId} teamId={teamId} />
</>
)}
</Row>

View file

@ -21,6 +21,7 @@ export function UserDeleteForm({
mutate(null, {
onSuccess: async () => {
touch('users');
touch(`users:${userId}`);
onSave?.();
onClose?.();
},
@ -35,9 +36,7 @@ export function UserDeleteForm({
confirmLabel={formatMessage(labels.delete)}
isDanger
>
<Row gap="1">
{formatMessage(messages.confirmDelete, { target: <b key={username}>{username}</b> })}
</Row>
<Row gap="1">{formatMessage(messages.confirmDelete, { target: username })}</Row>
</AlertDialog>
);
}

View file

@ -1,15 +1,16 @@
import { useState } from 'react';
import Link from 'next/link';
import { Row, Text, Icon, DataTable, DataColumn, MenuItem, Modal } from '@umami/react-zen';
import { Row, Text, Icon, DataTable, DataColumn, MenuItem, Modal, Dialog } from '@umami/react-zen';
import { Trash, Users } from '@/components/icons';
import { useMessages } from '@/components/hooks';
import { Edit } from '@/components/icons';
import { MenuButton } from '@/components/input/MenuButton';
import { DateDistance } from '@/components/common/DateDistance';
import { WebsiteDeleteForm } from '@/app/(main)/settings/websites/[websiteId]/WebsiteDeleteForm';
export function AdminWebsitesTable({ data = [] }: { data: any[] }) {
const { formatMessage, labels } = useMessages();
const [deleteUser, setDeleteUser] = useState(null);
const [deleteWebsite, setDeleteWebsite] = useState(null);
return (
<>
@ -64,7 +65,7 @@ export function AdminWebsitesTable({ data = [] }: { data: any[] }) {
</MenuItem>
<MenuItem
id="delete"
onAction={() => setDeleteUser(row)}
onAction={() => setDeleteWebsite(id)}
data-test="link-button-delete"
>
<Row alignItems="center" gap>
@ -79,7 +80,11 @@ export function AdminWebsitesTable({ data = [] }: { data: any[] }) {
}}
</DataColumn>
</DataTable>
<Modal isOpen={!!deleteUser}></Modal>
<Modal isOpen={!!deleteWebsite}>
<Dialog style={{ width: 400 }}>
<WebsiteDeleteForm websiteId={deleteWebsite} onClose={() => setDeleteWebsite(null)} />
</Dialog>
</Modal>
</>
);
}

View file

@ -42,7 +42,7 @@ export function SettingsLayout({ children }: { children: ReactNode }) {
<SideMenu items={items} selectedKey={value} />
</Column>
<Column>
<Panel>{children}</Panel>
<Panel minHeight="300px">{children}</Panel>
</Column>
</Grid>
</Column>

View file

@ -1,7 +1,7 @@
import { useContext, useState } from 'react';
import { Column, Tabs, TabList, Tab, TabPanel } from '@umami/react-zen';
import { TeamContext } from '@/app/(main)/teams/[teamId]/TeamProvider';
import { useLoginQuery, useMessages } from '@/components/hooks';
import { useLoginQuery, useMessages, useNavigation } from '@/components/hooks';
import { SectionHeader } from '@/components/common/SectionHeader';
import { ROLES } from '@/lib/constants';
import { Users } from '@/components/icons';
@ -15,7 +15,10 @@ export function TeamDetails({ teamId }: { teamId: string }) {
const team = useContext(TeamContext);
const { formatMessage, labels } = useMessages();
const { user } = useLoginQuery();
const [tab, setTab] = useState('details');
const { query, pathname } = useNavigation();
const [tab, setTab] = useState(query?.tab || 'details');
const isAdmin = pathname.includes('/admin');
const isTeamOwner =
!!team?.teamUser?.find(({ userId, role }) => role === ROLES.teamOwner && userId === user.id) &&
@ -32,7 +35,7 @@ export function TeamDetails({ teamId }: { teamId: string }) {
return (
<Column gap>
<SectionHeader title={team?.name} icon={<Users />}>
{!isTeamOwner && <TeamLeaveButton teamId={team.id} teamName={team.name} />}
{!isTeamOwner && !isAdmin && <TeamLeaveButton teamId={team.id} teamName={team.name} />}
</SectionHeader>
<Tabs selectedKey={tab} onSelectionChange={(value: any) => setTab(value)}>
<TabList>

View file

@ -33,6 +33,10 @@ export function TeamMembersTable({
{allowEdit && (
<DataColumn id="action" align="end">
{(row: any) => {
if (row?.role === ROLES.teamOwner) {
return null;
}
return (
<Row alignItems="center">
<TeamMemberEditButton teamId={teamId} userId={row?.user?.id} role={row?.role} />

View file

@ -57,7 +57,7 @@ export function WebsitesTable({
</MenuItem>
)}
{allowEdit && (
<MenuItem href={renderUrl(`/settings/websites/${websiteId}`)}>
<MenuItem href={`/settings/websites/${websiteId}`}>
<Row alignItems="center" gap>
<Icon data-test="link-button-edit">
<SquarePen />

View file

@ -1,4 +1,4 @@
import { useApi, useMessages } from '@/components/hooks';
import { useApi, useMessages, useModified } from '@/components/hooks';
import { TypeConfirmationForm } from '@/components/common/TypeConfirmationForm';
const CONFIRM_VALUE = 'DELETE';
@ -17,10 +17,13 @@ export function WebsiteDeleteForm({
const { mutate, isPending, error } = useMutation({
mutationFn: () => del(`/websites/${websiteId}`),
});
const { touch } = useModified();
const handleConfirm = async () => {
mutate(null, {
onSuccess: async () => {
touch('websites');
touch(`websites:${websiteId}`);
onSave?.();
onClose?.();
},

View file

@ -73,7 +73,7 @@ export function WebsiteShareForm({ websiteId, shareId, onSave, onClose }: Websit
<Row>
{id && <Button onPress={handleGenerate}>{formatMessage(labels.regenerate)}</Button>}
</Row>
<Row>
<Row alignItems="center" gap>
{onClose && <Button onPress={onClose}>{formatMessage(labels.cancel)}</Button>}
<FormSubmitButton isDisabled={false} isLoading={isPending}>
{formatMessage(labels.save)}

View file

@ -18,7 +18,7 @@ export function WebsitesPage() {
<WebsiteAddButton teamId={teamId} />
</PageHeader>
<Panel>
<WebsitesDataTable teamId={teamId} allowEdit={false} />
<WebsitesDataTable teamId={teamId} />
</Panel>
</Column>
</PageBody>

View file

@ -5,12 +5,11 @@ import { Share, Edit } from '@/components/icons';
import { Favicon } from '@/components/common/Favicon';
import { ActiveUsers } from '@/components/metrics/ActiveUsers';
import { WebsiteShareForm } from '@/app/(main)/settings/websites/[websiteId]/WebsiteShareForm';
import { useMessages, useNavigation } from '@/components/hooks';
import { useMessages } from '@/components/hooks';
import { LinkButton } from '@/components/common/LinkButton';
export function WebsiteHeader() {
const website = useWebsite();
const { renderUrl } = useNavigation();
return (
<PageHeader title={website.name} icon={<Favicon domain={website.domain} />} showBorder={false}>
@ -18,7 +17,7 @@ export function WebsiteHeader() {
<ActiveUsers websiteId={website.id} />
<Row alignItems="center" gap>
<ShareButton websiteId={website.id} shareId={website.shareId} />
<LinkButton href={renderUrl(`/settings/websites/${website.id}`)}>
<LinkButton href={`/settings/websites/${website.id}`}>
<Icon>
<Edit />
</Icon>
@ -42,7 +41,7 @@ const ShareButton = ({ websiteId, shareId }) => {
<Text>Share</Text>
</Button>
<Modal>
<Dialog title={formatMessage(labels.share)} style={{ width: 400 }}>
<Dialog title={formatMessage(labels.share)} style={{ width: 600 }}>
{({ close }) => {
return <WebsiteShareForm websiteId={websiteId} shareId={shareId} onClose={close} />;
}}

View file

@ -21,7 +21,7 @@ export async function GET(
return unauthorized();
}
const filters = getQueryFilters({
const filters = await getQueryFilters({
...query,
websiteId,
startAt: subMinutes(startOfMinute(new Date()), REALTIME_RANGE).getTime(),

View file

@ -18,7 +18,7 @@ export async function POST(request: Request) {
}
const parameters = await setWebsiteDate(websiteId, body.parameters);
const filters = getQueryFilters(body.filters);
const filters = await getQueryFilters(body.filters);
const data = await getAttribution(websiteId, parameters as AttributionParameters, filters);

View file

@ -18,7 +18,7 @@ export async function POST(request: Request) {
}
const parameters = await setWebsiteDate(websiteId, body.parameters);
const filters = getQueryFilters(body.filters);
const filters = await getQueryFilters(body.filters);
const data = await getBreakdown(websiteId, parameters as BreakdownParameters, filters);

View file

@ -18,7 +18,7 @@ export async function POST(request: Request) {
}
const parameters = await setWebsiteDate(websiteId, body.parameters);
const filters = getQueryFilters(body.filters);
const filters = await getQueryFilters(body.filters);
const data = await getFunnel(websiteId, parameters as FunnelParameters, filters);

View file

@ -18,7 +18,7 @@ export async function POST(request: Request) {
}
const parameters = await setWebsiteDate(websiteId, body.parameters);
const filters = getQueryFilters(body.filters);
const filters = await getQueryFilters(body.filters);
const data = await getGoal(websiteId, parameters as GoalParameters, filters);

View file

@ -17,7 +17,7 @@ export async function POST(request: Request) {
return unauthorized();
}
const queryFilters = await setWebsiteDate(websiteId, getQueryFilters(filters));
const queryFilters = await setWebsiteDate(websiteId, await getQueryFilters(filters));
const data = await getJourney(websiteId, parameters, queryFilters);

View file

@ -17,7 +17,7 @@ export async function POST(request: Request) {
return unauthorized();
}
const filters = getQueryFilters(body.filters);
const filters = await getQueryFilters(body.filters);
const parameters = await setWebsiteDate(websiteId, body.parameters);
const data = await getRetention(websiteId, parameters as RetentionParameters, filters);

View file

@ -18,7 +18,7 @@ export async function POST(request: Request) {
}
const parameters = await setWebsiteDate(websiteId, body.parameters);
const filters = getQueryFilters(body.filters);
const filters = await getQueryFilters(body.filters);
const data = await getRevenue(websiteId, parameters as RevenuParameters, filters);

View file

@ -18,7 +18,7 @@ export async function POST(request: Request) {
}
const parameters = await setWebsiteDate(websiteId, body.parameters);
const filters = getQueryFilters(body.filters);
const filters = await getQueryFilters(body.filters);
const data = await getUTM(websiteId, parameters as UTMParameters, filters);

View file

@ -23,7 +23,7 @@ export async function GET(request: Request, { params }: { params: Promise<{ team
return unauthorized('You must be the owner of this team.');
}
const filters = getQueryFilters(query);
const filters = await getQueryFilters(query);
const users = await getTeamUsers(
{

View file

@ -22,7 +22,7 @@ export async function GET(request: Request, { params }: { params: Promise<{ user
}
const { userId } = await params;
const filters = getQueryFilters(query);
const filters = await getQueryFilters(query);
const websites = await getAllUserWebsitesIncludingTeamOwner(userId);

View file

@ -26,7 +26,7 @@ export async function GET(
}
const { event } = query;
const filters = getQueryFilters(query);
const filters = await getQueryFilters(query);
const data = await getEventDataEvents(websiteId, {
...filters,

View file

@ -25,7 +25,7 @@ export async function GET(
return unauthorized();
}
const filters = getQueryFilters(query);
const filters = await getQueryFilters(query);
const data = await getEventDataFields(websiteId, filters);

View file

@ -27,7 +27,7 @@ export async function GET(
}
const { propertyName } = query;
const filters = getQueryFilters(query);
const filters = await getQueryFilters(query);
const data = await getEventDataProperties(websiteId, { ...filters, propertyName });

View file

@ -26,7 +26,7 @@ export async function GET(
return unauthorized();
}
const filters = getQueryFilters(query);
const filters = await getQueryFilters(query);
const data = await getEventDataStats(websiteId, filters);

View file

@ -28,7 +28,7 @@ export async function GET(
}
const { eventName, propertyName } = query;
const filters = getQueryFilters(query);
const filters = await getQueryFilters(query);
const data = await getEventDataValues(websiteId, {
...filters,

View file

@ -27,7 +27,7 @@ export async function GET(
return unauthorized();
}
const filters = getQueryFilters(query);
const filters = await getQueryFilters(query);
const data = await getWebsiteEvents(websiteId, filters);

View file

@ -29,7 +29,7 @@ export async function GET(
return unauthorized();
}
const filters = await setWebsiteDate(websiteId, getQueryFilters(query));
const filters = await setWebsiteDate(websiteId, await getQueryFilters(query));
const data = await getEventMetrics(websiteId, filters);

View file

@ -11,10 +11,10 @@ import {
VIDEO_DOMAINS,
PAID_AD_PARAMS,
} from '@/lib/constants';
import { parseRequest, getQueryFilters, setWebsiteDate } from '@/lib/request';
import { parseRequest, getQueryFilters } from '@/lib/request';
import { json, unauthorized, badRequest } from '@/lib/response';
import { getPageviewMetrics, getSessionMetrics, getChannelMetrics } from '@/queries';
import { filterParams } from '@/lib/schema';
import { dateRangeParams, filterParams, searchParams } from '@/lib/schema';
export async function GET(
request: Request,
@ -22,11 +22,10 @@ export async function GET(
) {
const schema = z.object({
type: z.string(),
startAt: z.coerce.number().int(),
endAt: z.coerce.number().int(),
limit: z.coerce.number().optional(),
offset: z.coerce.number().optional(),
search: z.string().optional(),
...dateRangeParams,
...searchParams,
...filterParams,
});
@ -37,13 +36,13 @@ export async function GET(
}
const { websiteId } = await params;
const { type, limit, offset, search } = query;
if (!(await canViewWebsite(auth, websiteId))) {
return unauthorized();
}
const filters = await setWebsiteDate(websiteId, getQueryFilters(query));
const { type, limit, offset, search } = query;
const filters = await getQueryFilters(query, websiteId);
if (search) {
filters[type] = `c.${search}`;

View file

@ -1,6 +1,6 @@
import { z } from 'zod';
import { canViewWebsite } from '@/lib/auth';
import { getQueryFilters, parseRequest, setWebsiteDate } from '@/lib/request';
import { getQueryFilters, parseRequest } from '@/lib/request';
import { dateRangeParams, filterParams } from '@/lib/schema';
import { getCompareDate } from '@/lib/date';
import { unauthorized, json } from '@/lib/response';
@ -27,7 +27,7 @@ export async function GET(
return unauthorized();
}
const filters = await setWebsiteDate(websiteId, getQueryFilters(query));
const filters = await getQueryFilters(query, websiteId);
const [pageviews, sessions] = await Promise.all([
getPageviewStats(websiteId, filters),

View file

@ -22,7 +22,7 @@ export async function GET(
const { websiteId } = await params;
const { propertyName } = query;
const filters = getQueryFilters(query);
const filters = await getQueryFilters(query);
if (!(await canViewWebsite(auth, websiteId))) {
return unauthorized();

View file

@ -22,7 +22,7 @@ export async function GET(
const { propertyName } = query;
const { websiteId } = await params;
const filters = await setWebsiteDate(websiteId, getQueryFilters(query));
const filters = await setWebsiteDate(websiteId, await getQueryFilters(query));
if (!(await canViewWebsite(auth, websiteId))) {
return unauthorized();

View file

@ -25,7 +25,7 @@ export async function GET(
return unauthorized();
}
const filters = getQueryFilters(query);
const filters = await getQueryFilters(query);
const data = await getSessionActivity(websiteId, sessionId, filters);

View file

@ -27,7 +27,7 @@ export async function GET(
return unauthorized();
}
const filters = await setWebsiteDate(websiteId, getQueryFilters(query));
const filters = await setWebsiteDate(websiteId, await getQueryFilters(query));
const data = await getWebsiteSessions(websiteId, filters);

View file

@ -27,7 +27,7 @@ export async function GET(
return unauthorized();
}
const filters = await setWebsiteDate(websiteId, getQueryFilters(query));
const filters = await setWebsiteDate(websiteId, await getQueryFilters(query));
const metrics = await getWebsiteSessionStats(websiteId, filters);

View file

@ -28,7 +28,7 @@ export async function GET(
return unauthorized();
}
const filters = getQueryFilters(query);
const filters = await getQueryFilters(query);
const data = await getWebsiteSessionsWeekly(websiteId, filters);

View file

@ -1,8 +1,8 @@
import { z } from 'zod';
import { parseRequest, getQueryFilters, setWebsiteDate } from '@/lib/request';
import { parseRequest, getQueryFilters } from '@/lib/request';
import { unauthorized, json } from '@/lib/response';
import { canViewWebsite } from '@/lib/auth';
import { filterParams } from '@/lib/schema';
import { dateRangeParams, filterParams } from '@/lib/schema';
import { getWebsiteStats } from '@/queries';
import { getCompareDate } from '@/lib/date';
@ -11,9 +11,8 @@ export async function GET(
{ params }: { params: Promise<{ websiteId: string }> },
) {
const schema = z.object({
startAt: z.coerce.number().int(),
endAt: z.coerce.number().int(),
compare: z.string().optional(),
...dateRangeParams,
...filterParams,
});
@ -29,7 +28,7 @@ export async function GET(
return unauthorized();
}
const filters = await setWebsiteDate(websiteId, getQueryFilters(query));
const filters = await getQueryFilters(query, websiteId);
const data = await getWebsiteStats(websiteId, filters);

View file

@ -39,7 +39,7 @@ export async function GET(
if (FILTER_GROUPS[type]) {
values = (await getWebsiteSegments(websiteId, type)).map(segment => ({ value: segment.name }));
} else {
const filters = getQueryFilters(query);
const filters = await getQueryFilters(query);
values = await getValues(websiteId, FILTER_COLUMNS[type], filters);
}

View file

@ -42,16 +42,18 @@ export function Pager({ page, pageSize, count, onPageChange }: PagerProps) {
total: maxPage.toLocaleString(),
})}
</Text>
<Button onPress={() => handlePageChange(-1)} isDisabled={firstPage}>
<Icon size="sm" rotate={180}>
<Chevron />
</Icon>
</Button>
<Button onPress={() => handlePageChange(1)} isDisabled={lastPage}>
<Icon size="sm">
<Chevron />
</Icon>
</Button>
<Row gap="1">
<Button variant="outline" onPress={() => handlePageChange(-1)} isDisabled={firstPage}>
<Icon size="sm" rotate={180}>
<Chevron />
</Icon>
</Button>
<Button variant="outline" onPress={() => handlePageChange(1)} isDisabled={lastPage}>
<Icon size="sm">
<Chevron />
</Icon>
</Button>
</Row>
</Row>
</Row>
);

View file

@ -11,8 +11,8 @@ export function useTeamsQuery(params?: Record<string, any>, options?: ReactQuery
queryKey: ['teams:admin', { modified, ...params }],
queryFn: pageParams => {
return get(`/admin/teams`, {
...params,
...pageParams,
...params,
});
},
...options,

View file

@ -17,8 +17,8 @@ export function useUserWebsitesQuery(
queryKey: ['websites', { userId, teamId, modified, ...params }],
queryFn: pageParams => {
return get(teamId ? `/teams/${teamId}/websites` : `/users/${userId || user.id}/websites`, {
...params,
...pageParams,
...params,
});
},
...options,

View file

@ -1,28 +1,24 @@
import { useState } from 'react';
import { Select, SelectProps, ListItem } from '@umami/react-zen';
import { useUserWebsitesQuery, useMessages } from '@/components/hooks';
import { useUserWebsitesQuery, useWebsiteQuery, useNavigation } from '@/components/hooks';
export function WebsiteSelect({
websiteId,
teamId,
variant,
onSelect,
...props
}: {
websiteId?: string;
teamId?: string;
variant?: 'primary' | 'outline' | 'quiet' | 'danger' | 'zero';
onSelect?: (key: any) => void;
} & SelectProps) {
const { formatMessage, labels } = useMessages();
const { router, renderUrl } = useNavigation();
const [search, setSearch] = useState('');
const [selectedId, setSelectedId] = useState(websiteId);
const { data: website } = useWebsiteQuery(websiteId);
const { data, isLoading } = useUserWebsitesQuery({ teamId }, { search, pageSize: 5 });
const handleSelect = (value: any) => {
setSelectedId(value);
onSelect?.(value);
router.push(renderUrl(`/websites/${value}`));
};
const handleSearch = (value: string) => {
@ -33,14 +29,14 @@ export function WebsiteSelect({
<Select
{...props}
items={data?.['data'] || []}
value={selectedId}
placeholder={formatMessage(labels.selectWebsite)}
value={websiteId}
isLoading={isLoading}
buttonProps={{ variant }}
allowSearch={true}
searchValue={search}
onSearch={handleSearch}
onChange={handleSelect}
renderValue={() => website?.name}
>
{({ id, name }: any) => <ListItem key={id}>{name}</ListItem>}
</Select>

View file

@ -66,7 +66,7 @@ export function getRequestDateRange(query: Record<string, string>) {
};
}
export async function getRequestFilters(query: Record<string, any>, websiteId?: string) {
export function getRequestFilters(query: Record<string, any>) {
const result: Record<string, any> = {};
for (const key of Object.keys(FILTER_COLUMNS)) {
@ -76,20 +76,17 @@ export async function getRequestFilters(query: Record<string, any>, websiteId?:
}
}
return result;
}
export async function getRequestSegments(websiteId: string, query: Record<string, any>) {
for (const key of Object.keys(FILTER_GROUPS)) {
const value = query[key];
if (value !== undefined) {
const segment = await getWebsiteSegment(websiteId, key, value);
if (key === 'segment') {
// merge filters into result
Object.assign(result, segment.parameters);
} else {
result[key] = segment.parameters;
}
return getWebsiteSegment(websiteId, key, value);
}
}
return result;
}
export async function setWebsiteDate(websiteId: string, data: Record<string, any>) {
@ -102,13 +99,18 @@ export async function setWebsiteDate(websiteId: string, data: Record<string, any
return data;
}
export function getQueryFilters(params: Record<string, any>): QueryFilters {
const dateRange = getRequestDateRange(params);
export async function getQueryFilters(
params: Record<string, any>,
websiteId?: string,
): Promise<QueryFilters> {
const dateRange = await setWebsiteDate(websiteId, getRequestDateRange(params));
const filters = getRequestFilters(params);
const segments = await getRequestSegments(websiteId, params);
return {
...dateRange,
...filters,
...segments,
page: params?.page,
pageSize: params?.page ? params?.pageSize || DEFAULT_PAGE_SIZE : undefined,
orderBy: params?.orderBy,

View file

@ -71,6 +71,9 @@ export interface FilterParams {
search?: string;
tag?: string;
eventType?: number;
segment?: string;
cohort?: string;
compare?: string;
}
export interface SortParams {

View file

@ -85,7 +85,7 @@ async function clickhouseQuery(
from (
select arrayJoin(event_name) as event_name,
created_at
from website_event_stats_hourly website_event
from website_event_stats_hourly as website_event
where website_id = {websiteId:UUID}
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
and event_type = {eventType:UInt32}

View file

@ -81,7 +81,7 @@ async function clickhouseQuery(
select
${getDateSQL('website_event.created_at', unit, timezone)} as t,
sum(views) as y
from website_event_stats_hourly website_event
from website_event_stats_hourly as website_event
${cohortQuery}
where website_id = {websiteId:UUID}
and created_at between {startDate:DateTime64} and {endDate:DateTime64}

View file

@ -103,7 +103,7 @@ async function clickhouseQuery(
${column} x,
uniq(session_id) y
${includeCountry ? ', country' : ''}
from website_event_stats_hourly website_event
from website_event_stats_hourly as website_event
${cohortQuery}
where website_id = {websiteId:UUID}
and created_at between {startDate:DateTime64} and {endDate:DateTime64}

View file

@ -81,7 +81,7 @@ async function clickhouseQuery(
select
${getDateSQL('website_event.created_at', unit, timezone)} as t,
uniq(session_id) as y
from website_event_stats_hourly website_event
from website_event_stats_hourly as website_event
where website_id = {websiteId:UUID}
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
and event_type = {eventType:UInt32}

View file

@ -94,7 +94,7 @@ async function clickhouseQuery(websiteId: string, filters: QueryFilters) {
uniq(visit_id) as visits,
sumIf(views, event_type = 1) as views,
lastAt as createdAt
from website_event_stats_hourly website_event
from website_event_stats_hourly as website_event
${cohortQuery}
where website_id = {websiteId:UUID}
${dateQuery}