mirror of
https://github.com/umami-software/umami.git
synced 2026-02-04 04:37:11 +01:00
Merge branch 'dev' of https://github.com/umami-software/umami into dev
This commit is contained in:
commit
4680c89e28
52 changed files with 498 additions and 118 deletions
|
|
@ -2,8 +2,8 @@
|
|||
CREATE TABLE "share" (
|
||||
"share_id" UUID NOT NULL,
|
||||
"entity_id" UUID NOT NULL,
|
||||
"share_type" INTEGER NOT NULL,
|
||||
"name" VARCHAR(200) NOT NULL,
|
||||
"share_type" INTEGER NOT NULL,
|
||||
"slug" VARCHAR(100) NOT NULL,
|
||||
"parameters" JSONB NOT NULL,
|
||||
"created_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP,
|
||||
|
|
@ -12,9 +12,6 @@ CREATE TABLE "share" (
|
|||
CONSTRAINT "share_pkey" PRIMARY KEY ("share_id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "share_share_id_key" ON "share"("share_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "share_slug_key" ON "share"("slug");
|
||||
|
||||
|
|
@ -28,7 +25,7 @@ SELECT gen_random_uuid(),
|
|||
name,
|
||||
1,
|
||||
share_id,
|
||||
'{}'::jsonb,
|
||||
'{"overview":true}'::jsonb,
|
||||
now()
|
||||
FROM "website"
|
||||
WHERE share_id IS NOT NULL;
|
||||
|
|
|
|||
|
|
@ -14,9 +14,6 @@ CREATE TABLE "board" (
|
|||
CONSTRAINT "board_pkey" PRIMARY KEY ("board_id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "board_board_id_key" ON "board"("board_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "board_slug_key" ON "board"("slug");
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,3 @@
|
|||
-- DropIndex
|
||||
DROP INDEX "board_board_id_key";
|
||||
|
||||
-- DropIndex
|
||||
DROP INDEX "link_link_id_key";
|
||||
|
||||
|
|
@ -19,9 +16,6 @@ DROP INDEX "segment_segment_id_key";
|
|||
-- DropIndex
|
||||
DROP INDEX "session_session_id_key";
|
||||
|
||||
-- DropIndex
|
||||
DROP INDEX "share_share_id_key";
|
||||
|
||||
-- DropIndex
|
||||
DROP INDEX "team_team_id_key";
|
||||
|
||||
|
|
|
|||
|
|
@ -3,14 +3,19 @@ import { Column } from '@umami/react-zen';
|
|||
import { PageHeader } from '@/components/common/PageHeader';
|
||||
import { Panel } from '@/components/common/Panel';
|
||||
import { useMessages } from '@/components/hooks';
|
||||
import { TeamsAddButton } from '../../teams/TeamsAddButton';
|
||||
import { AdminTeamsDataTable } from './AdminTeamsDataTable';
|
||||
|
||||
export function AdminTeamsPage() {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
|
||||
const handleSave = () => {};
|
||||
|
||||
return (
|
||||
<Column gap="6" margin="2">
|
||||
<PageHeader title={formatMessage(labels.teams)} />
|
||||
<PageHeader title={formatMessage(labels.teams)}>
|
||||
<TeamsAddButton onSave={handleSave} isAdmin={true} />
|
||||
</PageHeader>
|
||||
<Panel>
|
||||
<AdminTeamsDataTable />
|
||||
</Panel>
|
||||
|
|
|
|||
|
|
@ -7,8 +7,17 @@ import {
|
|||
TextField,
|
||||
} from '@umami/react-zen';
|
||||
import { useMessages, useUpdateQuery } from '@/components/hooks';
|
||||
import { UserSelect } from '@/components/input/UserSelect';
|
||||
|
||||
export function TeamAddForm({ onSave, onClose }: { onSave: () => void; onClose: () => void }) {
|
||||
export function TeamAddForm({
|
||||
onSave,
|
||||
onClose,
|
||||
isAdmin,
|
||||
}: {
|
||||
onSave: () => void;
|
||||
onClose: () => void;
|
||||
isAdmin: boolean;
|
||||
}) {
|
||||
const { formatMessage, labels, getErrorMessage } = useMessages();
|
||||
const { mutateAsync, error, isPending } = useUpdateQuery('/teams');
|
||||
|
||||
|
|
@ -26,6 +35,11 @@ export function TeamAddForm({ onSave, onClose }: { onSave: () => void; onClose:
|
|||
<FormField name="name" label={formatMessage(labels.name)}>
|
||||
<TextField autoComplete="off" />
|
||||
</FormField>
|
||||
{isAdmin && (
|
||||
<FormField name="ownerId" label={formatMessage(labels.teamOwner)}>
|
||||
<UserSelect buttonProps={{ style: { outline: 'none' } }} />
|
||||
</FormField>
|
||||
)}
|
||||
<FormButtons>
|
||||
<Button isDisabled={isPending} onPress={onClose}>
|
||||
{formatMessage(labels.cancel)}
|
||||
|
|
|
|||
76
src/app/(main)/teams/TeamMemberAddForm.tsx
Normal file
76
src/app/(main)/teams/TeamMemberAddForm.tsx
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
import {
|
||||
Button,
|
||||
Form,
|
||||
FormButtons,
|
||||
FormField,
|
||||
FormSubmitButton,
|
||||
ListItem,
|
||||
Select,
|
||||
} from '@umami/react-zen';
|
||||
import { useMessages, useUpdateQuery } from '@/components/hooks';
|
||||
import { UserSelect } from '@/components/input/UserSelect';
|
||||
import { ROLES } from '@/lib/constants';
|
||||
|
||||
const roles = [ROLES.teamManager, ROLES.teamMember, ROLES.teamViewOnly];
|
||||
|
||||
export function TeamMemberAddForm({
|
||||
teamId,
|
||||
onSave,
|
||||
onClose,
|
||||
}: {
|
||||
teamId: string;
|
||||
onSave?: () => void;
|
||||
onClose?: () => void;
|
||||
}) {
|
||||
const { formatMessage, labels, getErrorMessage } = useMessages();
|
||||
const { mutateAsync, error, isPending } = useUpdateQuery(`/teams/${teamId}/users`);
|
||||
|
||||
const handleSubmit = async (data: any) => {
|
||||
await mutateAsync(data, {
|
||||
onSuccess: async () => {
|
||||
onSave?.();
|
||||
onClose?.();
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const renderRole = role => {
|
||||
switch (role) {
|
||||
case ROLES.teamManager:
|
||||
return formatMessage(labels.manager);
|
||||
case ROLES.teamMember:
|
||||
return formatMessage(labels.member);
|
||||
case ROLES.teamViewOnly:
|
||||
return formatMessage(labels.viewOnly);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Form onSubmit={handleSubmit} error={getErrorMessage(error)}>
|
||||
<FormField
|
||||
name="userId"
|
||||
label={formatMessage(labels.username)}
|
||||
rules={{ required: 'Required' }}
|
||||
>
|
||||
<UserSelect teamId={teamId} />
|
||||
</FormField>
|
||||
<FormField name="role" label={formatMessage(labels.role)} rules={{ required: 'Required' }}>
|
||||
<Select items={roles} renderValue={value => renderRole(value as any)}>
|
||||
{roles.map(value => (
|
||||
<ListItem key={value} id={value}>
|
||||
{renderRole(value)}
|
||||
</ListItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormField>
|
||||
<FormButtons>
|
||||
<Button isDisabled={isPending} onPress={onClose}>
|
||||
{formatMessage(labels.cancel)}
|
||||
</Button>
|
||||
<FormSubmitButton variant="primary" isDisabled={isPending}>
|
||||
{formatMessage(labels.save)}
|
||||
</FormSubmitButton>
|
||||
</FormButtons>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
|
@ -4,7 +4,13 @@ import { Plus } from '@/components/icons';
|
|||
import { messages } from '@/components/messages';
|
||||
import { TeamAddForm } from './TeamAddForm';
|
||||
|
||||
export function TeamsAddButton({ onSave }: { onSave?: () => void }) {
|
||||
export function TeamsAddButton({
|
||||
onSave,
|
||||
isAdmin = false,
|
||||
}: {
|
||||
onSave?: () => void;
|
||||
isAdmin?: boolean;
|
||||
}) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { toast } = useToast();
|
||||
const { touch } = useModified();
|
||||
|
|
@ -25,7 +31,7 @@ export function TeamsAddButton({ onSave }: { onSave?: () => void }) {
|
|||
</Button>
|
||||
<Modal>
|
||||
<Dialog title={formatMessage(labels.createTeam)} style={{ width: 400 }}>
|
||||
{({ close }) => <TeamAddForm onSave={handleSave} onClose={close} />}
|
||||
{({ close }) => <TeamAddForm onSave={handleSave} onClose={close} isAdmin={isAdmin} />}
|
||||
</Dialog>
|
||||
</Modal>
|
||||
</DialogTrigger>
|
||||
|
|
|
|||
40
src/app/(main)/teams/TeamsMemberAddButton.tsx
Normal file
40
src/app/(main)/teams/TeamsMemberAddButton.tsx
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
import { Button, Dialog, DialogTrigger, Icon, Modal, Text, useToast } from '@umami/react-zen';
|
||||
import { useMessages, useModified } from '@/components/hooks';
|
||||
import { Plus } from '@/components/icons';
|
||||
import { messages } from '@/components/messages';
|
||||
import { TeamMemberAddForm } from './TeamMemberAddForm';
|
||||
|
||||
export function TeamsMemberAddButton({
|
||||
teamId,
|
||||
onSave,
|
||||
}: {
|
||||
teamId: string;
|
||||
onSave?: () => void;
|
||||
isAdmin?: boolean;
|
||||
}) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { toast } = useToast();
|
||||
const { touch } = useModified();
|
||||
|
||||
const handleSave = async () => {
|
||||
toast(formatMessage(messages.saved));
|
||||
touch('teams:members');
|
||||
onSave?.();
|
||||
};
|
||||
|
||||
return (
|
||||
<DialogTrigger>
|
||||
<Button>
|
||||
<Icon>
|
||||
<Plus />
|
||||
</Icon>
|
||||
<Text>{formatMessage(labels.addMember)}</Text>
|
||||
</Button>
|
||||
<Modal>
|
||||
<Dialog title={formatMessage(labels.addMember)} style={{ width: 400 }}>
|
||||
{({ close }) => <TeamMemberAddForm teamId={teamId} onSave={handleSave} onClose={close} />}
|
||||
</Dialog>
|
||||
</Modal>
|
||||
</DialogTrigger>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,10 +1,12 @@
|
|||
import { Column } from '@umami/react-zen';
|
||||
import { Column, Heading, Row } from '@umami/react-zen';
|
||||
import { TeamLeaveButton } from '@/app/(main)/teams/TeamLeaveButton';
|
||||
import { PageHeader } from '@/components/common/PageHeader';
|
||||
import { Panel } from '@/components/common/Panel';
|
||||
import { useLoginQuery, useNavigation, useTeam } from '@/components/hooks';
|
||||
import { useLoginQuery, useMessages, useNavigation, useTeam } from '@/components/hooks';
|
||||
import { Users } from '@/components/icons';
|
||||
import { labels } from '@/components/messages';
|
||||
import { ROLES } from '@/lib/constants';
|
||||
import { TeamsMemberAddButton } from '../TeamsMemberAddButton';
|
||||
import { TeamEditForm } from './TeamEditForm';
|
||||
import { TeamManage } from './TeamManage';
|
||||
import { TeamMembersDataTable } from './TeamMembersDataTable';
|
||||
|
|
@ -13,6 +15,7 @@ export function TeamSettings({ teamId }: { teamId: string }) {
|
|||
const team: any = useTeam();
|
||||
const { user } = useLoginQuery();
|
||||
const { pathname } = useNavigation();
|
||||
const { formatMessage } = useMessages();
|
||||
|
||||
const isAdmin = pathname.includes('/admin');
|
||||
|
||||
|
|
@ -37,6 +40,10 @@ export function TeamSettings({ teamId }: { teamId: string }) {
|
|||
<TeamEditForm teamId={teamId} allowEdit={canEdit} showAccessCode={canEdit} />
|
||||
</Panel>
|
||||
<Panel>
|
||||
<Row alignItems="center" justifyContent="space-between">
|
||||
<Heading size="2">{formatMessage(labels.members)}</Heading>
|
||||
{isAdmin && <TeamsMemberAddButton teamId={teamId} />}
|
||||
</Row>
|
||||
<TeamMembersDataTable teamId={teamId} allowEdit={canEdit} />
|
||||
</Panel>
|
||||
{isTeamOwner && (
|
||||
|
|
|
|||
|
|
@ -21,9 +21,15 @@ export interface JourneyProps {
|
|||
steps: number;
|
||||
startStep?: string;
|
||||
endStep?: string;
|
||||
view: string;
|
||||
}
|
||||
|
||||
export function Journey({ websiteId, steps, startStep, endStep }: JourneyProps) {
|
||||
const EVENT_TYPES = {
|
||||
views: 1,
|
||||
events: 2,
|
||||
};
|
||||
|
||||
export function Journey({ websiteId, steps, startStep, endStep, view }: JourneyProps) {
|
||||
const [selectedNode, setSelectedNode] = useState(null);
|
||||
const [activeNode, setActiveNode] = useState(null);
|
||||
const { formatMessage, labels } = useMessages();
|
||||
|
|
@ -32,6 +38,8 @@ export function Journey({ websiteId, steps, startStep, endStep }: JourneyProps)
|
|||
steps,
|
||||
startStep,
|
||||
endStep,
|
||||
view,
|
||||
eventType: EVENT_TYPES[view],
|
||||
});
|
||||
|
||||
useEscapeKey(() => setSelectedNode(null));
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
'use client';
|
||||
import { Column, Grid, ListItem, SearchField, Select } from '@umami/react-zen';
|
||||
import { Column, Grid, ListItem, Row, SearchField, Select } from '@umami/react-zen';
|
||||
import { FilterButtons } from 'dist';
|
||||
import { useState } from 'react';
|
||||
import { WebsiteControls } from '@/app/(main)/websites/[websiteId]/WebsiteControls';
|
||||
import { Panel } from '@/components/common/Panel';
|
||||
|
|
@ -14,10 +15,26 @@ export function JourneysPage({ websiteId }: { websiteId: string }) {
|
|||
const {
|
||||
dateRange: { startDate, endDate },
|
||||
} = useDateRange();
|
||||
const [view, setView] = useState('all');
|
||||
const [steps, setSteps] = useState(DEFAULT_STEP);
|
||||
const [startStep, setStartStep] = useState('');
|
||||
const [endStep, setEndStep] = useState('');
|
||||
|
||||
const buttons = [
|
||||
{
|
||||
id: 'all',
|
||||
label: formatMessage(labels.all),
|
||||
},
|
||||
{
|
||||
id: 'views',
|
||||
label: formatMessage(labels.views),
|
||||
},
|
||||
{
|
||||
id: 'events',
|
||||
label: formatMessage(labels.events),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Column gap>
|
||||
<WebsiteControls websiteId={websiteId} />
|
||||
|
|
@ -52,6 +69,9 @@ export function JourneysPage({ websiteId }: { websiteId: string }) {
|
|||
/>
|
||||
</Column>
|
||||
</Grid>
|
||||
<Row justifyContent="flex-end">
|
||||
<FilterButtons items={buttons} value={view} onChange={setView} />
|
||||
</Row>
|
||||
<Panel height="900px" allowFullscreen>
|
||||
<Journey
|
||||
websiteId={websiteId}
|
||||
|
|
@ -60,6 +80,7 @@ export function JourneysPage({ websiteId }: { websiteId: string }) {
|
|||
steps={steps}
|
||||
startStep={startStep}
|
||||
endStep={endStep}
|
||||
view={view}
|
||||
/>
|
||||
</Panel>
|
||||
</Column>
|
||||
|
|
|
|||
|
|
@ -21,30 +21,28 @@ export function WebsiteChart({
|
|||
const { pageviews, sessions, compare } = (data || {}) as any;
|
||||
|
||||
const chartData = useMemo(() => {
|
||||
if (data) {
|
||||
const result = {
|
||||
pageviews,
|
||||
sessions,
|
||||
};
|
||||
if (!data) {
|
||||
return { pageviews: [], sessions: [] };
|
||||
}
|
||||
|
||||
if (compare) {
|
||||
result.compare = {
|
||||
pageviews: result.pageviews.map(({ x }, i) => ({
|
||||
return {
|
||||
pageviews,
|
||||
sessions,
|
||||
...(compare && {
|
||||
compare: {
|
||||
pageviews: pageviews.map(({ x }, i) => ({
|
||||
x,
|
||||
y: compare.pageviews[i]?.y,
|
||||
d: compare.pageviews[i]?.x,
|
||||
})),
|
||||
sessions: result.sessions.map(({ x }, i) => ({
|
||||
sessions: sessions.map(({ x }, i) => ({
|
||||
x,
|
||||
y: compare.sessions[i]?.y,
|
||||
d: compare.sessions[i]?.x,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
return { pageviews: [], sessions: [] };
|
||||
},
|
||||
}),
|
||||
};
|
||||
}, [data, startDate, endDate, unit]);
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -169,6 +169,12 @@ export function WebsiteExpandedMenu({
|
|||
path: updateParams({ view: 'hostname' }),
|
||||
icon: <Network />,
|
||||
},
|
||||
{
|
||||
id: 'distinctId',
|
||||
label: formatMessage(labels.distinctId),
|
||||
path: updateParams({ view: 'distinctId' }),
|
||||
icon: <Tag />,
|
||||
},
|
||||
{
|
||||
id: 'tag',
|
||||
label: formatMessage(labels.tag),
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import {
|
|||
} from '@umami/react-zen';
|
||||
import { Fragment } from 'react';
|
||||
import { useMessages, useNavigation } from '@/components/hooks';
|
||||
import { Edit, More, Share } from '@/components/icons';
|
||||
import { Edit, MoreHorizontal, Share } from '@/components/icons';
|
||||
|
||||
export function WebsiteMenu({ websiteId }: { websiteId: string }) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
|
|
@ -33,7 +33,7 @@ export function WebsiteMenu({ websiteId }: { websiteId: string }) {
|
|||
<MenuTrigger>
|
||||
<Button variant="quiet">
|
||||
<Icon>
|
||||
<More />
|
||||
<MoreHorizontal />
|
||||
</Icon>
|
||||
</Button>
|
||||
<Popover placement="bottom">
|
||||
|
|
|
|||
|
|
@ -7,14 +7,18 @@ import { formatLongNumber, formatShortTime } from '@/lib/format';
|
|||
|
||||
export function WebsiteMetricsBar({
|
||||
websiteId,
|
||||
compareMode,
|
||||
}: {
|
||||
websiteId: string;
|
||||
showChange?: boolean;
|
||||
compareMode?: boolean;
|
||||
}) {
|
||||
const { isAllTime } = useDateRange();
|
||||
const { isAllTime, dateCompare } = useDateRange();
|
||||
const { formatMessage, labels, getErrorMessage } = useMessages();
|
||||
const { data, isLoading, isFetching, error } = useWebsiteStatsQuery(websiteId);
|
||||
const { data, isLoading, isFetching, error } = useWebsiteStatsQuery({
|
||||
websiteId,
|
||||
compare: compareMode ? dateCompare?.compare : undefined,
|
||||
});
|
||||
|
||||
const { pageviews, visitors, visits, bounces, totaltime, comparison } = data || {};
|
||||
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ export function WebsiteNav({
|
|||
event: undefined,
|
||||
compare: undefined,
|
||||
view: undefined,
|
||||
unit: undefined,
|
||||
});
|
||||
|
||||
const items = [
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
'use client';
|
||||
import { Column } from '@umami/react-zen';
|
||||
import { Column, Row } from '@umami/react-zen';
|
||||
import { ExpandedViewModal } from '@/app/(main)/websites/[websiteId]/ExpandedViewModal';
|
||||
import { Panel } from '@/components/common/Panel';
|
||||
import { UnitFilter } from '@/components/input/UnitFilter';
|
||||
import { WebsiteChart } from './WebsiteChart';
|
||||
import { WebsiteControls } from './WebsiteControls';
|
||||
import { WebsiteMetricsBar } from './WebsiteMetricsBar';
|
||||
|
|
@ -13,6 +14,9 @@ export function WebsitePage({ websiteId }: { websiteId: string }) {
|
|||
<WebsiteControls websiteId={websiteId} />
|
||||
<WebsiteMetricsBar websiteId={websiteId} showChange={true} />
|
||||
<Panel minHeight="520px">
|
||||
<Row justifyContent="end">
|
||||
<UnitFilter />
|
||||
</Row>
|
||||
<WebsiteChart websiteId={websiteId} />
|
||||
</Panel>
|
||||
<WebsitePanels websiteId={websiteId} />
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ export function ComparePage({ websiteId }: { websiteId: string }) {
|
|||
return (
|
||||
<Column gap>
|
||||
<WebsiteControls websiteId={websiteId} allowCompare={true} />
|
||||
<WebsiteMetricsBar websiteId={websiteId} showChange={true} />
|
||||
<WebsiteMetricsBar websiteId={websiteId} compareMode={true} showChange={true} />
|
||||
<Panel minHeight="520px">
|
||||
<WebsiteChart websiteId={websiteId} compareMode={true} />
|
||||
</Panel>
|
||||
|
|
|
|||
|
|
@ -93,6 +93,11 @@ export function CompareTables({ websiteId }: { websiteId: string }) {
|
|||
label: formatMessage(labels.hostname),
|
||||
path: renderPath('hostname'),
|
||||
},
|
||||
{
|
||||
id: 'distinctId',
|
||||
label: formatMessage(labels.distinctId),
|
||||
path: renderPath('distinctId'),
|
||||
},
|
||||
{
|
||||
id: 'tag',
|
||||
label: formatMessage(labels.tags),
|
||||
|
|
|
|||
|
|
@ -25,6 +25,19 @@ export function EventsTable(props: DataTableProps) {
|
|||
const { updateParams } = useNavigation();
|
||||
const { formatValue } = useFormat();
|
||||
|
||||
const renderLink = (label: string, hostname: string) => {
|
||||
return (
|
||||
<a
|
||||
href={`//${hostname}${label}`}
|
||||
style={{ fontWeight: 'bold' }}
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
>
|
||||
{label}
|
||||
</a>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<DataTable {...props}>
|
||||
<DataColumn id="event" label={formatMessage(labels.event)} width="2fr">
|
||||
|
|
@ -43,7 +56,7 @@ export function EventsTable(props: DataTableProps) {
|
|||
title={row.eventName || row.urlPath}
|
||||
truncate
|
||||
>
|
||||
{row.eventName || row.urlPath}
|
||||
{row.eventName || renderLink(row.urlPath, row.hostname)}
|
||||
</Text>
|
||||
{row.hasData > 0 && <PropertiesButton websiteId={row.websiteId} eventId={row.id} />}
|
||||
</Row>
|
||||
|
|
|
|||
|
|
@ -74,8 +74,9 @@ export function RealtimeLog({ data }: { data: any }) {
|
|||
os: string;
|
||||
country: string;
|
||||
device: string;
|
||||
hostname: string;
|
||||
}) => {
|
||||
const { __type, eventName, urlPath, browser, os, country, device } = log;
|
||||
const { __type, eventName, urlPath, browser, os, country, device, hostname } = log;
|
||||
|
||||
if (__type === TYPE_EVENT) {
|
||||
return (
|
||||
|
|
@ -86,7 +87,8 @@ export function RealtimeLog({ data }: { data: any }) {
|
|||
url: (
|
||||
<a
|
||||
key="a"
|
||||
href={`//${website?.domain}${urlPath}`}
|
||||
href={`//${hostname}${urlPath}`}
|
||||
style={{ fontWeight: 'bold' }}
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
>
|
||||
|
|
@ -100,7 +102,12 @@ export function RealtimeLog({ data }: { data: any }) {
|
|||
|
||||
if (__type === TYPE_PAGEVIEW) {
|
||||
return (
|
||||
<a href={`//${website?.domain}${urlPath}`} target="_blank" rel="noreferrer noopener">
|
||||
<a
|
||||
href={`//${hostname}${urlPath}`}
|
||||
style={{ fontWeight: 'bold' }}
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
>
|
||||
{urlPath}
|
||||
</a>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -39,10 +39,23 @@ export function SessionActivity({
|
|||
const { isMobile } = useMobile();
|
||||
let lastDay = null;
|
||||
|
||||
const renderLink = (label: string, hostname: string) => {
|
||||
return (
|
||||
<a
|
||||
href={`//${hostname}${label}`}
|
||||
style={{ fontWeight: 'bold' }}
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
>
|
||||
{label}
|
||||
</a>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<LoadingPanel data={data} isLoading={isLoading} error={error}>
|
||||
<Column gap>
|
||||
{data?.map(({ eventId, createdAt, urlPath, eventName, visitId, hasData }) => {
|
||||
{data?.map(({ eventId, createdAt, urlPath, eventName, visitId, hostname, hasData }) => {
|
||||
const showHeader = !lastDay || !isSameDay(new Date(lastDay), new Date(createdAt));
|
||||
lastDay = createdAt;
|
||||
|
||||
|
|
@ -61,7 +74,7 @@ export function SessionActivity({
|
|||
: formatMessage(labels.viewedPage)}
|
||||
</Text>
|
||||
<Text weight="bold" style={{ maxWidth: isMobile ? '400px' : null }} truncate>
|
||||
{eventName || urlPath}
|
||||
{eventName || renderLink(urlPath, hostname)}
|
||||
</Text>
|
||||
{hasData > 0 && <PropertiesButton websiteId={websiteId} eventId={eventId} />}
|
||||
</Row>
|
||||
|
|
|
|||
|
|
@ -12,11 +12,16 @@ export async function POST(request: Request) {
|
|||
}
|
||||
|
||||
const { websiteId, parameters, filters } = body;
|
||||
const { eventType } = parameters;
|
||||
|
||||
if (!(await canViewWebsite(auth, websiteId))) {
|
||||
return unauthorized();
|
||||
}
|
||||
|
||||
if (eventType) {
|
||||
filters.eventType = eventType;
|
||||
}
|
||||
|
||||
const queryFilters = await getQueryFilters(filters, websiteId);
|
||||
|
||||
const data = await getJourney(websiteId, parameters, queryFilters);
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ export async function GET(request: Request) {
|
|||
export async function POST(request: Request) {
|
||||
const schema = z.object({
|
||||
name: z.string().max(50),
|
||||
ownerId: z.uuid().optional(),
|
||||
});
|
||||
|
||||
const { auth, body, error } = await parseRequest(request, schema);
|
||||
|
|
@ -40,7 +41,7 @@ export async function POST(request: Request) {
|
|||
return unauthorized();
|
||||
}
|
||||
|
||||
const { name } = body;
|
||||
const { name, ownerId } = body;
|
||||
|
||||
const team = await createTeam(
|
||||
{
|
||||
|
|
@ -48,7 +49,7 @@ export async function POST(request: Request) {
|
|||
name,
|
||||
accessCode: `team_${getRandomChars(16)}`,
|
||||
},
|
||||
auth.user.id,
|
||||
ownerId ?? auth.user.id,
|
||||
);
|
||||
|
||||
return json(team);
|
||||
|
|
|
|||
|
|
@ -31,9 +31,11 @@ export async function GET(
|
|||
|
||||
const data = await getWebsiteStats(websiteId, filters);
|
||||
|
||||
const compare = filters.compare ?? 'prev';
|
||||
|
||||
const { startDate, endDate } = getCompareDate(compare, filters.startDate, filters.endDate);
|
||||
const { startDate, endDate } = getCompareDate(
|
||||
filters.compare ?? 'prev',
|
||||
filters.startDate,
|
||||
filters.endDate,
|
||||
);
|
||||
|
||||
const comparison = await getWebsiteStats(websiteId, {
|
||||
...filters,
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ export function useWebsiteExpandedMetricsQuery(
|
|||
options?: ReactQueryOptions<WebsiteExpandedMetricsData>,
|
||||
) {
|
||||
const { get, useQuery } = useApi();
|
||||
const { startAt, endAt, unit, timezone } = useDateParameters();
|
||||
const { startAt, endAt } = useDateParameters();
|
||||
const filters = useFilterParameters();
|
||||
|
||||
return useQuery<WebsiteExpandedMetricsData>({
|
||||
|
|
@ -29,8 +29,6 @@ export function useWebsiteExpandedMetricsQuery(
|
|||
websiteId,
|
||||
startAt,
|
||||
endAt,
|
||||
unit,
|
||||
timezone,
|
||||
...filters,
|
||||
...params,
|
||||
},
|
||||
|
|
@ -39,8 +37,6 @@ export function useWebsiteExpandedMetricsQuery(
|
|||
get(`/websites/${websiteId}/metrics/expanded`, {
|
||||
startAt,
|
||||
endAt,
|
||||
unit,
|
||||
timezone,
|
||||
...filters,
|
||||
...params,
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ export function useWebsiteMetricsQuery(
|
|||
options?: ReactQueryOptions<WebsiteMetricsData>,
|
||||
) {
|
||||
const { get, useQuery } = useApi();
|
||||
const { startAt, endAt, unit, timezone } = useDateParameters();
|
||||
const { startAt, endAt } = useDateParameters();
|
||||
const filters = useFilterParameters();
|
||||
|
||||
return useQuery<WebsiteMetricsData>({
|
||||
|
|
@ -25,8 +25,6 @@ export function useWebsiteMetricsQuery(
|
|||
websiteId,
|
||||
startAt,
|
||||
endAt,
|
||||
unit,
|
||||
timezone,
|
||||
...filters,
|
||||
...params,
|
||||
},
|
||||
|
|
@ -35,8 +33,6 @@ export function useWebsiteMetricsQuery(
|
|||
get(`/websites/${websiteId}/metrics`, {
|
||||
startAt,
|
||||
endAt,
|
||||
unit,
|
||||
timezone,
|
||||
...filters,
|
||||
...params,
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import type { UseQueryOptions } from '@tanstack/react-query';
|
||||
import { useDateParameters } from '@/components/hooks/useDateParameters';
|
||||
import { useDateRange } from '@/components/hooks/useDateRange';
|
||||
import { useApi } from '../useApi';
|
||||
import { useFilterParameters } from '../useFilterParameters';
|
||||
|
||||
|
|
@ -20,21 +19,16 @@ export interface WebsiteStatsData {
|
|||
}
|
||||
|
||||
export function useWebsiteStatsQuery(
|
||||
websiteId: string,
|
||||
{ websiteId, compare }: { websiteId: string; compare?: string },
|
||||
options?: UseQueryOptions<WebsiteStatsData, Error, WebsiteStatsData>,
|
||||
) {
|
||||
const { get, useQuery } = useApi();
|
||||
const { startAt, endAt, unit, timezone } = useDateParameters();
|
||||
const { compare } = useDateRange();
|
||||
const { startAt, endAt } = useDateParameters();
|
||||
const filters = useFilterParameters();
|
||||
|
||||
return useQuery<WebsiteStatsData>({
|
||||
queryKey: [
|
||||
'websites:stats',
|
||||
{ websiteId, startAt, endAt, unit, timezone, compare, ...filters },
|
||||
],
|
||||
queryFn: () =>
|
||||
get(`/websites/${websiteId}/stats`, { startAt, endAt, unit, timezone, compare, ...filters }),
|
||||
queryKey: ['websites:stats', { websiteId, compare, startAt, endAt, ...filters }],
|
||||
queryFn: () => get(`/websites/${websiteId}/stats`, { compare, startAt, endAt, ...filters }),
|
||||
enabled: !!websiteId,
|
||||
...options,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -12,13 +12,12 @@ export function useWeeklyTrafficQuery(websiteId: string, params?: Record<string,
|
|||
return useQuery({
|
||||
queryKey: [
|
||||
'sessions',
|
||||
{ websiteId, modified, startAt, endAt, unit, timezone, ...params, ...filters },
|
||||
{ websiteId, modified, startAt, endAt, timezone, ...params, ...filters },
|
||||
],
|
||||
queryFn: () => {
|
||||
return get(`/websites/${websiteId}/sessions/weekly`, {
|
||||
startAt,
|
||||
endAt,
|
||||
unit,
|
||||
timezone,
|
||||
...params,
|
||||
...filters,
|
||||
|
|
|
|||
|
|
@ -7,13 +7,13 @@ import { getItem } from '@/lib/storage';
|
|||
|
||||
export function useDateRange(options: { ignoreOffset?: boolean; timezone?: string } = {}) {
|
||||
const {
|
||||
query: { date = '', offset = 0, compare = 'prev' },
|
||||
query: { date = '', unit = '', offset = 0, compare = 'prev' },
|
||||
} = useNavigation();
|
||||
const { locale } = useLocale();
|
||||
|
||||
const dateRange = useMemo(() => {
|
||||
const dateRangeObject = parseDateRange(
|
||||
date || getItem(DATE_RANGE_CONFIG) || DEFAULT_DATE_RANGE_VALUE,
|
||||
unit,
|
||||
locale,
|
||||
options.timezone,
|
||||
);
|
||||
|
|
@ -21,12 +21,13 @@ export function useDateRange(options: { ignoreOffset?: boolean; timezone?: strin
|
|||
return !options.ignoreOffset && offset
|
||||
? getOffsetDateRange(dateRangeObject, +offset)
|
||||
: dateRangeObject;
|
||||
}, [date, offset, options]);
|
||||
}, [date, unit, offset, options]);
|
||||
|
||||
const dateCompare = getCompareDate(compare, dateRange.startDate, dateRange.endDate);
|
||||
|
||||
return {
|
||||
date,
|
||||
unit,
|
||||
offset,
|
||||
compare,
|
||||
isAllTime: date.endsWith(`:all`),
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ export function useFields() {
|
|||
{ name: 'region', type: 'string', label: formatMessage(labels.region) },
|
||||
{ name: 'city', type: 'string', label: formatMessage(labels.city) },
|
||||
{ name: 'hostname', type: 'string', label: formatMessage(labels.hostname) },
|
||||
{ name: 'distinctId', type: 'string', label: formatMessage(labels.distinctId) },
|
||||
{ name: 'tag', type: 'string', label: formatMessage(labels.tag) },
|
||||
{ name: 'event', type: 'string', label: formatMessage(labels.event) },
|
||||
];
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ export function useFilterParameters() {
|
|||
event,
|
||||
tag,
|
||||
hostname,
|
||||
distinctId,
|
||||
page,
|
||||
pageSize,
|
||||
search,
|
||||
|
|
@ -42,6 +43,7 @@ export function useFilterParameters() {
|
|||
event,
|
||||
tag,
|
||||
hostname,
|
||||
distinctId,
|
||||
search,
|
||||
segment,
|
||||
cohort,
|
||||
|
|
@ -61,6 +63,7 @@ export function useFilterParameters() {
|
|||
event,
|
||||
tag,
|
||||
hostname,
|
||||
distinctId,
|
||||
page,
|
||||
pageSize,
|
||||
search,
|
||||
|
|
|
|||
|
|
@ -61,7 +61,9 @@ export function FilterEditForm({ websiteId, onChange, onClose }: FilterEditFormP
|
|||
websiteId={websiteId}
|
||||
value={currentFilters}
|
||||
onChange={setCurrentFilters}
|
||||
exclude={excludeFilters ? ['path', 'title', 'hostname', 'tag', 'event'] : []}
|
||||
exclude={
|
||||
excludeFilters ? ['path', 'title', 'hostname', 'distinctId', 'tag', 'event'] : []
|
||||
}
|
||||
/>
|
||||
</TabPanel>
|
||||
<TabPanel id="segments">
|
||||
|
|
|
|||
71
src/components/input/UnitFilter.tsx
Normal file
71
src/components/input/UnitFilter.tsx
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
import { ListItem, Row, Select } from '@umami/react-zen';
|
||||
import { useMessages, useNavigation } from '@/components/hooks';
|
||||
import { DATE_RANGE_CONFIG, DEFAULT_DATE_RANGE_VALUE } from '@/lib/constants';
|
||||
import { getItem } from '@/lib/storage';
|
||||
|
||||
export function UnitFilter() {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { router, query, updateParams } = useNavigation();
|
||||
|
||||
const DATE_RANGE_UNIT_CONFIG = {
|
||||
'0week': {
|
||||
defaultUnit: 'day',
|
||||
availableUnits: ['day', 'hour'],
|
||||
},
|
||||
'7day': {
|
||||
defaultUnit: 'day',
|
||||
availableUnits: ['day', 'hour'],
|
||||
},
|
||||
'0month': {
|
||||
defaultUnit: 'day',
|
||||
availableUnits: ['day', 'hour'],
|
||||
},
|
||||
'30day': {
|
||||
defaultUnit: 'day',
|
||||
availableUnits: ['day', 'hour'],
|
||||
},
|
||||
'90day': {
|
||||
defaultUnit: 'day',
|
||||
availableUnits: ['day', 'month'],
|
||||
},
|
||||
'6month': {
|
||||
defaultUnit: 'month',
|
||||
availableUnits: ['month', 'day'],
|
||||
},
|
||||
};
|
||||
|
||||
const unitConfig =
|
||||
DATE_RANGE_UNIT_CONFIG[query.date || getItem(DATE_RANGE_CONFIG) || DEFAULT_DATE_RANGE_VALUE];
|
||||
|
||||
if (!unitConfig) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const handleChange = (value: string) => {
|
||||
router.push(updateParams({ unit: value }));
|
||||
};
|
||||
|
||||
const options = unitConfig.availableUnits.map(unit => ({
|
||||
id: unit,
|
||||
label: formatMessage(labels[unit]),
|
||||
}));
|
||||
|
||||
const selectedUnit = query.unit ?? unitConfig.defaultUnit;
|
||||
|
||||
return (
|
||||
<Row>
|
||||
<Select
|
||||
value={selectedUnit}
|
||||
onChange={handleChange}
|
||||
popoverProps={{ placement: 'bottom right' }}
|
||||
style={{ width: 100 }}
|
||||
>
|
||||
{options.map(({ id, label }) => (
|
||||
<ListItem key={id} id={id}>
|
||||
{label}
|
||||
</ListItem>
|
||||
))}
|
||||
</Select>
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
71
src/components/input/UserSelect.tsx
Normal file
71
src/components/input/UserSelect.tsx
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
import { ListItem, Row, Select, type SelectProps, Text } from '@umami/react-zen';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { Empty } from '@/components/common/Empty';
|
||||
import { useMessages, useTeamMembersQuery, useUsersQuery } from '@/components/hooks';
|
||||
|
||||
export function UserSelect({
|
||||
teamId,
|
||||
onChange,
|
||||
...props
|
||||
}: {
|
||||
teamId?: string;
|
||||
} & SelectProps) {
|
||||
const { formatMessage, messages } = useMessages();
|
||||
const { data: users, isLoading: usersLoading } = useUsersQuery();
|
||||
const { data: teamMembers, isLoading: teamMembersLoading } = useTeamMembersQuery(teamId);
|
||||
const [username, setUsername] = useState<string>();
|
||||
const [search, setSearch] = useState('');
|
||||
|
||||
const listItems = useMemo(() => {
|
||||
if (!users) {
|
||||
return [];
|
||||
}
|
||||
if (!teamId || !teamMembers) {
|
||||
return users.data;
|
||||
}
|
||||
const teamMemberIds = teamMembers.data.map(({ userId }) => userId);
|
||||
return users.data.filter(({ id }) => !teamMemberIds.includes(id));
|
||||
}, [users, teamMembers, teamId]);
|
||||
|
||||
const handleSearch = (value: string) => {
|
||||
setSearch(value);
|
||||
};
|
||||
|
||||
const handleOpenChange = () => {
|
||||
setSearch('');
|
||||
};
|
||||
|
||||
const handleChange = (id: string) => {
|
||||
setUsername(listItems.find(item => item.id === id)?.username);
|
||||
onChange(id);
|
||||
};
|
||||
|
||||
const renderValue = () => {
|
||||
return (
|
||||
<Row maxWidth="160px">
|
||||
<Text truncate>{username}</Text>
|
||||
</Row>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Select
|
||||
{...props}
|
||||
items={listItems}
|
||||
value={username}
|
||||
isLoading={usersLoading || (teamId && teamMembersLoading)}
|
||||
allowSearch={true}
|
||||
searchValue={search}
|
||||
onSearch={handleSearch}
|
||||
onChange={handleChange}
|
||||
onOpenChange={handleOpenChange}
|
||||
renderValue={renderValue}
|
||||
listProps={{
|
||||
renderEmptyState: () => <Empty message={formatMessage(messages.noResultsFound)} />,
|
||||
style: { maxHeight: 'calc(42vh - 65px)' },
|
||||
}}
|
||||
>
|
||||
{({ id, username }: any) => <ListItem key={id}>{username}</ListItem>}
|
||||
</Select>
|
||||
);
|
||||
}
|
||||
|
|
@ -41,7 +41,7 @@ export function WebsiteDateFilter({
|
|||
}),
|
||||
);
|
||||
} else {
|
||||
router.push(updateParams({ date, offset: undefined }));
|
||||
router.push(updateParams({ date, offset: undefined, unit: undefined }));
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -245,7 +245,10 @@ export const labels = defineMessages({
|
|||
tag: { id: 'label.tag', defaultMessage: 'Tag' },
|
||||
segment: { id: 'label.segment', defaultMessage: 'Segment' },
|
||||
cohort: { id: 'label.cohort', defaultMessage: 'Cohort' },
|
||||
minute: { id: 'label.minute', defaultMessage: 'Minute' },
|
||||
hour: { id: 'label.hour', defaultMessage: 'Hour' },
|
||||
day: { id: 'label.day', defaultMessage: 'Day' },
|
||||
month: { id: 'label.month', defaultMessage: 'Month' },
|
||||
date: { id: 'label.date', defaultMessage: 'Date' },
|
||||
pageOf: { id: 'label.page-of', defaultMessage: 'Page {current} of {total}' },
|
||||
create: { id: 'label.create', defaultMessage: 'Create' },
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ export const MetricCard = ({
|
|||
showChange = false,
|
||||
}: MetricCardProps) => {
|
||||
const diff = value - change;
|
||||
const pct = ((value - diff) / diff) * 100;
|
||||
const pct = diff !== 0 ? ((value - diff) / diff) * 100 : value !== 0 ? 100 : 0;
|
||||
const props = useSpring({ x: Number(value) || 0, from: { x: 0 } });
|
||||
const changeProps = useSpring({ x: Number(pct) || 0, from: { x: 0 } });
|
||||
|
||||
|
|
|
|||
|
|
@ -19,11 +19,13 @@ export * from '@/app/(main)/teams/TeamAddForm';
|
|||
export * from '@/app/(main)/teams/TeamJoinForm';
|
||||
export * from '@/app/(main)/teams/TeamLeaveButton';
|
||||
export * from '@/app/(main)/teams/TeamLeaveForm';
|
||||
export * from '@/app/(main)/teams/TeamMemberAddForm';
|
||||
export * from '@/app/(main)/teams/TeamProvider';
|
||||
export * from '@/app/(main)/teams/TeamsAddButton';
|
||||
export * from '@/app/(main)/teams/TeamsDataTable';
|
||||
export * from '@/app/(main)/teams/TeamsHeader';
|
||||
export * from '@/app/(main)/teams/TeamsJoinButton';
|
||||
export * from '@/app/(main)/teams/TeamsMemberAddButton';
|
||||
export * from '@/app/(main)/teams/TeamsTable';
|
||||
export * from '@/app/(main)/websites/[websiteId]/settings/WebsiteData';
|
||||
export * from '@/app/(main)/websites/[websiteId]/settings/WebsiteDeleteForm';
|
||||
|
|
|
|||
|
|
@ -55,6 +55,7 @@ export const SESSION_COLUMNS = [
|
|||
'country',
|
||||
'city',
|
||||
'region',
|
||||
'distinctId',
|
||||
];
|
||||
|
||||
export const SEGMENT_TYPES = {
|
||||
|
|
@ -69,6 +70,7 @@ export const FILTER_COLUMNS = {
|
|||
referrer: 'referrer_domain',
|
||||
domain: 'referrer_domain',
|
||||
hostname: 'hostname',
|
||||
distinctId: 'distinct_id',
|
||||
title: 'page_title',
|
||||
query: 'url_query',
|
||||
os: 'os',
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import {
|
|||
differenceInCalendarMonths,
|
||||
differenceInCalendarWeeks,
|
||||
differenceInCalendarYears,
|
||||
differenceInDays,
|
||||
differenceInHours,
|
||||
differenceInMinutes,
|
||||
endOfDay,
|
||||
|
|
@ -136,7 +137,12 @@ export function parseDateValue(value: string) {
|
|||
return { num: +num, unit };
|
||||
}
|
||||
|
||||
export function parseDateRange(value: string, locale = 'en-US', timezone?: string): DateRange {
|
||||
export function parseDateRange(
|
||||
value: string,
|
||||
unitValue?: string,
|
||||
locale = 'en-US',
|
||||
timezone?: string,
|
||||
): DateRange {
|
||||
if (typeof value !== 'string') {
|
||||
return null;
|
||||
}
|
||||
|
|
@ -146,7 +152,7 @@ export function parseDateRange(value: string, locale = 'en-US', timezone?: strin
|
|||
|
||||
const startDate = new Date(+startTime);
|
||||
const endDate = new Date(+endTime);
|
||||
const unit = getMinimumUnit(startDate, endDate);
|
||||
const unit = getMinimumUnit(startDate, endDate, true);
|
||||
|
||||
return {
|
||||
startDate,
|
||||
|
|
@ -169,14 +175,14 @@ export function parseDateRange(value: string, locale = 'en-US', timezone?: strin
|
|||
endDate: endOfHour(now),
|
||||
offset: 0,
|
||||
num: num || 1,
|
||||
unit,
|
||||
unit: unitValue || unit,
|
||||
value,
|
||||
};
|
||||
case 'day':
|
||||
return {
|
||||
startDate: num ? subDays(startOfDay(now), num) : startOfDay(now),
|
||||
endDate: endOfDay(now),
|
||||
unit: num ? 'day' : 'hour',
|
||||
unit: unitValue ? unitValue : num ? 'day' : 'hour',
|
||||
offset: 0,
|
||||
num: num || 1,
|
||||
value,
|
||||
|
|
@ -187,7 +193,7 @@ export function parseDateRange(value: string, locale = 'en-US', timezone?: strin
|
|||
? subWeeks(startOfWeek(now, { locale: dateLocale }), num)
|
||||
: startOfWeek(now, { locale: dateLocale }),
|
||||
endDate: endOfWeek(now, { locale: dateLocale }),
|
||||
unit: 'day',
|
||||
unit: unitValue || 'day',
|
||||
offset: 0,
|
||||
num: num || 1,
|
||||
value,
|
||||
|
|
@ -196,7 +202,7 @@ export function parseDateRange(value: string, locale = 'en-US', timezone?: strin
|
|||
return {
|
||||
startDate: num ? subMonths(startOfMonth(now), num) : startOfMonth(now),
|
||||
endDate: endOfMonth(now),
|
||||
unit: num ? 'month' : 'day',
|
||||
unit: unitValue ? unitValue : num ? 'month' : 'day',
|
||||
offset: 0,
|
||||
num: num || 1,
|
||||
value,
|
||||
|
|
@ -205,7 +211,7 @@ export function parseDateRange(value: string, locale = 'en-US', timezone?: strin
|
|||
return {
|
||||
startDate: num ? subYears(startOfYear(now), num) : startOfYear(now),
|
||||
endDate: endOfYear(now),
|
||||
unit: 'month',
|
||||
unit: unitValue || 'month',
|
||||
offset: 0,
|
||||
num: num || 1,
|
||||
value,
|
||||
|
|
@ -273,12 +279,20 @@ export function getAllowedUnits(startDate: Date, endDate: Date) {
|
|||
return index >= 0 ? units.splice(index) : [];
|
||||
}
|
||||
|
||||
export function getMinimumUnit(startDate: number | Date, endDate: number | Date) {
|
||||
export function getMinimumUnit(
|
||||
startDate: number | Date,
|
||||
endDate: number | Date,
|
||||
isDateRange: boolean = false,
|
||||
) {
|
||||
if (differenceInMinutes(endDate, startDate) <= 60) {
|
||||
return 'minute';
|
||||
} else if (differenceInHours(endDate, startDate) <= 48) {
|
||||
} else if (
|
||||
isDateRange
|
||||
? differenceInHours(endDate, startDate) <= 48
|
||||
: differenceInDays(endDate, startDate) <= 30
|
||||
) {
|
||||
return 'hour';
|
||||
} else if (differenceInCalendarMonths(endDate, startDate) <= 6) {
|
||||
} else if (differenceInCalendarMonths(endDate, startDate) <= 7) {
|
||||
return 'day';
|
||||
} else if (differenceInCalendarMonths(endDate, startDate) <= 24) {
|
||||
return 'month';
|
||||
|
|
|
|||
|
|
@ -20,14 +20,6 @@ const PRISMA_LOG_OPTIONS = {
|
|||
};
|
||||
|
||||
const DATE_FORMATS = {
|
||||
minute: 'YYYY-MM-DD HH24:MI:00',
|
||||
hour: 'YYYY-MM-DD HH24:00:00',
|
||||
day: 'YYYY-MM-DD HH24:00:00',
|
||||
month: 'YYYY-MM-01 HH24:00:00',
|
||||
year: 'YYYY-01-01 HH24:00:00',
|
||||
};
|
||||
|
||||
const DATE_FORMATS_UTC = {
|
||||
minute: 'YYYY-MM-DD"T"HH24:MI:00"Z"',
|
||||
hour: 'YYYY-MM-DD"T"HH24:00:00"Z"',
|
||||
day: 'YYYY-MM-DD"T"HH24:00:00"Z"',
|
||||
|
|
@ -52,7 +44,7 @@ function getDateSQL(field: string, unit: string, timezone?: string): string {
|
|||
return `to_char(date_trunc('${unit}', ${field} at time zone '${timezone}'), '${DATE_FORMATS[unit]}')`;
|
||||
}
|
||||
|
||||
return `to_char(date_trunc('${unit}', ${field}), '${DATE_FORMATS_UTC[unit]}')`;
|
||||
return `to_char(date_trunc('${unit}', ${field}), '${DATE_FORMATS[unit]}')`;
|
||||
}
|
||||
|
||||
function getDateWeeklySQL(field: string, timezone?: string) {
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ export const filterParams = {
|
|||
city: z.string().optional(),
|
||||
tag: z.string().optional(),
|
||||
hostname: z.string().optional(),
|
||||
distinctId: z.string().optional(),
|
||||
language: z.string().optional(),
|
||||
event: z.string().optional(),
|
||||
segment: z.uuid().optional(),
|
||||
|
|
@ -89,6 +90,7 @@ export const fieldsParam = z.enum([
|
|||
'city',
|
||||
'tag',
|
||||
'hostname',
|
||||
'distinctId',
|
||||
'language',
|
||||
'event',
|
||||
]);
|
||||
|
|
@ -166,6 +168,7 @@ export const journeyReportSchema = z.object({
|
|||
steps: z.coerce.number().min(2).max(7),
|
||||
startStep: z.string().optional(),
|
||||
endStep: z.string().optional(),
|
||||
eventType: z.coerce.number().int().positive().optional(),
|
||||
}),
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ async function relationalQuery(
|
|||
sum(${getTimestampDiffSQL('t.min_time', 't.max_time')}) as "totaltime"
|
||||
from (
|
||||
select
|
||||
${column} name,
|
||||
${column} as "name",
|
||||
website_event.session_id,
|
||||
website_event.visit_id,
|
||||
count(*) as "c",
|
||||
|
|
@ -72,6 +72,7 @@ async function relationalQuery(
|
|||
${filterQuery}
|
||||
group by name, website_event.session_id, website_event.visit_id
|
||||
) as t
|
||||
where name != ''
|
||||
group by name
|
||||
order by visitors desc, visits desc
|
||||
limit ${limit}
|
||||
|
|
|
|||
|
|
@ -89,7 +89,7 @@ async function relationalQuery(
|
|||
when ${toPostgresPositionClause('referrer_domain', EMAIL_DOMAINS)} or utm_medium ilike '%mail%' then 'email'
|
||||
when ${toPostgresPositionClause('referrer_domain', SHOPPING_DOMAINS)} or utm_medium ilike '%shop%' then concat(prefix, 'Shopping')
|
||||
when ${toPostgresPositionClause('referrer_domain', VIDEO_DOMAINS)} or utm_medium ilike '%video%' then concat(prefix, 'Video')
|
||||
else '' end AS name,
|
||||
else '' end as "name",
|
||||
session_id,
|
||||
visit_id,
|
||||
c,
|
||||
|
|
|
|||
|
|
@ -30,7 +30,8 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) {
|
|||
session.device,
|
||||
session.country,
|
||||
website_event.url_path as "urlPath",
|
||||
website_event.referrer_domain as "referrerDomain"
|
||||
website_event.referrer_domain as "referrerDomain",
|
||||
website_event.hostname
|
||||
from website_event
|
||||
${cohortQuery}
|
||||
inner join session
|
||||
|
|
@ -65,7 +66,8 @@ async function clickhouseQuery(websiteId: string, filters: QueryFilters): Promis
|
|||
device,
|
||||
country,
|
||||
url_path as urlPath,
|
||||
referrer_domain as referrerDomain
|
||||
referrer_domain as referrerDomain,
|
||||
hostname
|
||||
from website_event
|
||||
${cohortQuery}
|
||||
where website_id = {websiteId:UUID}
|
||||
|
|
|
|||
|
|
@ -86,7 +86,7 @@ async function relationalQuery(
|
|||
sum(${getTimestampDiffSQL('t.min_time', 't.max_time')}) as "totaltime"
|
||||
from (
|
||||
select
|
||||
${column} as name,
|
||||
${column} as "name",
|
||||
website_event.session_id,
|
||||
website_event.visit_id,
|
||||
count(*) as "c",
|
||||
|
|
|
|||
|
|
@ -52,8 +52,8 @@ async function relationalQuery(
|
|||
function getUTMQuery(utmColumn: string) {
|
||||
return `
|
||||
select
|
||||
coalesce(we.${utmColumn}, '') name,
|
||||
${currency ? 'sum(e.value)' : 'count(distinct we.session_id)'} value
|
||||
coalesce(we.${utmColumn}, '') as "name",
|
||||
${currency ? 'sum(e.value)' : 'count(distinct we.session_id)'} as "value"
|
||||
from model m
|
||||
join website_event we
|
||||
on we.created_at = m.created_at
|
||||
|
|
@ -128,7 +128,7 @@ async function relationalQuery(
|
|||
`
|
||||
${currency ? revenueEventQuery : eventQuery}
|
||||
${getModelQuery(model)}
|
||||
select coalesce(we.referrer_domain, '') name,
|
||||
select coalesce(we.referrer_domain, '') as "name",
|
||||
${currency ? 'sum(e.value)' : 'count(distinct we.session_id)'} value
|
||||
from model m
|
||||
join website_event we
|
||||
|
|
@ -166,8 +166,8 @@ async function relationalQuery(
|
|||
when coalesce(li_fat_id, '') != '' then 'LinkedIn Ads'
|
||||
when coalesce(twclid, '') != '' then 'Twitter Ads (X)'
|
||||
else ''
|
||||
end name,
|
||||
${currency ? 'sum(e.value)' : 'count(distinct we.session_id)'} value
|
||||
end as "name",
|
||||
${currency ? 'sum(e.value)' : 'count(distinct we.session_id)'} as "value"
|
||||
from model m
|
||||
join website_event we
|
||||
on we.created_at = m.created_at
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ async function relationalQuery(
|
|||
endStepQuery: string;
|
||||
params: Record<string, string>;
|
||||
} {
|
||||
const params = {};
|
||||
const params: { startStep?: string; endStep?: string } = {};
|
||||
let sequenceQuery = '';
|
||||
let startStepQuery = '';
|
||||
let endStepQuery = '';
|
||||
|
|
@ -172,7 +172,7 @@ async function clickhouseQuery(
|
|||
endStepQuery: string;
|
||||
params: Record<string, string>;
|
||||
} {
|
||||
const params = {};
|
||||
const params: { startStep?: string; endStep?: string } = {};
|
||||
let sequenceQuery = '';
|
||||
let startStepQuery = '';
|
||||
let endStepQuery = '';
|
||||
|
|
|
|||
|
|
@ -76,8 +76,8 @@ async function relationalQuery(
|
|||
const country = await rawQuery(
|
||||
`
|
||||
select
|
||||
session.country as name,
|
||||
sum(revenue) value
|
||||
session.country as "name",
|
||||
sum(revenue) as "value"
|
||||
from revenue
|
||||
${joinQuery}
|
||||
join session
|
||||
|
|
@ -176,8 +176,8 @@ async function clickhouseQuery(
|
|||
>(
|
||||
`
|
||||
select
|
||||
website_event.country as name,
|
||||
sum(website_revenue.revenue) as value
|
||||
website_event.country as "name",
|
||||
sum(website_revenue.revenue) as "value"
|
||||
from website_revenue
|
||||
any left join (
|
||||
select *
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ async function relationalQuery(websiteId: string, sessionId: string, filters: Qu
|
|||
event_type as "eventType",
|
||||
event_name as "eventName",
|
||||
visit_id as "visitId",
|
||||
hostname,
|
||||
event_id IN (select website_event_id
|
||||
from event_data
|
||||
where website_id = {{websiteId::uuid}}
|
||||
|
|
@ -60,6 +61,7 @@ async function clickhouseQuery(websiteId: string, sessionId: string, filters: Qu
|
|||
event_type as eventType,
|
||||
event_name as eventName,
|
||||
visit_id as visitId,
|
||||
hostname,
|
||||
event_id IN (select event_id
|
||||
from event_data
|
||||
where website_id = {websiteId:UUID}
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ async function relationalQuery(
|
|||
sum(${getTimestampDiffSQL('t.min_time', 't.max_time')}) as "totaltime"
|
||||
from (
|
||||
select
|
||||
${column} name,
|
||||
${column} as "name",
|
||||
${includeCountry ? 'country,' : ''}
|
||||
website_event.session_id,
|
||||
website_event.visit_id,
|
||||
|
|
@ -82,6 +82,7 @@ async function relationalQuery(
|
|||
group by name, website_event.session_id, website_event.visit_id
|
||||
${includeCountry ? ', country' : ''}
|
||||
) as t
|
||||
where name != ''
|
||||
group by name
|
||||
${includeCountry ? ', country' : ''}
|
||||
order by visitors desc, visits desc
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue